bigywater.tech
Java · 238

逃逸分析:对象什么时候可以不进堆

逃逸分析让不会逃出方法的对象不进堆(标量替换),甚至去掉锁。它到底什么时候生效、什么时候失效?这篇用 ThreadMXBean 的线程分配字节数逐个验证,并用 -XX:-DoEscapeAnalysis 做对照。

≈0 vs 24 字节
不逃逸的 Pt 对象每次迭代的分配:默认 / 关闭 EA
24 字节
只有 1/1024 迭代逃逸的对象,默认配置每次迭代的分配
0.59 → 6.40 ns
局部对象上的 synchronized 每次迭代:默认 → 关闭 EA

不逃逸、逃逸与条件逃逸

实测 · 本机执行输出
  [默认(C2 逃逸分析开启)]
  局部对象只用来调用 sum()(不逃逸)                                 每次迭代分配   0.01 字节,每次迭代  0.58 ns
  同样的对象,再赋给静态字段(逃逸)                                    每次迭代分配  24.00 字节,每次迭代  1.97 ns
  在方法里创建并返回,调用方立刻用完(内联后不逃逸)                            每次迭代分配   0.00 字节,每次迭代  0.58 ns
  只有 1/1024 的迭代才赋给静态字段(条件逃逸)                           每次迭代分配  24.00 字节,每次迭代  1.39 ns
  [-XX:-DoEscapeAnalysis]
  局部对象只用来调用 sum()(不逃逸)                                 每次迭代分配  24.00 字节,每次迭代  1.64 ns
  同样的对象,再赋给静态字段(逃逸)                                    每次迭代分配  24.00 字节,每次迭代  1.69 ns
  在方法里创建并返回,调用方立刻用完(内联后不逃逸)                            每次迭代分配  24.00 字节,每次迭代  1.36 ns
  只有 1/1024 的迭代才赋给静态字段(条件逃逸)                           每次迭代分配  24.00 字节,每次迭代  1.29 ns

哪些写法会让它失效

实测 · 本机执行输出
  [默认(C2 逃逸分析开启)]
  传给一个很大的、不会被内联的方法                                     每次迭代分配  20.78 字节,每次迭代  9.39 ns
  new int[8],只读写下标 0 和 7(局部小数组)                        每次迭代分配   0.00 字节,每次迭代  0.58 ns
  new int[100],只读写下标 0 和 99                            每次迭代分配 416.00 字节,每次迭代 23.47 ns
  new int[4 + (i & 3)](长度不是常量)                         每次迭代分配   0.02 字节,每次迭代  0.58 ns
  lambda 捕获一个变量并立刻调用                                   每次迭代分配   0.00 字节,每次迭代  0.31 ns
  遍历一个 3 元素 List 的 for-each(迭代器)                       每次迭代分配  32.00 字节,每次迭代  6.13 ns
  Optional.of(...).map(...).orElse(...)                每次迭代分配  32.00 字节,每次迭代  2.77 ns
  [-XX:-DoEscapeAnalysis]
  传给一个很大的、不会被内联的方法                                     每次迭代分配  24.00 字节,每次迭代  6.22 ns
  new int[8],只读写下标 0 和 7(局部小数组)                        每次迭代分配   0.02 字节,每次迭代  0.58 ns
  new int[100],只读写下标 0 和 99                            每次迭代分配 416.00 字节,每次迭代 33.13 ns
  new int[4 + (i & 3)](长度不是常量)                         每次迭代分配   0.02 字节,每次迭代  0.61 ns
  lambda 捕获一个变量并立刻调用                                   每次迭代分配  16.00 字节,每次迭代  1.11 ns
  遍历一个 3 元素 List 的 for-each(迭代器)                       每次迭代分配  32.00 字节,每次迭代  3.96 ns
  Optional.of(...).map(...).orElse(...)                每次迭代分配  32.00 字节,每次迭代  3.30 ns

装箱、StringBuilder 与锁消除

实测 · 本机执行输出
  [默认(C2 逃逸分析开启)]
  Integer.valueOf(i + 1000)(超出缓存范围)只用于求和               每次迭代分配   0.01 字节,每次迭代  0.58 ns
  Integer 放进 ArrayList 再取出                             每次迭代分配  16.00 字节,每次迭代  3.22 ns
  StringBuilder 局部 append 后取 length                    每次迭代分配   0.02 字节,每次迭代  0.03 ns
  "ab" + int 字符串拼接后取 length                            每次迭代分配   0.02 字节,每次迭代  0.03 ns
  synchronized 在局部 new Object() 上                      每次迭代分配   0.01 字节,每次迭代  0.59 ns
  [-XX:-DoEscapeAnalysis]
  Integer.valueOf(i + 1000)(超出缓存范围)只用于求和               每次迭代分配  16.00 字节,每次迭代  2.93 ns
  Integer 放进 ArrayList 再取出                             每次迭代分配  16.00 字节,每次迭代  5.07 ns
  StringBuilder 局部 append 后取 length                    每次迭代分配  56.00 字节,每次迭代  3.33 ns
  "ab" + int 字符串拼接后取 length                            每次迭代分配  48.00 字节,每次迭代  2.99 ns
  synchronized 在局部 new Object() 上                      每次迭代分配  16.00 字节,每次迭代  6.40 ns

分步演示

逃逸分析
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。JDK 21.0.12,-XX:+UseSerialGC -Xmx1g;com.sun.management.ThreadMXBean.getCurrentThreadAllocatedBytes 测当前线程的分配字节数
    • 没有验证:-XX:EliminateAllocationArraySizeLimit 调大后的效果、部分逃逸分析在 Graal 编译器中的表现、C1 的行为、JDK 17 与 27 的差别、逃逸分析失效的具体原因(只记录了现象:int[8] 在关闭 EA 后同样不分配,所以数组的结果不能完全归功于 EA;每次迭代的 ns 数字很小,可能包含循环被整体优化的效果)
    京ICP备2026067502号-1京公网安备11010502063487号