高精度微基准测试(JMH)速成手册!

举报
喵手 发表于 2026/01/15 17:00:08 2026/01/15
【摘要】 开篇语哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区:C站/掘金/腾讯云/阿里云/华为云/51CTO;欢迎大家常来逛逛  今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。  我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都尽量抽业余时间把自己所学到所会的,通过文章的形式进行输出,...

开篇语

哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区:C站/掘金/腾讯云/阿里云/华为云/51CTO;欢迎大家常来逛逛

  今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。

  我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都尽量抽业余时间把自己所学到所会的,通过文章的形式进行输出,希望以这种方式帮助到更多的初学者或者想入门的小伙伴们,同时也能对自己的技术进行沉淀,加以复盘,查缺补漏。

小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦。三连即是对作者我写作道路上最好的鼓励与支持!

1. 基准设计原则(要点速览)

  • 隔离与再现性:每次基准尽量在相同的硬件与 JVM 配置下运行;记录 java 版本、JVM flags、CPU 模型、OS、频率调速器状态等元数据。
  • 充分预热(warmup):JIT 优化/内联需要时间,必须预热(JMH 提供 @Warmup 或命令行参数 -wi)。
  • 多次测量与统计:使用多次测量迭代(@Measurement/-i)与多次 fork(@Fork/-f)来观察方差与稳定性。
  • 正确的基准模式:选择合适的 @BenchmarkMode(吞吐/平均/样本/单次延迟),例如比较吞吐就用 Mode.Throughput
  • 控制干扰:使用 @Fork(…) 启动干净 JVM;避免在同一 JVM 中同时跑其它负载;为可重复性固定堆大小(-Xms -Xmx)。
  • 使用适合的 State 范围@State(Scope.Thread)Scope.BenchmarkScope.Group 选择正确,避免误用共享状态产生竞态。

2. 常用 JMH 注解与参数(总结)

  • @Benchmark:标注基准方法。
  • @State:管理共享/局部状态(Scope.Thread / Scope.Benchmark / Scope.Group)。
  • @Param:参数化输入(大小、策略等)。
  • @Warmup(iterations = X, time = Y, timeUnit = Z):预热轮次/时间。
  • @Measurement(iterations = X, time = Y, timeUnit = Z):测量轮次/时间。
  • @Fork(value = N, jvmArgsAppend = {...}):fork 新 JVM 次数及 JVM args。
  • @BenchmarkMode(Mode.Throughput / Mode.AverageTime / Mode.SampleTime / Mode.SingleShotTime)
  • @OutputTimeUnit(TimeUnit.MICROSECONDS):设定输出单位。
  • 使用 Blackhole 注入或让方法返回结果以防死代码消除(DCE)。

3. 避免 JVM 优化“作假”(常用技巧)

  • 死代码消除(DCE):确保基准方法的结果被使用或传给 Blackhole,不要只是做计算但不返回。
  • 方法内联:JIT 会内联,可能掩盖真实成本;用 @CompilerControl(CompilerControl.Mode.DONT_INLINE)(需要 org.openjdk.jmh:jmh-core 支持)或把被测代码放在不同类中以观察差异。
  • 常量传播:避免把待测数据写成 final static 常量,使其被常量折叠;使用 @Setup 动态生成输入。
  • 共享可变状态:若使用 Scope.Benchmark,多个线程共享同一实例,可能引入内存可见性成本或锁争用,区分真实并发成本与测量误差。
  • 在受控进程中运行:用 @Fork(1)(或更多)并在 @Fork 中设置 jvmArgsAppend(例如固定堆:-Xms2g -Xmx2g)以便可重复。

4. 结果分析与可重复性最佳实践

  • 多次 fork 与统计显著性:用多个 forks(例如 3~5)得到多个独立样本,查看平均与方差;高方差说明测量不稳。
  • 查看分布而非单值:用 Mode.SampleTime 与 percentiles 输出或保存 CSV,观察分布形状(长尾/抖动)。
  • 记录完整元数据:记录 JMH 输出日志(它会生成 benchmark 输出),保存 benchmarks.jar 的运行命令和 JVM flags。
  • 对比需一致条件:对比两实现时,保证相同 @Param、相同数据输入、相同 JVM 配置与运行环境。
  • 用分析器定位热点:JMH 支持 -prof 插件(例如 gc, perf, async 等),配合 async-profiler / perf 找出内联/锁/内存分配热点。

5. 常见陷阱(要 Avoid)

  • 没做预热或预热太少 → 测得的是“JIT 转换过程”而非稳态性能。
  • 将共享可变状态误设为 Scope.Benchmark 而又在多线程中读写 → 竞态/锁等待污染结果。
  • 使用不同大小数据但不归一化(例如 cache effect)→ 结论误导。
  • 在 IDE 里单次运行基准而不是用 fork 的独立 JVM → 受 IDE/JVM 设置影响。

