逃逸分析和栈上分配

一句话速记

逃逸分析 = 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
  • 三种优化:栈上分配 / 标量替换(最重要)/ 同步消除
  • 标量替换 = 对象拆字段存寄存器,对象根本不创建
  • 默认开启:DoEscapeAnalysis EliminateAllocations EliminateLocks
  • 局限:只对 JIT 编译过的热点代码有效,对象一旦赋给字段/返回值就无效
  • 一句话:「逃逸分析让短命小对象绕开 GC——JIT 直接把它拆碎放寄存器里」

状态

  • 已背速记
  • 能讲通俗版
  • 能答追问