一句话总结
逃逸分析是 JIT 编译器的一种优化技术,分析对象的作用域:如果对象只在方法内使用(不逃逸),就可以做三项优化:① 栈上分配(通过标量替换实现,对象字段拆分为基本类型在栈上分配,随方法结束自动销毁,无需 GC)、② 同步消除(去除不必要的锁,如 StringBuffer 在单线程中的 synchronized)、③ 标量替换(将聚合量拆分为标量,省去对象头内存)。JDK 6u23 后默认开启。
初级理解
什么是逃逸?
// ① 不逃逸:对象只在方法内使用
public void noEscape() {
User user = new User(); // user 不会逃出 noEscape() 方法
user.setName("张三");
System.out.println(user.getName());
}
// ② 方法逃逸:对象作为返回值返回
public User methodEscape() {
User user = new User();
return user; // user 逃逸到方法外部
}
// ③ 线程逃逸:对象被其他线程访问
public class ThreadEscape {
private User sharedUser; // 成员变量,可能被多线程访问
public void setUser(User u) { sharedUser = u; }
}
逃逸分析的三种优化
| 优化 | 原理 | 效果 |
| 栈上分配 | 不逃逸对象在栈上分配,方法结束自动销毁 | 减少 GC 压力 |
| 同步消除 | 不逃逸对象的锁操作可以消除 | 减少锁开销 |
| 标量替换 | 将对象拆分为基本类型字段,直接在栈上/寄存器中操作 | 减少内存占用 |
中级深入
标量替换详解
// 标量(Scalar):不可再分解的量,如 int、long、引用
// 聚合量(Aggregate):可分解的量,如对象
// 原始代码
public int calc() {
Point p = new Point(1, 2); // Point 是聚合量
return p.x + p.y;
}
// 标量替换后(等价于)
public int calc() {
int x = 1; // 标量,在栈上/寄存器中
int y = 2; // 标量
return x + y;
}
// Point 对象根本没有在堆上分配!
// 这就是"栈上分配"的实际实现方式
同步消除示例
// 原始代码
public String concat() {
StringBuffer sb = new StringBuffer(); // sb 不逃逸
sb.append("hello"); // append 是 synchronized 的
sb.append(" world");
return sb.toString();
}
// 同步消除后(等价于)
public String concat() {
StringBuffer sb = new StringBuffer();
// append 的 synchronized 被消除了!
sb.append("hello"); // 无锁
sb.append(" world"); // 无锁
return sb.toString();
}
// 这就是为什么单线程下 StringBuffer 和 StringBuilder 性能差不多
高级拓展
逃逸分析参数
# 逃逸分析(JDK 6u23+ 默认开启)
-XX:+DoEscapeAnalysis # 开启逃逸分析
-XX:-DoEscapeAnalysis # 关闭逃逸分析
# 标量替换(默认开启,依赖逃逸分析)
-XX:+EliminateAllocations # 开启标量替换
-XX:-EliminateAllocations # 关闭标量替换
# 同步消除(默认开启,依赖逃逸分析)
-XX:+EliminateLocks # 开启同步消除
-XX:-EliminateLocks # 关闭同步消除
# 查看逃逸分析结果(需要 debug 版 JVM)
-XX:+PrintEscapeAnalysis
-XX:+PrintEliminateAllocations
验证逃逸分析效果
// 测试:关闭 vs 开启逃逸分析
// -Xmx1g -Xms1g -XX:-DoEscapeAnalysis → GC 频繁
// -Xmx1g -Xms1g -XX:+DoEscapeAnalysis → 几乎无 GC
public class EscapeTest {
public static void main(String[] args) {
long start = System.currentTimeMillis();
for (int i = 0; i < 100_000_000; i++) {
alloc(); // 创建 1 亿个对象
}
System.out.println("耗时: " + (System.currentTimeMillis() - start));
}
static void alloc() {
User u = new User(); // 不逃逸,标量替换后无堆分配
u.id = 1;
u.name = "test";
}
}
// 关闭逃逸分析:大量 GC,耗时很长
// 开启逃逸分析:几乎无 GC,耗时很短
逃逸分析的局限性
逃逸分析不是万能的:1) 分析本身消耗 CPU,对象逃逸比例高时反而降低性能;2) 无法对"部分逃逸"进行优化(如对象在 if-else 的一个分支逃逸);3) 目前 HotSpot 不支持真正的栈上分配,只通过标量替换实现类似效果。
实战场景
场景:循环中创建对象
// 很多人担心循环中 new 对象会导致 GC
// 实际上如果对象不逃逸,JIT 会做标量替换
// 这段代码在高频调用下几乎不产生 GC
public void process(List<Data> list) {
for (Data d : list) {
Result r = new Result(); // r 不逃逸,标量替换
r.value = d.calc();
// 使用 r...
}
}
// 但如果把 r 加入外部集合,就逃逸了
List<Result> results = new ArrayList<>();
public void process(List<Data> list) {
for (Data d : list) {
Result r = new Result();
results.add(r); // r 逃逸了!会在堆上分配
}
}
面试模拟
Q:什么是逃逸分析?有什么作用?
A:逃逸分析是 JIT 编译器的优化技术,分析对象的作用域。如果对象不逃逸(只在方法内使用),可以做三项优化:标量替换(对象拆分为基本类型,栈上分配,减少 GC)、同步消除(去除不必要的锁)、栈上分配(通过标量替换实现)。JDK 6u23 后默认开启。
Q:Java 对象一定在堆上分配吗?
A:不一定。如果逃逸分析发现对象不逃逸,JIT 会通过标量替换将对象字段拆分为基本类型,直接在栈上/寄存器中操作,对象本身不会在堆上分配。这就是常说的"栈上分配",但 HotSpot 目前是通过标量替换实现的,不是真正的栈上分配对象。
Q:同步消除是什么?
A:如果逃逸分析发现一个对象只被一个线程访问(不逃逸到其他线程),那么对该对象的同步操作(synchronized)可以被消除。例如单线程中使用 StringBuffer,其 append() 方法的 synchronized 会被 JIT 消除,性能接近 StringBuilder。