逃逸分析和栈上分配
一句话速记
逃逸分析 = JIT 编译器分析对象的作用域,判断对象是否”逃逸”出方法或线程。没有逃逸的对象有三种优化:① 栈上分配(随栈帧销毁,无需 GC)② 标量替换(对象被拆成字段存寄存器/栈)③ 同步消除(锁只有当前线程用,直接去掉 synchronized)。核心效果:减少堆分配压力、减少 GC 频率、减少锁竞争开销。
通俗解释(5 分钟版)
什么是对象逃逸:
// 不逃逸:只在方法内部用
void foo() {
Point p = new Point(1, 2); // p 不逃逸出方法
int result = p.x + p.y;
}
// 逃逸到堆:被赋给字段 / 返回 / 传给其他线程
Point p = new Point(1, 2); // 赋给实例字段 → 逃逸
return new Point(1, 2); // 作为返回值 → 逃逸
executor.submit(() -> use(p)); // 被 lambda 捕获传给其他线程 → 逃逸三种优化效果:
① 栈上分配:不逃逸的对象直接在栈帧上分配,方法返回时自动释放,GC 不管。
② 标量替换(更常用):
// 源码
Point p = new Point(1, 2);
int sum = p.x + p.y;
// 标量替换后(编译器直接把字段打散)
int px = 1;
int py = 2;
int sum = px + py;
// Point 对象根本不创建了!③ 同步消除:
// 源码
synchronized (new Object()) { // 锁对象不逃逸,只有当前线程用
// do something
}
// JIT 直接去掉这个 synchronized关键细节
1)逃逸分析的作用域
逃逸分析是 JIT 编译时做的(不是 javac 编译期),基于方法内联后的代码分析——所以方法内联深度影响逃逸分析效果。
方法内联 → 扩大了分析范围 → 更多对象能证明不逃逸
内联深度有上限(-XX:MaxInlineLevel=9 默认)→ 超过则放弃
2)标量替换比栈上分配更常见
原因:真正的”栈上分配”要求栈帧能容纳对象(含 header + 字段),对于复杂对象实现较难。而标量替换本质是把对象拆成字段,字段直接放寄存器/栈,更容易被 JIT 执行。
JVM 参数:
-XX:+DoEscapeAnalysis # 默认开启(JDK 6u23+)
-XX:+EliminateAllocations # 标量替换,默认开启
-XX:+EliminateLocks # 同步消除,默认开启
-XX:-DoEscapeAnalysis # 关闭逃逸分析(调试用)3)逃逸分析的局限
场景 结果
─────────────────────────────────────────────────────
对象赋给实例字段 必定逃逸
对象作为方法返回值 逃逸
Lambda 捕获局部变量传给其他线程 逃逸
方法未被内联(太大/太复杂) 分析不到 → 不优化
反射创建的对象 逃逸(JIT 无法分析)
4)实际影响有多大?
场景:高频调用的工具方法(比如 DTO.of(a,b,c) 每次创建一个包装对象):
// 优化前:每次调用堆分配一个 DTO
DTO result = DTO.of(userId, orderId); // new DTO(...)
// 如果 DTO 不逃逸,JIT 可标量替换:
// long userId_ = userId;
// long orderId_ = orderId;
// → DTO 对象不创建,减少 GC 压力测试验证:用 -XX:+PrintEscapeAnalysis 或 JMH 基准测试对比有无逃逸分析的 allocation rate。
5)常见误区
误区:逃逸分析等于栈上分配,所有小对象都在栈上。
实际:真正栈上分配在 HotSpot 里目前主要靠标量替换实现,“纯栈上分配”(保持对象完整在栈上)HotSpot 的实际支持有限——Graal VM 对此支持更好。
误区:逃逸分析在解释执行模式下有效。
实际:逃逸分析只在 **JIT 编译代码(C2 编译器)**中生效,解释模式无效。热点方法才会被 JIT,冷路径代码不受益。
延伸追问
- Q:怎么验证某个对象是否被栈上分配/标量替换?
→ 用
-XX:+PrintEscapeAnalysis -XX:+PrintEliminateAllocations输出分析结果;或用 JMH +async-profiler的 allocation profiler 看 GC allocation rate 变化。 - Q:Lambda 一定会导致对象逃逸吗? → Lambda 被编译成匿名类实例,如果只在方法内部调用(不传给其他线程、不赋给字段)有时也能被分析为不逃逸——但实践中 lambda 大多捕获外部变量,逃逸分析通常无法优化。
- Q:逃逸分析与 GraalVM 的关系? → GraalVM (Graal JIT) 对逃逸分析更激进,做了更多标量替换和真正的栈上分配(Partial Escape Analysis),在 benchmark 上比 HotSpot C2 分配更少。GraalVM AOT (native-image) 更是完全静态分析,很多对象根本不存在。
我的记法
- 逃逸分析是 JIT 做的,不是 javac
- 三种优化:栈上分配 / 标量替换(最重要)/ 同步消除
- 标量替换 = 对象拆字段存寄存器,对象根本不创建
- 默认开启:
DoEscapeAnalysisEliminateAllocationsEliminateLocks - 局限:只对 JIT 编译过的热点代码有效,对象一旦赋给字段/返回值就无效
- 一句话:「逃逸分析让短命小对象绕开 GC——JIT 直接把它拆碎放寄存器里」
状态
- 已背速记
- 能讲通俗版
- 能答追问