bigywater.tech
Java · 392

Graal JIT:可编程编译器如何优化 Java

Graal 是用 Java 写成、可以替换 C2 的 JIT 编译器。这篇在同一个 JDK 上只切换编译器,量出它的逃逸分析、稳态吞吐、预热曲线和内存成本,并如实记录一个预期没有出现的优化。

0 → 24 字节
关闭 Graal 的部分逃逸分析后,分支内逃逸场景下每次调用的分配量
4 / 5
5 个小负载中 C2 比 Graal 更快的数量(社区版 Graal 25.0.2)
250 vs 205 MiB
同一个 6 秒负载下 Graal(libgraal)与 C2 的峰值常驻内存

同一个 JDK 上的三种编译器

实测 · 本机执行输出
  [C1(TieredStopAtLevel=1)] UseJVMCICompiler = false,  EnableJVMCI = true,  UseJVMCINativeLibrary = true,  TieredStopAtLevel = 1
  [C2] UseJVMCICompiler = false,  EnableJVMCI = true,  UseJVMCINativeLibrary = true,  TieredStopAtLevel = 4
  [Graal JIT(默认)] UseJVMCICompiler = true,  EnableJVMCI = true,  UseJVMCINativeLibrary = true,  TieredStopAtLevel = 4

逃逸分析

实测 · 本机执行输出
  v1:对象在罕见分支(约每 100 万次一次)里存入普通静态字段,分支之后还读 p.x、p.y;v2:同样但存入 volatile 静态字段;v3:分支条件永远不成立(i == -5);v4:罕见分支存入另一个静态字段 holder,之后还读 p;v5:罕见分支里逃逸的是另一个新对象,p 本身不逃逸;v6:先读完 p.x + p.y,罕见分支里再把 p 存入静态字段,分支之后不再用 p
  [C2] v1 23.86 字节;  v2 24.00 字节;  v3 0.00 字节;  v4 24.00 字节;  v5 0.00 字节;  v6 24.00 字节
  [Graal JIT(默认)] v1 24.00 字节;  v2 24.00 字节;  v3 0.00 字节;  v4 24.00 字节;  v5 0.00 字节;  v6 24.00 字节
  [Graal JIT + -Djdk.graal.PartialEscapeAnalysis=false] v1 24.00 字节;  v2 24.00 字节;  v3 24.00 字节;  v4 24.00 字节;  v5 24.00 字节;  v6 24.00 字节
  [C1(TieredStopAtLevel=1)] v1 24.00 字节;  v2 24.00 字节;  v3 24.00 字节;  v4 24.00 字节;  v5 24.00 字节;  v6 24.00 字节

稳态吞吐

实测 · 本机执行输出
  [C1(TieredStopAtLevel=1)]
      stream   每毫秒 188.7 次(中位数;最小 182.5 最大 189.6)
      poly3    每毫秒 328.1 次(中位数;最小 327.8 最大 328.2)
      poly2    每毫秒 384.7 次(中位数;最小 382.4 最大 385.0)
      string   每毫秒 148.6 次(中位数;最小 148.5 最大 148.8)
      hash     每毫秒 148.5 次(中位数;最小 148.2 最大 148.6)
  [C2]
      stream   每毫秒 857.8 次(中位数;最小 848.7 最大 866.0)
      poly3    每毫秒 339.6 次(中位数;最小 338.3 最大 341.8)
      poly2    每毫秒 1386.5 次(中位数;最小 1385.2 最大 1387.2)
      string   每毫秒 409.8 次(中位数;最小 408.3 最大 414.4)
      hash     每毫秒 250.1 次(中位数;最小 249.6 最大 250.6)
  [Graal JIT(默认)]
      stream   每毫秒 654.3 次(中位数;最小 645.1 最大 685.5)
      poly3    每毫秒 353.9 次(中位数;最小 353.7 最大 354.0)
      poly2    每毫秒 423.9 次(中位数;最小 423.8 最大 423.9)
      string   每毫秒 167.1 次(中位数;最小 147.0 最大 179.9)
      hash     每毫秒 231.7 次(中位数;最小 227.5 最大 232.7)

预热曲线

实测 · 本机执行输出
  [C1(TieredStopAtLevel=1)] 第 1 批 112.2 ms,第 2 批 108.4,第 5 批 108.9,第 10 批 108.4,第 20 批 108.7,第 40 批 108.3;前 40 批合计 4345 ms
  [C2] 第 1 批 35.2 ms,第 2 批 25.4,第 5 批 25.1,第 10 批 24.0,第 20 批 24.6,第 40 批 23.5;前 40 批合计 984 ms
  [Graal JIT(默认)] 第 1 批 43.8 ms,第 2 批 30.7,第 5 批 30.1,第 10 批 30.9,第 20 批 31.3,第 40 批 31.0;前 40 批合计 1246 ms

资源成本

实测 · 本机执行输出
  [C1(TieredStopAtLevel=1)] 6 秒内完成 1,104,000 次;峰值常驻内存 195 MiB;real 6.05 s,user 6.03 s,sys 0.04 s
  [C2] 6 秒内完成 5,116,000 次;峰值常驻内存 205 MiB;real 6.04 s,user 6.10 s,sys 0.03 s
  [Graal JIT(默认)] 6 秒内完成 3,950,000 次;峰值常驻内存 250 MiB;real 6.05 s,user 6.11 s,sys 0.04 s

分步演示

Graal JIT
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。GraalVM Community 25.0.2+10.1(JDK 25.0.2+10-jvmci-b01),macOS arm64;吞吐是 7 轮取中位数的自写循环,不是 JMH;每种负载只有一个小工作函数
    • 没有验证:Oracle GraalVM(含 Graal 企业版优化)、libgraal 与 jargraal 的区别、更大的真实应用与长时间运行(GC 压力下的行为)、Linux x86_64 平台、v1 / v2 / v4 / v6 没有被消除的原因(只确认了现象)、-Djdk.graal.* 的其它优化开关
    京ICP备2026067502号-1京公网安备11010502063487号