栈上替换(OSR)是将正在执行的循环从解释模式无缝切换到已编译优化机器码,不改变逻辑只更换执行方式,关键在触发条件、状态保持与编译时机。

栈上替换(OSR)不是让代码“进化”,而是让正在执行的循环体从解释模式无缝切换到已编译的优化机器码——它不改逻辑、不重写算法,只换执行方式。关键在于触发条件、状态保持和编译时机这三点。
确保循环具备 OSR 触发基础
OSR 不会为任意循环启动。JVM(或 V8 等引擎)只对满足以下条件的循环体考虑 OSR:
- 循环必须在方法内部持续执行足够长时间,被计数器反复采样(如 HotSpot 默认需循环迭代超 10,000 次)
- 循环体不能频繁出现不可预测的分支(如随机抛异常、类型剧烈变化、调用未内联的虚方法)
- 局部变量生命周期稳定,避免逃逸分析失败导致栈帧结构复杂化
- 不要在循环内动态生成新类、反射调用或修改方法句柄——这些会中断 JIT 的热点识别链
主动引导 JIT 识别并编译该循环
你无法手动“调用 OSR”,但可以显著提升其被选中的概率:
- 把计算密集逻辑尽量收拢进单一、无副作用的 for/while 循环,避免拆成多个小循环或混入 I/O、锁、日志
- 使用 final 或稳定类型的变量(如 int i 而非 Object i),帮助 JIT 推断类型和范围,加快编译决策
- 避免在循环中调用尚未被 C1/C2 编译过的方法;可先预热调用一次外部方法,再进入主循环
- 对 Java,可用 -XX:+PrintCompilation -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining 观察是否打出类似 osr_compiled_method 或 osr_nmethod 的日志
理解 OSR 切换时的真实行为
所谓“运行期进化”,本质是执行路径迁移,而非代码重构:
- JVM 会在解释执行的栈帧中保存当前循环变量值、程序计数器位置、操作数栈快照
- 新编译的 OSR 版本机器码会从“当前迭代轮次”开始执行,比如原解释器刚执行完第 10001 次迭代,OSR 启动后就从第 10002 次继续
- 不会重新初始化局部变量,也不会跳过已执行的副作用(如已修改的数组元素、已累加的 sum 值)
- 若循环中存在未被内联的调用,OSR 版本仍会走该调用,只是调用本身可能已被提前编译优化
验证与调试建议
确认 OSR 是否生效,不能只看性能提升,要查证据:
- 开启 -XX:+UnlockDiagnosticVMOptions -XX:+LogCompilation -XX:LogFile=jit.log,搜索 osr_bci(OSR bytecode index)字段
- 用 JFR(Java Flight Recorder)录制运行过程,在 Compiler Phase 事件中筛选 “OnStackReplacement” 类型
- 对比同一循环在 -Xint(纯解释)和默认混合模式下的执行时间曲线:OSR 启动后通常出现明显拐点,而非平滑上升
- V8 引擎中可通过 --trace-opt --trace-osr 查看 Sparkplug/TurboFan 是否对某 loop 执行了 OSR 编译











