system.nanotime()是唯一可靠的纳秒级计时器,因其单调、高分辨率且不受系统时钟调整影响;基准测试需预热、多轮采样、取中位数均值以消除jit和干扰。

为什么 System.nanoTime() 是唯一靠谱的纳秒级计时器
Java 中只有 System.nanoTime() 提供真正与系统无关、单调递增、高分辨率(通常达纳秒级)的时间源;System.currentTimeMillis() 仅毫秒精度,且受系统时钟调整影响,完全不能用于性能测量。JVM 本身不保证纳秒级绝对精度(底层依赖 CLOCK_MONOTONIC 或类似机制),但它的差值(end - start)在单次 JVM 运行中是稳定、可比、无漂移的——这正是性能对比所需的全部条件。
怎么写一个安全、可复现的基准测量代码段
直接调用一次 System.nanoTime() 测量单次执行毫无意义:JIT 编译、分支预测、CPU 频率波动、TLB 缺失都会淹没真实耗时。必须做多轮预热 + 多轮采样,并剔除异常值。
- 先用空循环或目标方法调用 10k–100k 次「预热」,触发 JIT 编译和类初始化(避免把
ClassLoader开销算进结果) - 正式测量至少 100 轮,每轮执行 1k–10k 次目标代码(减少计时器开销占比)
- 每次记录总耗时(
endNanos - startNanos),再除以次数得单次均值;不要记录单次纳秒值——它可能低至 0(低于计时器分辨率)或突增至几百纳秒(GC 抢占、中断) - 最终取所有轮次均值的中位数,比平均值更能抵抗毛刺干扰
示例关键片段:
long start = System.nanoTime(); for (int i = 0; i <h3>对比两个算法时最容易忽略的三个陷阱</h3><p>对比 A 和 B 的性能,不是分别跑一遍然后比数字——变量控制稍有偏差,结论就不可信。</p>
-
共享状态污染:如果 A 和 B 共用同一个
ArrayList或缓存,B 的运行可能受益于 A 触发的 CPU cache 预热或对象晋升到老年代,导致 B 显著“变快”。应为每个算法分配独立实例,或在每次测量前重置/重建全部依赖对象 - JIT 状态不同步:A 跑完后 JIT 已优化其字节码,但 B 可能还没被编译。务必让 A 和 B 各自完成完整预热(各自跑够万次),再进入正式测量轮次
- 测量粒度失真:若 A 执行快(如 20ns),B 执行慢(如 150ns),但你只测单次,
System.nanoTime()的实际分辨率可能是 10–50ns,A 的测量值会大量集中在 0 或 10ns,B 则分散在 140–170ns——此时直接比均值会低估 A 的优势。应提高每轮执行次数(比如 100k 次),让总耗时远大于计时器误差
什么时候该换 JMH 而不是手写 nanoTime
手写 System.nanoTime() 测量只适用于快速验证、调试热点、或嵌入已有测试流程。一旦涉及严谨对比(比如发版前性能回归)、多参数组合(不同数据规模、不同 JVM 参数)、或需要 GC/内存分配统计,立刻停用手工方案。
-
JMH自动处理预热、fork JVM 进程隔离、多轮采样、统计显著性检验(如 Mann-Whitney U test),还能生成perfasm火焰图 - 手写代码无法规避「Blackhole」问题:JVM 可能因结果未被使用而彻底优化掉你的被测逻辑(尤其是纯计算无副作用的方法),
JMH通过Blackhole.consume()强制保留副作用 - 哪怕只是想加一句「测量分配了多少对象」,手写方案要自己 hook
java.lang.instrument,而@Fork(jvmArgsAppend = "-XX:+PrintGCDetails")一行就搞定
真实场景中,95% 的“临时测一下”最后都演变成需要 JMH 的长期 benchmark ——早切早省心。










