标量替换以逃逸分析为前提,仅对不逃逸且字段全为标量、无重写equals/hashCode、无同步语义的小对象生效;成功后对象结构被彻底消除,而非移至栈上。

逃逸分析是标量替换的前提条件
标量替换不会单独发生,它必须等逃逸分析先判定对象“不逃逸”才能触发。JIT 编译器在方法编译阶段跑一遍逃逸分析,如果发现 new Point(10, 20) 这样的对象只在当前方法内使用、没被赋给 static 字段、没作为参数传进非内联方法、也没调用 getClass() 或 synchronized,才会标记为 *** object is not escaping;只有这时,标量替换才可能启动。
换句话说:逃逸分析输出的是“能不能优化”的判断,标量替换是其中一种具体优化动作。两者不是并列关系,而是因果链——没有前者,后者根本不会被考虑。
标量替换只对“可拆解”的小对象生效
即使逃逸分析通过,JIT 也不会对所有不逃逸对象都做标量替换。它要求对象满足几个硬性条件:
- 所有字段必须是标量(
int、long、boolean等基本类型,或不可变的String) - 不能含可变引用字段(比如
private List<String> tags) - 不能有重写的
hashCode()或equals(),尤其避免触发反射或新建对象 - 推荐用
record Point(int x, int y),final + 无副作用,逃逸友好度最高
一旦字段里出现 AtomicInteger 或 ConcurrentHashMap,标量替换直接失效——JIT 无法证明这些字段没有隐式同步行为。
验证标量替换是否真发生,不能只看 JVM 参数
-XX:+DoEscapeAnalysis 和 -XX:+EliminateAllocations 默认已开,但开启 ≠ 生效。真正要看运行时证据:
- 加
-XX:+PrintEscapeAnalysis,日志中出现 *** scalar replaceable 才算确认 - 用
-Xlog:gc+allocation=debug对比同一逻辑前后 Eden 区分配字节数——标量替换后应明显下降甚至归零 - 调试版 JDK 加
-XX:+PrintOptoAssembly,搜索汇编里是否还有new Point指令(没有 = 已拆解)
JIT 需要预热:单次执行或短循环无效,目标方法至少被调用数千次再观察。ZGC 下逃逸分析更积极,而 G1 在某些场景下可能更保守。
标量替换后,对象在字节码和运行时都“不存在”了
这不是把对象从堆挪到栈,而是彻底抹除对象结构。比如这段代码:
record Point(int x, int y) {}
Point p = new Point(1, 2);
return p.x() + p.y();标量替换生效后,p 不再是一个引用变量,p.x() 和 p.y() 会被替换成两个独立的局部变量(比如 int x = 1; int y = 2;),后续所有使用都直接操作这两个值。连对象头、类元信息、GC 标记位全都不再分配。
这点特别容易被忽略:你以为只是“分配位置变了”,实际是 JIT 把整个对象模型从执行路径里剪掉了。所以别指望用 System.identityHashCode(p) 去调试——它一调就破功,导致逃逸分析失败,标量替换立即退化。


















