JIT编译要求必须预热,因JVM执行分解释→C1→C2三阶段,前几万次调用在慢速解释模式,需预热让JIT完成观察、编译与优化;单次测量受GC、调度等干扰不可靠,须多轮重复取统计值;死代码消除需Blackhole消费结果规避。

预热和重复测量不是可选项,而是JMH基准测试的强制前提。跳过它们,测出来的数字大概率既不代表启动态、也不代表稳态,等于没测。
为什么JIT编译器要求必须预热
JVM执行Java字节码分三阶段:解释执行 → C1编译(客户端编译器)→ C2编译(服务端编译器)。前几万次调用基本在解释模式下跑,慢;等JIT把热点方法编译成本地机器码后,性能可能提升5倍以上。如果你的@Benchmark方法只跑100次,它永远卡在解释执行里;跑10万次又可能刚进C2就结束了——结果不可比。
预热就是给JIT留出“观察+编译+优化”的时间窗口。JMH默认用@Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS),意思是:先不计时,连续跑3轮、每轮1秒,让JIT完成编译并稳定下来。
- 预热轮数太少(如
iterations = 1),C2可能根本没来得及介入 - 预热时间太短(如
time = 100, timeUnit = TimeUnit.MILLISECONDS),连C1都未必完成 - 预热和测量用不同参数(比如预热用
Mode.Throughput,测量用Mode.AverageTime),会导致JIT编译路径不一致,数据失真
为什么单次测量不可信,必须重复多轮
即使JIT已稳定,单轮测量仍受多种干扰:GC停顿、OS线程调度抖动、TLB/Cache预热未满、CPU频率动态升降。一次@Measurement(iterations = 1)的结果,可能比真实值高或低20%以上。
JMH默认@Measurement(iterations = 5),每轮独立计时、独立统计,最后取中位数或置信区间。这不是为了“凑数”,而是用统计手段过滤噪声。
- 轮数太少(如
iterations = 2),无法识别异常值;iterations = 1直接失去统计意义 - 每轮时间太短(如
time = 100, timeUnit = TimeUnit.MILLISECONDS),GC可能插进来打断,导致某轮耗时虚高 - 忽略
@Fork配置,默认复用同一JVM进程,上一轮残留状态(如缓存、对象分配压力)会污染下一轮
死代码消除怎么绕过预热也拦不住
预热解决不了JIT的“死代码消除”问题。如果@Benchmark方法返回void且内部无副作用(比如只算一个局部变量,结果没被读取),JIT可能直接把整个循环干掉——你看到的“纳秒级耗时”,其实是空循环。
正确做法是用Blackhole消费结果:
@Benchmark
public void testSomething(Blackhole blackhole) {
int result = doSomething();
blackhole.consume(result); // 强制JIT保留计算逻辑
}
- 漏写
blackhole.consume(),哪怕预热10轮、测量20轮,结果仍是0纳秒 - 用
System.out.println()或volatile字段替代Blackhole,会引入I/O或内存屏障开销,污染基准值 -
Blackhole必须作为方法参数注入,不能自己new——JMH要控制它的生命周期
预热和重复测量的本质,是把JVM从“不确定状态”逼进“可控稳态”。但这个过程本身很脆弱:参数错一点、写法差一毫,数据就废。别指望靠“多跑几次”弥补设计缺陷,每个@Warmup和@Measurement的值,都得对应你实际想回答的问题。



















