jit只编译hotspot代码是为了权衡编译开销与性能收益,仅对调用≥10000次或循环回边≥9333次的方法编译,通过-xx:+printcompilation等参数可验证是否编译成功。

Java的JIT为什么只编译hotspot代码,而不是所有代码?
因为编译本身有开销:生成本地机器码要耗CPU、内存,还可能拖慢启动速度。JIT不追求“全量编译”,而是赌——把运行最频繁、对性能影响最大的那部分代码(即hotspot)编译掉,其余仍走解释执行。这是权衡后的务实选择。
实际中,一段代码要被识别为hotspot,得满足两个硬条件:
- 方法调用次数 ≥
CompileThreshold(默认10000,Client VM是1500) - 循环回边(back-edge)执行次数 ≥
OnStackReplacePercentage(默认9333,基于CompileThreshold动态算)
注意:hotspot不是按“代码行”或“方法名”静态标记的,而是在运行时由HotSpot VM的methodData结构实时统计+触发的。你改了代码逻辑但没触发阈值,它就永远不编译。
怎么确认某段代码真被JIT编译了?
靠日志,不是靠猜。加JVM参数打开编译日志是最直接的方式:
立即学习“Java免费学习笔记(深入)”;
-XX:+PrintCompilation -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining
输出里看到类似这样的行,就说明成功了:
123 45 3 java.lang.String::hashCode (67 bytes)
其中45是编译ID,3表示编译级别(3=C1,4=C2),(67 bytes)是生成的本地码大小。如果只看到made not entrant或made zombie,说明这段代码被优化后又废弃了——这很常见,尤其在开启-XX:+TieredStopAtLevel=1时。
容易踩的坑:
- 没加
-XX:+UnlockDiagnosticVMOptions,-XX:+PrintInlining会静默失效 - 在IDE里跑main方法,往往达不到默认10000次调用,建议用循环压测+
-XX:CompileThreshold=100临时调低 -
PrintCompilation不显示失败原因,想查具体为何没编译,得配合-XX:+TraceClassLoading和-XX:+LogCompilation(生成hotspot_pid*.log)
invokedynamic和Lambda表达式怎么影响hotspot判定?
它们让热点探测变复杂了。Lambda本质是通过invokedynamic指令绑定到CallSite,而CallSite可能被多次重写(如第一次指向解释版,后续指向编译版)。JIT不会直接编译Lambda体,而是编译它最终绑定的那个目标方法(比如lambda$foo$0这种合成方法)。
这意味着:
- 如果你反复创建新Lambda(如在循环里写
list.forEach(x -> {...})),每次都是新CallSite,统计归零,大概率编译不了 -
invokedynamic本身的解析开销会被计入方法调用计数,但它自己不会被编译——真正被编译的是它最终跳转到的那个方法 - 用
-XX:+PrintAssembly(需hsdis)看汇编时,搜不到lambda$名,得找它实际委托的方法名
为什么有些代码明明很热,却始终没被JIT?
最常见的三个硬性拦截点:
-
方法太大:默认超过8000字节字节码(由-XX:MaxTrivialSize和-XX:MaxInlineSize等控制)直接不进C1/C2编译队列 -
栈上替换(OSR)失败:长循环里刚达到回边阈值,但此时栈帧太深或有未处理异常,JIT放弃OSR,继续解释执行 -
去优化(deoptimization)太频繁:比如类型推测失败(java.lang.ClassCastException在编译后抛出),VM会把已编译方法标为not entrant,并降级回解释——这时你再看PrintCompilation,会发现同一方法ID反复出现又消失
这些机制不是bug,是JIT在“激进优化”和“语义正确”之间拉锯的结果。越想压榨性能,越得接受这种动态性——它不像AOT那样一锤定音,也不像纯解释器那样稳定可预测。
真正难的不是理解探测逻辑,而是当性能卡点出现在“本该编译却没编译”的时候,得一层层剥开CompileCommand、PrintInlining、LogCompilation输出,看清到底是阈值没到、被拒之门外,还是刚编完就被踢出去了。










