Java 方法级性能测试不能简单用 System.currentTimeMillis() 包一层循环。JVM 有预热、JIT 编译、逃逸分析、死代码消除、GC 和线程调度,普通测试很容易测到错误结论。JMH 就是为微基准测试准备的工具。
JMH 适合测小范围热点
JMH 适合在已经定位到热点方法后,比较不同实现的成本。例如字符串拼接、序列化方式、集合操作、算法实现、不同输入规模下的耗时变化。
它不适合替代完整链路压测。接口响应时间、数据库访问、网络调用、缓存命中率这些问题,仍然需要压测、APM、日志和 profiler 一起看。
微基准回答的是“小片代码怎么表现”,不是“整个系统能扛多少流量”。
预热和测量要分开
JVM 刚启动时,代码可能还在解释执行,热点方法还没有被 JIT 编译。直接测第一次运行结果,通常不稳定。
JMH 用 @Warmup 设置预热轮次,用 @Measurement 设置正式测量轮次。预热让运行状态更接近长期服务进程,正式测量再输出可比较数据。
@Fork 可以让测试在新的 JVM 进程里跑,减少当前进程状态对结果的影响。
防止代码被优化掉
如果测试方法计算了一个结果但没有使用,JIT 可能认为这段代码没有意义,直接优化掉。这样测出来的结果会非常漂亮,但没有任何业务价值。
常见处理方式是返回结果,或者使用 JMH 提供的 Blackhole 消费结果。不要在 benchmark 里用空方法假装消费,也不要把日志打印放进核心测量路径。
测试模式要和问题匹配
JMH 常用模式包括:
Throughput:吞吐量,单位时间能执行多少次。AverageTime:平均耗时。SampleTime:采样耗时分布。SingleShotTime:单次执行时间。
如果比较两个方法每秒能处理多少次,用吞吐量;如果关心单次延迟,用平均耗时或采样耗时。模式不同,分数不能直接横向比较。
并行不一定更快
并行求和、并行 Stream 或多线程算法,都有任务拆分、线程调度和结果合并成本。
输入规模小时,串行可能更快;输入规模足够大、计算足够重、共享状态足够少时,并行才可能体现优势。JMH 的 @Param 可以用不同输入规模跑一组结果,避免只拿一个数字下结论。
结果要看误差范围
JMH 输出里不要只看 Score,还要看 Error 和 Units。
如果两个实现分数接近,但误差范围重叠很大,就不能轻易说谁更快。应该增加测量轮次、隔离机器负载、减少外部干扰,或者重新设计 benchmark。
微基准结论最好写清楚测试环境、输入规模、JDK 版本和测试模式,否则以后很难复现。
实用结论
JMH 能帮你把小范围性能判断从“感觉”变成“可测量”。它解决了 JVM 预热、JIT 优化、死代码消除、fork 隔离和结果统计这些细节。
但它不是万能压测工具。正确姿势是:先用线上指标或 profiler 找热点,再用 JMH 比较局部实现,最后回到真实链路验证整体收益。




