逃逸分析是jvm在编译期静态推断对象引用是否可能被方法外访问,若确定不逃逸则支持栈上分配或标量替换等优化,其结果高度依赖方法内联与调用上下文。

逃逸分析到底在分析什么
它不是分析代码“会不会跑飞”,而是 JVM 在编译期(或 JIT 编译时)静态推断一个对象的引用是否可能被方法外访问。如果确定不会——比如只在当前方法栈帧内创建、使用、销毁,那这个对象就“没逃逸”。只有确认不逃逸,JVM 才敢做后续优化。
-
new出来的对象不一定堆分配:逃逸分析通过后,JVM 可能直接把它拆开或放在栈上 - 逃逸与否取决于引用传播路径,不是对象大小或是否 final
- 开启逃逸分析需配合
-XX:+DoEscapeAnalysis(HotSpot 8u60+ 默认开启,但某些场景下会被自动关闭)
栈上分配为什么经常失效
栈上分配(Stack Allocation)是逃逸分析的常见结果之一,但实际中极少看到明显效果,原因很实在:
- 方法内联没发生:
foo()调用bar(),而bar()返回了对象引用 → 逃逸判定失败 - 对象被写入静态字段、实例字段、数组、集合(哪怕只存了一次)→ 直接标记为全局逃逸
- 使用了同步块(
synchronized)且锁对象是该实例 → JIT 为保证锁膨胀一致性,强制堆分配 - JVM 认为栈帧太大,或当前线程栈空间紧张 → 主动放弃(日志里会显示
stack allocation failed: too large)
标量替换真正生效的条件
标量替换(Scalar Replacement)比栈上分配更激进:它把对象“拆成零件”,只保留用到的字段,丢掉对象头、锁信息、GC 元数据等。但它有硬性前提:
- 对象必须不逃逸(逃逸分析通过)
- 对象类不能被动态代理增强(如 Spring CGLIB、Hibernate Javassist 生成的子类)
- 字段不能是对象引用(除非那个引用本身也不逃逸,且也支持标量替换)
- JIT 编译器得“看得懂”所有字段访问:比如
obj.x + obj.y可拆;但obj.toString()或反射访问 → 替换中止
示例:
立即学习“Java免费学习笔记(深入)”;
public int calc() {
Point p = new Point(1, 2); // Point 是 final 类,x/y 是 int
return p.x + p.y;
}
JIT 可能完全不生成 Point 实例,直接用常量 1 和 2 运算。
怎么验证这些优化真发生了
别信文档,看 JIT 日志最可靠。启动参数加:-XX:+UnlockDiagnosticVMOptions -XX:+PrintEscapeAnalysis -XX:+PrintOptoAssembly
-
PrintEscapeAnalysis会输出每类方法的逃逸结论,如point not escaped或point is global escape - 注意:日志里出现
scalar replaced才表示标量替换成功;allocated on stack极少见,多数时候是“没逃逸 + 标量替换”一起发生 - 禁用优化反而容易观察:加
-XX:-DoEscapeAnalysis后对比 GC 日志里的对象分配速率,差异明显
逃逸分析的复杂点不在原理,而在它高度依赖 JIT 的内联决策和上下文快照——同一个方法,在不同调用链下,逃逸结论可能完全不同。你写的 new,最终在哪分配,得看它“遇见了谁”。









