OSR是JVM在方法执行中途将解释器栈帧替换为已编译优化机器码栈帧的技术,解决热循环启动慢问题而非加速循环逻辑本身;其触发需满足回边计数达阈值(默认140)、C2编译启用、循环未被内联或逃逸等条件。

OSR 是什么,它真能加速长循环?
OSR(On-Stack Replacement)不是“加速”循环的手段,而是 JVM 在循环已运行一段时间后,把正在执行的解释器栈帧替换成已编译的优化机器码栈帧的技术。它解决的是「热循环启动慢」的问题,而不是让循环逻辑本身变快。你写了个 10 万次的 for 循环,前几十次走解释执行,后面被 OSR 接管——耗时从每次 10μs 降到 0.2μs,这感觉像“加速”,但本质是 JIT 编译补上了。
触发 OSR 需要哪些条件?
OSR 不是自动对所有长循环生效的,它依赖 JVM 对“方法热度”的统计和编译策略。关键点包括:
-
-XX:+TieredStopAtLevel=1会禁用 C2 编译,直接关闭 OSR(因为 OSR 主要由 C2 触发) - 循环必须在同一个方法内,且不能被内联掉(比如被
@HotSpotIntrinsicCandidate优化成 intrinsic 后,可能跳过 OSR) - JVM 默认只对调用次数 ≥ 10000 的方法做 C2 编译;而 OSR 还额外要求该方法的「循环回边次数」(back-edge count)达到阈值,默认是
140(可通过-XX:OnStackReplacePercentage调整) - 如果循环里有未解析的虚方法调用、频繁异常、或对象逃逸明显,C2 可能放弃 OSR 编译
为什么有时候加了 -XX:+PrintCompilation 也看不到 osr_nms?
常见原因有三个:
- 你的循环太短:比如只跑几百次就退出,JVM 来不及收集足够回边计数就结束了
- 用了
-Xint或-XX:TieredStopAtLevel=1,强制停留在解释或 C1 层,C2 根本不启动 - 循环体太大或含同步块:C2 对 OSR 编译有代码大小限制(默认约 8KB 字节码),超限会静默跳过;
synchronized块也可能导致 OSR 失败,因锁状态难以安全迁移
验证是否触发,可加参数:-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:CompileCommand=print,*YourClass.yourMethod,看输出里有没有 osr_nms 标记。
OSR 真正影响性能的关键细节
OSR 栈帧替换不是原子无缝的:它需要暂停线程、复制局部变量、重写栈结构。这个过程本身有开销,且替换后首次执行仍可能触发逆优化(deoptimization),比如发现某次循环中实际类型与预测不符。所以 OSR 更适合「稳定运行数百毫秒以上」的长循环,而不是那种每次只跑几十次、频繁启停的场景。另外,System.currentTimeMillis() 这类系统调用在 OSR 编译后可能被完全消除(如果 JIT 判定其结果未被使用),但若用于日志打点,反而会因时间调用本身未被内联而拖慢 OSR 效率。


















