逃逸分析是jvm在jit编译阶段判断对象是否逃逸出方法或线程的静态+动态数据流分析,仅当对象完全不发生方法逃逸、线程逃逸和实例逃逸时,才标记为not escaping,进而触发标量替换——即彻底消除new指令,将字段平铺为栈帧或寄存器中的独立标量变量。

Java 中 JVM 并不是“把对象拆解为基本类型”这个动作本身,而是通过逃逸分析确认对象完全不逃逸后,由 JIT 编译器在生成本地代码时,直接跳过对象创建过程,只保留其字段作为独立的局部变量(标量),存入栈帧或 CPU 寄存器中。
逃逸分析是前提,不是优化动作
它不改变字节码,也不分配内存,只是在 JIT 编译阶段做的一次静态+动态数据流分析,回答一个问题:这个对象会不会被当前方法之外的代码访问?只有答案是“完全不会”,才可能触发后续优化。
- 方法逃逸:对象被返回、传参给其他方法(包括 System.out.println、logger.info 等任意非 final 方法)
- 线程逃逸:赋值给 static 字段、共享集合、或被其他线程可见的 this 字段
- 实例逃逸:被存入 this 的可外部访问字段(比如 new A().field = obj)
三者全无,JVM 才标记该对象为 not escaping,进入下一步判断。
标量替换才是真正的“拆解”机制
所谓“拆解”,不是运行时把堆上对象切开,而是在编译期彻底消除 new 指令,把对象字段平铺为局部变量。例如:
Point p = new Point(1, 2); // record Point(int x, int y) { ... }
int sum = p.x + p.y;
JIT 可能编译为:
- 两个 int 局部变量:
x = 1、y = 2 - 直接计算:
sum = x + y - 全程没有 Point 对象头、没有内存地址、无法反射、无法调用 getClass() 或 synchronized
如果字段本身也是简单对象(如内部含 final int z),且也未逃逸,JIT 还会递归拆解;但一旦遇到 String、List、Object[] 或任何不可静态判定的引用字段,整条链就中断。
包装类(Integer/Long)无法被拆解
即使写 Integer i = 42; 且 i 完全未逃逸,JVM 也不会把它变成栈上的 int。因为:
- Integer 是真实类,有身份、哈希码、同步能力、可被 == 比较(涉及缓存逻辑)
- HotSpot 明确禁止对 JDK 内置包装类做标量替换(安全与兼容性考虑)
- 自动装箱本质仍是 new Integer(x),逃逸分析看到的是对象引用,不是数值语义
想让数值真正留在栈上,唯一可靠方式是:直接用 int、long 等基本类型声明和运算。
怎么确认它真的发生了
不能靠猜,得看 JVM 运行时输出:
- 加参数:
-XX:+UnlockDiagnosticVMOptions -XX:+PrintEscapeAnalysis -XX:+PrintEliminateAllocations - 关键日志:
*** object is not escaping和*** scalar replaceable - 更硬核验证:
-XX:+PrintOptoAssembly查反汇编,确认没有new Point指令,只有寄存器直赋(如mov %r11, 1)
GC 日志中同一逻辑的堆分配字节数下降,也是间接证据。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











