bigywater.tech
Java · 340

OSR:长循环运行中如何切换到编译代码

一个方法只被调用一次,里面的循环却要跑很久,JIT 怎么才能在循环中途换成编译后的代码?这篇用循环耗时曲线、关闭 OSR 的对照和 PrintCompilation 的 % 记录,把这件事演示出来。

0.7 vs 26 ns
OSR 开启(稳定后)与 -XX:-UseOnStackReplacement 时长循环每次迭代的耗时
@ 34 / @ 13
PrintCompilation 里 OSR 记录的字节码偏移(分别对应 main 的循环头与 Nest 的内层循环头)
2 轮
PrintCompilation 里观察到的 C1 → C2 → 作废 的循环次数(原因没有查明)

关闭 OSR 的对照

实测 · 本机执行输出
  [默认(OSR 开,分层编译)]
    每 2 百万次迭代的 ns/迭代(桶号:值):1:2.0 2:1.2 3:0.7 4:0.7 5:0.7 6:0.7 7:0.7 8:0.7 9:0.7 10:0.7 11:0.7 12:0.7 13:0.7 14:0.7
  [-XX:-UseOnStackReplacement(关闭 OSR)]
    每 2 百万次迭代的 ns/迭代(桶号:值):1:26.0 2:26.0 3:26.0 4:26.0 5:26.1 6:26.2 7:26.1 8:26.0 9:26.2 10:26.1 11:26.0 12:26.2 13:26.0 14:26.1
  [-Xint(只解释执行)]
    每 2 百万次迭代的 ns/迭代(桶号:值):1:23.8 2:23.4 3:23.5 4:23.5 5:23.5 6:23.9 7:23.7 8:23.6 9:23.5 10:24.2 11:23.9 12:23.4 13:23.6 14:23.4
  [-XX:TieredStopAtLevel=1(只用 C1)]
    每 2 百万次迭代的 ns/迭代(桶号:值):1:2.2 2:1.3 3:1.3 4:1.3 5:1.4 6:1.4 7:1.4 8:1.4 9:1.4 10:1.4 11:1.4 12:1.4 13:1.4 14:1.4
  [-XX:-TieredCompilation(只用 C2)]
    每 2 百万次迭代的 ns/迭代(桶号:值):1:1.3 2:1.3 3:0.7 4:0.7 5:0.7 6:0.7 7:0.7 8:0.7 9:0.7 10:0.7 11:0.7 12:0.7 13:0.7 14:0.7

PrintCompilation 的 OSR 记录

实测 · 本机执行输出
    17 5 % 3 Main::main @ 34 (206 bytes)
    17 6 3 Main::main (206 bytes)
    18 7 % 4 Main::main @ 34 (206 bytes)
    18 5 % 3 Main::main @ 34 (206 bytes) made not entrant
    19 7 % 4 Main::main @ 34 (206 bytes) made not entrant
    19 8 % 3 Main::main @ 34 (206 bytes)
    20 9 % 4 Main::main @ 34 (206 bytes)
    21 8 % 3 Main::main @ 34 (206 bytes) made not entrant
    38 9 % 4 Main::main @ 34 (206 bytes) made not entrant

OSR 的时间线

实测 · 本机执行输出
  (时间戳 ms,tier,@bci): [('17', '3', '34'), ('18', '4', '34'), ('18', '3', '34'), ('19', '4', '34'), ('19', '3', '34'), ('20', '4', '34'), ('21', '3', '34'), ('38', '4', '34')]
  同一次运行里各桶的耗时: 每 2 百万次迭代的 ns/迭代(桶号:值):1:2.1 2:1.2 3:0.7 4:0.7 5:0.7 6:0.7 7:0.7 8:0.7 9:0.7 10:0.7 11:0.7 12:0.7 13:0.7 14:0.7

嵌套循环

实测 · 本机执行输出
    17 5 % 3 Nest::main @ 13 (60 bytes)
    17 6 3 Nest::main (60 bytes)
    17 7 % 4 Nest::main @ 13 (60 bytes)
    18 5 % 3 Nest::main @ 13 (60 bytes) made not entrant
    19 7 % 4 Nest::main @ 13 (60 bytes) made not entrant
    20 8 % 3 Nest::main @ 13 (60 bytes)
    20 9 % 4 Nest::main @ 13 (60 bytes)
    21 8 % 3 Nest::main @ 13 (60 bytes) made not entrant
    198 9 % 4 Nest::main @ 13 (60 bytes) made not entrant
  字节码里的循环:外层循环头在偏移 4(36→39: goto 4),内层循环头在偏移 13(30→33: goto 13);PrintCompilation 里的 @ 后面是 OSR 入口的字节码偏移

分步演示

OSR
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。JDK 21.0.12,Apple Silicon;自写计时(不是 JMH)
    • 没有验证:第一轮 OSR 编译被作废(made not entrant)的原因(没有用 -XX:+TraceDeoptimization 等)、-XX:OnStackReplacePercentage 与 Tier3BackEdgeThreshold 的作用、OSR 对方法后续调用的影响、JDK 27 与 x86 上的行为、解释器到 OSR 的状态迁移细节(局部变量如何搬运)
    京ICP备2026067502号-1京公网安备11010502063487号