⭐ 推荐:社区规则条款 V1.0

Java 的逃逸分析?

GJ论坛站长
GJ论坛站长 管理员资深会员 👑黑卡会员
发布于 2026-10-22 04:56 ·6 浏览 ·0 回复
内容摘要

解释 Java 逃逸分析如何判断对象不逃逸、方法逃逸或线程逃逸,并说明标量替换、分配消除、锁消除等优化,以及通过 JVM 参数和示例验证其默认开启与开关效果。

学完这篇你能搞清楚 Java 逃逸分析到底在分析什么、它换来了哪些实际优化,以及怎样用 JVM 参数亲手验证——不用背结论,自己跑一遍就知道。

第一步:先弄明白「逃逸」指的是什么

逃逸分析(Escape Analysis)是 JIT 编译器在把字节码编译成本地机器码时做的一项分析:判断一个 new 出来的对象,会不会「跑出」它被创建的方法或线程。判断结果有三种状态:

  • 不逃逸:对象只在方法内部使用,方法返回后就没人能引用它。
  • 方法逃逸:对象被作为返回值返回、赋给静态字段/实例字段、存进数组或集合、传给其他方法。
  • 线程逃逸:对象被别的线程拿到,比如赋给静态变量、放进共享队列。

只有「不逃逸」的对象,后面那些优化才有机会生效。

第二步:看懂它能带来哪四项优化

逃逸分析的产出不是「把对象放栈上」这一句话,而是四个具体动作:

  1. 标量替换:把对象的字段拆成几个局部变量(标量),直接放进栈帧或寄存器,对象本身根本不创建。这才是大家口中「栈上分配」的真实实现方式。
  2. 分配消除:连字段都不需要保留时(比如创建后只读一个字段),整条 new 指令被删掉。
  3. 锁消除:如果 synchronized 锁住的对象不逃逸,别的线程根本拿不到它,monitor 的加锁解锁指令直接删掉。
  4. 同步消除的连带的读消除:锁没了,锁保护的字段读取也可能被优化掉。

第三步:判断你的代码会不会逃逸

写循环时最容易踩的坑,就是明明想「临时对象」,却无意间让它逃逸了。下面几种写法都会让对象逃逸:

static List<Point> cache = new ArrayList<>();
Point p = new Point(1, 2);
cache.add(p);                    // 逃逸:进入共享集合
this.point = new Point(1, 2);    // 逃逸:赋给实例字段
return new Point(1, 2);          // 逃逸:作为返回值
arr[0] = new Point(1, 2);        // 逃逸:写入数组元素

不逃逸的典型形态是:对象在方法内创建、只调用它的方法或读取字段、不出方法边界。

注意:赋给 static 字段、实例字段,写成数组元素,或者被 lambda/匿名内部类捕获,都会被判定为逃逸,哪怕你觉得「实际上只有一个线程在用」。

第四步:动手验证,用参数开关对比

先确认参数状态。JDK 8 及以上版本里,逃逸分析默认是开的,用下面这条命令查(Linux/macOS 用 grep,Windows 用 findstr):

java -XX:+PrintFlagsFinal -version | grep -E "DoEscapeAnalysis|EliminateAllocations|EliminateLocks"

三个值应该都是 true。相关参数是:

  • -XX:+DoEscapeAnalysis:总开关,JDK 6u23 之后默认开启
  • -XX:+EliminateAllocations:标量替换
  • -XX:+EliminateLocks:锁消除

再写个最小验证程序:

public class EscapeDemo {
    static class Point { int x, y; Point(int x, int y){ this.x = x; this.y = y; } }

    static int sum() {
        int s = 0;
        for (int i = 0; i < 1_000_000; i++) {
            Point p = new Point(i, i);   // 不逃逸
            s += p.x + p.y;
        }
        return s;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) System.out.println(sum());
    }
}

用 -Xmx64m 跑,加 -Xlog:gc(JDK 9+)或 -XX:+PrintGCDetails(JDK 8)看 GC 次数;再追加 -XX:-DoEscapeAnalysis 跑一次对比。开启时如果没有发生 GC,说明这一百万个 Point 压根没被分配出来。

想看更底层的信息,可以加 -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining 观察方法内联情况。注意 -XX:+PrintEscapeAnalysis 这个参数只在 fastdebug 版本的 JDK 上可用,正式版传进去会报 Unrecognized VM option,这属于正常现象,不是你的问题。

注意:sum() 要被执行足够多次(默认 -XX:CompileThreshold=10000)才会触发 C2 编译,优化才生效。只跑一两次的代码,逃逸分析基本帮不上忙。另外,逃逸分析只在 C2(Server 编译器)里实现,用 -Xint 纯解释执行时完全不生效。

第五步:记住几条边界,别为它改代码风格

  • 对象一旦逃逸,四项优化全部失效,所以优化效果在「对象小、生命周期短、循环内创建」的场景最明显。
  • 方法内联会改变逃逸范围:内联后创建点和使用点出现在同一个编译单元里,原本逃逸的写法也可能被优化。
  • 大对象、字段很多的对象,标量替换要付出拆解成本,不一定划算,JIT 会自己权衡。
  • 别为了迎合逃逸分析把代码写成反人类的形态,可读性优先;真实瓶颈请用 JMH 或 async-profiler 测,别靠猜。

小结:

  1. 逃逸分析判断对象是否逃出方法/线程,分不逃逸、方法逃逸、线程逃逸三种。
  2. 它带来的实际优化是标量替换、分配消除、锁消除,「栈上分配」只是这些优化的俗称。
  3. JDK 8 起默认开启,用 -XX:+PrintFlagsFinal 确认 DoEscapeAnalysis 为 true。
  4. 验证靠循环 + 小堆 + GC 日志对比,而且必须跑到 JIT 编译阈值以上。
  5. 对象赋给字段、数组、返回值、被内部类捕获都会逃逸,此时优化全部失效。
  6. 别为优化牺牲可读性,先用工具定位真实热点再说。
本文转载自 GJ站长论坛,原文地址:https://www.gj0.com/thread-2369.html
转载请注明出处,版权归原作者所有。
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 0

还没有回复,来抢沙发~