6. 实战练习(代码)

下面给出一个可直接拷贝并运行的 JMH 基准类,比较两种“整数求和”实现:经典 for 循环与 IntStream.sum()。这一对比能展示在不同输入规模、JIT 优化下的差异(注意:Stream 实现通常分配更多对象/中间状态,性能会随规模与 JIT 不同而变化)。

说明:把下面类放入一个包含 JMH 依赖的 Maven/Gradle 项目中,然后 mvn clean packagejava -jar target/benchmarks.jar 运行,或在 IDE 中使用 org.openjdk.jmh.Main

package com.example.jmh;

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;

import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;
import java.util.Random;

@State(Scope.Thread)
@BenchmarkMode({Mode.Throughput, Mode.AverageTime})
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Fork(value = 3, jvmArgsAppend = {"-Xms2g", "-Xmx2g"})
@Warmup(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS)
@Measurement(iterations = 10, time = 500, timeUnit = TimeUnit.MILLISECONDS)
public class SumBenchmark {

    @Param({"100", "10000", "1000000"})
    public int N;

    private int[] data;

    @Setup(Level.Trial)
    public void setup() {
        Random rnd = new Random(12345);
        data = new int[N];
        for (int i = 0; i < N; i++) {
            data[i] = rnd.nextInt(100);
        }
    }

    // 1) plain for-loop sum — baseline
    @Benchmark
    public long sumForLoop() {
        long s = 0L;
        for (int i = 0; i < data.length; i++) {
            s += data[i];
        }
        return s; // returning prevents DCE
    }

    // 2) IntStream sum
    @Benchmark
    public long sumIntStream() {
        return IntStream.of(data).sum();
    }

    // 3) example using Blackhole if you prefer to consume values
    @Benchmark
    public void sumForLoopBlackhole(Blackhole bh) {
        long s = 0L;
        for (int i = 0; i < data.length; i++) {
            s += data[i];
        }
        bh.consume(s);
    }
}

运行示例命令(Maven)

  1. mvn clean package(确保 jmh-corejmh-generator-annprocess 在依赖与 annotationProcessor 中)
  2. java -jar target/benchmarks.jar -wi 5 -i 10 -f 3 -t 1
    关键参数说明:-wi 预热迭代,-i 测量迭代,-f fork 次数,-t 线程数,-bm 可指定基准模式等。

7. 如何解读示例输出(举例说明)

假设你看到这样的输出(简化):

Benchmark                         (N)  Mode  Cnt     Score    Error  Units
SumBenchmark.sumForLoop           100  thrpt   30  10000.123 ± 50.456  ops/s
SumBenchmark.sumIntStream         100  thrpt   30   2000.789 ± 30.123  ops/s
SumBenchmark.sumForLoop         1000000  avgt    10    12.345 ± 0.321  ms/op
SumBenchmark.sumIntStream       1000000  avgt    10    45.678 ± 1.234  ms/op

解读要点:

  • Mode = thrpt(吞吐)或 avgt(平均时间)。吞吐高越好,平均时间低越好。
  • Cnt 表示测量样本个数(通常是 forks * iterations)。
  • Score 是测量值;Error 是统计误差(通常是置信区间的一部分)。当 Error 相对 Score 很大时说明不稳定,需要更多 samples 或改善环境隔离。
  • 不要只看单个 N 的结果,观察参数化 N 在不同规模下的趋势(缓存局部性、分配成本、JIT 优化差别)。

8. 进阶建议(profiling、内存、并发)

  • 使用 -prof gc 查看 GC 行为;-prof perf 或 async-profiler(若可用)找 CPU 热点、分配热点。
  • 想测并发缩放:使用 @Threads@Group 来设计并发基准(注意 State 的 Scope)。
  • 对有分配的实现,关注分配率(allocs/s)与 GC 停顿,JMH profiler 能帮助识别频繁分配的代码路径。

9. 简短检查清单(Checklist)

  • [ ] 使用 @Warmup,并确保预热足够。
  • [ ] 使用 @Fork 启动独立 JVM;并固定堆大小。
  • [ ] 避免 DCE(使用返回或 Blackhole)。
  • [ ] 为参数化输入使用 @Param 并覆盖有代表性的规模。
  • [ ] 记录所有 JVM flags、硬件与运行环境信息。
  • [ ] 使用 profiler 定位真实瓶颈,而不是只看时间数字。

… …

文末

好啦,以上就是我这期的全部内容,如果有任何疑问,欢迎下方留言哦,咱们下期见。

… …

学习不分先后,知识不分多少;事无巨细,当以虚心求教;三人行,必有我师焉!!!

wished for you successed !!!


⭐️若喜欢我,就请关注我叭。

⭐️若对您有用,就请点赞叭。
⭐️若有疑问,就请评论留言告诉我叭。


版权声明:本文由作者原创,转载请注明出处,谢谢支持!

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。