解释器和jit编译器协同工作:启动时解释执行,热点方法由jit(c1/c2)编译为本地代码,实现冷代码解释、热代码编译的动态切换。

解释执行和JIT编译到底谁在干活?
Java程序启动时,HotSpot VM默认用解释器逐行翻译字节码成机器指令——快启动、省内存,但慢。等某个方法被频繁调用(比如循环体、热点代码),JIT编译器(如C1或C2)就悄悄把它编译成本地机器码,后续直接执行,跳过解释开销。
这不是“二选一”,而是动态切换:冷代码解释,热代码编译,编译后还可能被OSR(栈上替换)中途切入,甚至退优化(deoptimization)——比如发现类型假设错了。
- 判断是否“热”的关键参数是
-XX:CompileThreshold(默认10000次调用),但Server模式下实际触发还受-XX:+UseCounterDecay等影响 -
C1编译快、开销小,适合响应敏感场景;C2做深度优化(如逃逸分析、循环展开),但耗时长、内存高 - 用
-XX:+PrintCompilation能看到每个方法何时被编译、用的哪种编译器
为什么有些代码死活不被JIT编译?
常见现象:加了-XX:+PrintCompilation,但某个关键方法始终没输出编译日志——它可能被JIT“跳过”了。
原因往往不是性能问题,而是编译器主动放弃:
立即学习“Java免费学习笔记(深入)”;
- 方法太长(超过
-XX:MaxTrivialSize或-XX:FreqInlineSize限制),C1/C2认为编译收益低 - 包含未实现的字节码(如某些JNI回调、动态代理生成的非法指令)
- 运行时发现类未完全初始化、或存在
final字段写入竞争,触发deoptimization后被标记为“不可靠”,不再重编译 - 用了
-XX:-TieredStopAtLevel=1这类降级配置,强制只用C1或纯解释
怎么验证某段代码真正在跑编译后的版本?
光看PrintCompilation输出不够——它只说明“已提交编译任务”,不代表已生效。真正生效要等编译完成+栈上替换完成。
更可靠的验证方式:
- 用
jstack -l <pid></pid>查线程栈,如果看到JIT compiled frame或本地地址(非Interpreter字样),说明正在跑编译代码 - 配合
-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly(需安装hsdis),看是否输出汇编码(注意:仅Linux/x64/Oracle JDK 8u71+或OpenJDK 11+支持) - 对同一方法反复压测,观察
perf stat -e cycles,instructions,cache-misses指标是否显著下降——编译后指令数通常减少20%~50%
解释与JIT混用时最容易忽略的副作用
多数人只关心“快不快”,却忽略协同机制带来的隐蔽行为:
- 解释执行时,局部变量还在栈帧里;JIT后可能被分配到CPU寄存器,导致
java.lang.Thread.currentThread().getStackTrace()返回的行号偶尔偏移(尤其内联后) - 调试时断点可能“跳过”——因为JIT代码不维护完整的调试信息,
javac -g生成的LineNumberTable在编译后可能失效 - GC安全点(safepoint)位置变化:解释器每条字节码都可停,JIT代码只在特定位置(如方法入口、循环回边)设检查点,长时间计算不碰安全点会导致GC停顿飙升
这些不是bug,是设计取舍。真要稳定行为,要么关JIT(-Xint),要么用-XX:+PrintGCDetails -XX:+PrintSafepointStatistics确认安全点分布是否合理。










