当前位置: 首页 > news >正文

性能优化 - 工具篇:基准测试 JMH

文章目录

  • Pre
  • 引言
  • 1. JMH 简介
  • 2. JMH 执行流程详解
  • 3. 关键注解详解
    • 3.1 @Warmup
    • 3.2 @Measurement
    • 3.3 @BenchmarkMode
    • 3.4 @OutputTimeUnit
    • 3.5 @Fork
    • 3.6 @Threads
    • 3.7 @Group 与 @GroupThreads
    • 3.8 @State
    • 3.9 @Setup 与 @TearDown
    • 3.10 @Param
    • 3.11 @CompilerControl
  • 4. 示例代码与分析
    • 4.1 关键点解读
    • 4.2 运行与结果示例
  • 5. 结果可视化与二次处理
    • 5.1 支持的输出格式
    • 5.2 可视化工具推荐
  • 小结

在这里插入图片描述


Pre

性能优化 - 理论篇:常见指标及切入点

性能优化 - 理论篇:性能优化的七类技术手段

性能优化 - 理论篇:CPU、内存、I/O诊断手段

性能优化 - 工具篇:常用的性能测试工具


  1. 引言:手工计时的局限,说明 JMH 的必要性与优势;
  2. JMH 简介:工具背景、引入方式(JDK 12 内置与 Maven 依赖);
  3. JMH 执行流程:Fork、Threads、Warmup、Measurement 四阶段说明;
  4. 关键注解详解:
    4.1 @Warmup:预热原理与配置要点;
    4.2 @Measurement:测量阶段的迭代策略;
    4.3 @BenchmarkMode:常见模式及输出含义;
    4.4 @OutputTimeUnit:结果单位切换;
    4.5 @Fork:进程隔离与 JVM 参数传递;
    4.6 @Threads:并发线程数控制;
    4.7 @Group 与 @GroupThreads(简单介绍);
    4.8 @State:状态作用域(Benchmark/Thread/Group);
    4.9 @Setup 与 @TearDown:初始化与清理时机;
    4.10 @Param:不同参数组合测试法;
    4.11 @CompilerControl:强制内联与编译控制;
  5. 示例代码分析:对比 shift 与 div 两种写法的基准测试;
  6. 结果可视化:JMH 支持的五种输出格式,常用 CSV/JSON 导出;以及三款可视化工具(JMH Visualizer、JMH Visual Chart、meta-chart);
  7. 小结:JMH 在热点代码验证与专项微基准测试中的应用价值;

引言

在以往的项目开发与性能调优中,我们经常会写类似如下的简单计时代码:

long start = System.currentTimeMillis();
// logic
long cost = System.currentTimeMillis() - start;
System.out.println("Logic cost : " + cost);

然而,这种方式往往并不准确:

  1. JIT 编译与方法内联

    • 在 HotSpot JVM 中,编译器会对“热点方法”进行 JIT 编译并尝试内联;
    • 如果仅执行一次计时,没有经过充分“预热”,那就是测的是“解释执行”甚至是“部分编译前”的性能。
  2. 测量误差与噪声干扰

    • 操作系统调度、垃圾回收、上下文切换、CPU 频率变动等因素,都可能导致 System.currentTimeMillis() 级别的测量出现几十毫秒甚至几百毫秒的抖动;
    • 对于微基准测试(microbenchmark),通常期望测量精度达到“纳秒”级别,需要靠更专业的工具来保证。
  3. 重复测量与结果收敛

    • 如果逻辑执行速度非常快,仅靠一次或少量循环,很难区分不同实现间的微小差距;
    • 必须反复多次执行、结合预热阶段,才能进入“JIT 稳定态”,得到接近真实的执行时间。

基于上述原因,JMH(Java Microbenchmark Harness) 应运而生。JMH 是 OpenJDK 官方团队维护的微基准测试框架,内置在 JDK 12 及更高版本中(早期需要通过 Maven 坐标引入)。它能够以纳秒级精度准确地测量 Java 方法的吞吐量与延迟,同时自动完成:

  • 多轮预热(Warmup)以驱动 JIT 编译;
  • 多次迭代测量(Measurement)并收集统计数据;
  • 多进程隔离(Fork)确保 JVM 参数与环境一致;
  • 并发线程控制(Threads)模拟多线程竞争场景。

1. JMH 简介

  • 起源与背景

    • JMH 最初由 OpenJDK 团队开发,用于给 JVM 本身的性能回归测试提供微基准支持;
    • 随后广泛应用于社区,被各大中间件与框架团队用来验证算法、数据结构或方法优化效果。
  • 引入方式

    • JDK12+ 内置:如果使用 JDK 12 或更高版本,默认包含了 jmh-corejmh-generator-annprocess

    • Maven/Gradle 依赖(适用于 JDK11 及以下版本):

      <dependencies><dependency><groupId>org.openjdk.jmh</groupId><artifactId>jmh-core</artifactId><version>1.23</version></dependency><dependency><groupId>org.openjdk.jmh</groupId><artifactId>jmh-generator-annprocess</artifactId><version>1.23</version><scope>provided</scope></dependency>
      </dependencies>
      
    • IDE 集成:将上述依赖导入后,IDE(如 IntelliJ IDEA)会识别 @Benchmark 注解并支持直接运行;

  • 基本原理
    JMH 通过注解与代码生成器,将被测试的方法包装成专用的 Runner(子进程),在子进程内执行多轮预热与测量,并将数据通过 Socket 或共享内存传回主进程,最后统一汇总、统计并输出。整个流程大致如下:

  1. Fork(进程隔离):JMH 启动指定数量的子进程,每个子进程都运行相同的测试。
  2. Warmup(预热):在每个子进程中,对目标方法执行若干轮预热(可配置轮数和时长),驱动 JIT 编译与内联优化。
  3. Measurement(正式测量):预热结束后,进入正式测量阶段,同样执行可配置轮数和时长,收集方法执行耗时或吞吐统计。
  4. Result 收集与输出:每个子进程将自身测量结果发送给主进程,主进程汇总所有子进程数据并最终输出(TEXT/CSV/JSON/LATEX/…)。

2. JMH 执行流程详解

在 JMH 运行日志中,你会看到类似下面的输出:

# JMH version: 1.23
# VM version: JDK 11.0.5, Java HotSpot(TM) 64-Bit Server VM, 11.0.5+10
# Warmup: 3 iterations, 1 s each
# Warmup Iteration   1: 0.281 ops/ns
# Warmup Iteration   2: 0.376 ops/ns
# Warmup Iteration   3: 0.483 ops/ns
# Measurement: 5 iterations, 1 s each
# Fork: 1 of 1
# Threads: 2 threads
# Benchmark: com.example.BenchmarkTest.shiftIteration   1: 1646.000 ns/op
Iteration   2: 1243.000 ns/op
Iteration   3: 1273.000 ns/op
Iteration   4: 1395.000 ns/op
Iteration   5: 1423.000 ns/opResult "com.example.BenchmarkTest.shift":2.068 ±(99.9%) 0.038 ns/op [Average](min, avg, max) = (2.059, 2.068, 2.083), stdev = 0.010CI (99.9%): [2.030, 2.106] (assumes normal distribution)

下面将结合这段日志逐项说明流程与参数含义:

  1. Warmup(预热)

    • 例中配置为 3 iterations, 1 s each,表示在每个 Fork 进程中,先进行 3 轮预热,每轮持续 1 秒,将预热阶段测得的吞吐(ops/ns)丢弃,不纳入最终统计;
    • 预热结束后,JMH 会自动等待 JIT 完成优化,使得被测方法进入“稳态”(steady-state)状态,从而让 Measurement 阶段结果更准确。
  2. Measurement(测量)

    • 配置为 5 iterations, 1 s each,意味着正式测量阶段同样分 5 轮,每轮持续 1 秒,将每轮测量结果记录到输出;
    • 日志中每行 Iteration X: YYY ns/op,即表示第 X 轮的平均耗时(以纳秒为单位);
    • JMH 会将所有轮次的测量结果汇总,计算整体的平均值(avg)、标准差(stdev)、95% 或 99.9% 置信区间(CI)等统计指标。
  3. Fork(进程隔离)

    • 例中为 Fork: 1 of 1,表示只启动一个子进程进行测试;如果配置成 @Fork(3),JMH 会依次启动 3 个子进程,每个子进程重复上述 Warmup + Measurement 流程,最终在主进程以更大样本量做全局聚合;

    • 通过多 Fork 可以减少单个子进程环境的干扰(如 GC、操作系统调度差异)对结果的影响;

    • 如果设置 @Fork(0),则表示“非 Fork 模式”,会直接在当前 JVM 进程中执行所有轮次。但 JMH 官方强烈不推荐非 Fork 模式在正式测量时使用,会提示警告:

      # *** WARNING: Non-forked runs may silently omit JVM options, mess up profilers, disable compiler hints, etc. ***
      
    • Fork 注解支持参数 jvmArgsAppend,可以为每个子进程传入不同的 JVM 参数,例如:

      @Fork(value = 3, jvmArgsAppend = {"-Xmx2048m", "-server", "-XX:+AggressiveOpts"})
      

      这样每个子进程都会以 -Xmx2048m -server -XX:+AggressiveOpts 的参数启动,更精细地控制测量环境。

  4. Threads(线程并发)

    • 例中日志 # Threads: 2 threads,对应 @Threads(2) 注解,表示每个子进程都会启动 2 个并行线程同时执行被测方法,以测试并发场景下的吞吐或延迟;
    • 如果配置 @Threads(Threads.MAX),JMH 会自动根据机器上的逻辑 CPU 核心数创建同等数量的线程;
    • 线程数过多可能导致上下文切换过于频繁,从而影响测量结果,需要根据测试目标(单线程性能 vs 多线程吞吐)以及硬件条件合理设置。

3. 关键注解详解

下面一一介绍 JMH 常用注解的语义、配置参数与注意事项。

3.1 @Warmup

@Warmup(iterations = 5,time = 1,timeUnit = TimeUnit.SECONDS,batchSize = 1  // 可选
)
  • 用途:配置预热阶段的迭代轮数与持续时长,让被测方法在 Measurement 阶段之前尽量被 JIT 编译、内联完成。

  • 参数说明

    • iterations:预热轮数,表示要执行多少轮预热;
    • time + timeUnit:每轮预热的持续时间,例如 1, TimeUnit.SECONDS 表示每轮持续 1 秒;
    • batchSize:每次迭代中“批量”调用被测方法的次数。默认值为 1,若被测方法非常轻量,可以将其调大,使得每次迭代进行更多次调用,然后再累积测量。

示例日志

# Warmup: 3 iterations, 1 s each
# Warmup Iteration   1: 0.281 ops/ns
# Warmup Iteration   2: 0.376 ops/ns
# Warmup Iteration   3: 0.483 ops/ns

这里第 1 轮预热的吞吐:0.281 操作/纳秒 ,第 2 轮:0.376 操作/纳秒,以此类推。预热结果不会被记录到最终输出。

实践建议

  • 如果方法执行非常快(例如几十、几百纳秒),建议将 batchSize 设置为几百或几千,以避免单次测量粒度过小而受操作系统调度影响。
  • 在分布式服务发布过程中,往往要先做请求“容量预热”与“灰度放量”。这种预热与 JMH 里的 @Warmup 逻辑相似,都是为了让服务或方法进入最优状态。

3.2 @Measurement

@Measurement(iterations = 5,time = 1,timeUnit = TimeUnit.SECONDS,batchSize = 1  // 可选
)
  • 用途:配置正式测量阶段的迭代轮数与持续时长,被测方法在每轮测量中的平均耗时或吞吐将被收集。
  • 参数与 @Warmup 相同:区别就在于,这里的结果会被统计并输出。

示例日志

# Measurement: 5 iterations, 1 s each
Iteration   1: 1646.000 ns/op
Iteration   2: 1243.000 ns/op
Iteration   3: 1273.000 ns/op
Iteration   4: 1395.000 ns/op
Iteration   5: 1423.000 ns/op

表示测量阶段共 5 轮,每轮持续 1 秒。测量数据是每轮的平均耗时(纳秒/操作)。

实践建议

  • 在正式测量时,务必确保测试环境稳定:关闭无关进程、保证 CPU 频率固定、网络与磁盘 I/O 低干扰等;
  • 测试结束后,关注平均值之外的最小/最大/标准差,以了解代码在不同时刻是否存在抖动。

3.3 @BenchmarkMode

@BenchmarkMode({Mode.Throughput, Mode.AverageTime})
  • 用途:指定 JMH 在测量阶段“统计哪种指标”,可同时指定多个模式。

  • 常见模式(Mode)解释

    • Throughput:吞吐量(operations per time unit),例如 “ops/ms” 表示每毫秒的调用次数,相当于 QPS。
    • AverageTime:平均耗时(time per operation),例如 “ns/op” 表示每次调用平均耗时纳秒。
    • SampleTime:随机采样模式,会在随机时间间隔抓取一次调用耗时分布,用于统计 TP90/TP99 等百分位延迟指标。
    • SingleShotTime:单次执行耗时(适合测试一次性初始化、静态块、Cold Start 等);
    • All:同时统计上述所有模式并在输出中显示。

示例输出

Result "com.example.BenchmarkTest.shift":2.068 ±(99.9%) 0.038 ns/op [Average](min, avg, max) = (2.059, 2.068, 2.083), stdev = 0.010CI (99.9%): [2.030, 2.106] (assumes normal distribution)
Benchmark            Mode  Cnt  Score   Error  Units
BenchmarkTest.div    avgt    5  2.072 ± 0.053  ns/op
BenchmarkTest.shift  avgt    5  2.068 ± 0.038  ns/op

这里 avgt 表示 AverageTime 模式,测量了 5 轮后,shift() 的平均耗时约为 2.068ns,误差范围 ±0.038ns。

实践建议

  • 若关注“总体吞吐(QPS)”,请选择 Throughput 模式;若关注“平均延迟”,则用 AverageTime
  • 在多数微基准测试中,仅关注 AverageTime 即可;如需延迟分布,可切换到 SampleTime 并再结合百分位统计。

3.4 @OutputTimeUnit

@OutputTimeUnit(TimeUnit.MILLISECONDS)
  • 用途:指定最终输出结果的“时间单位”,可用在类或方法级别。常见单位:TimeUnit.SECONDSMILLISECONDSMICROSECONDSNANOSECONDS

  • 示例

    • @BenchmarkMode(Mode.Throughput) + @OutputTimeUnit(TimeUnit.MILLISECONDS),最终结果中 Score 单位会显示为 ops/ms
    • @BenchmarkMode(Mode.Throughput) + @OutputTimeUnit(TimeUnit.SECONDS),则结果单位为 ops/s
    • @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.NANOSECONDS),则结果单位为 ns/op

示例输出(吞吐模式,单位为 ops/ms)

Benchmark             Mode  Cnt       Score       Error   Units 
BenchmarkTest.div    thrpt    5  482999.685 ±  6415.832  ops/ms 
BenchmarkTest.shift  thrpt    5  480599.263 ± 20752.609  ops/ms

实践建议

  • 根据待测方法耗时大小,选择合适的单位:若方法耗时在几百纳秒级别,用纳秒;若耗时在微秒~毫秒,用 MICROSECONDSMILLISECONDS

3.5 @Fork

@Fork(value = 1, jvmArgsAppend = {"-Xmx2048m", "-server", "-XX:+AggressiveOpts"})
  • 用途:指定测量时要 fork 出多少个子进程来执行完整的 Warmup + Measurement 流程,以及向每个子进程传入哪些 JVM 参数。

  • 参数说明

    • value:子进程数量,大于等于 1;
    • jvmArgsAppend:为子进程 JVM 增加额外参数(如 -Xmx2g-XX:+UseG1GC-XX:+AggressiveOpts 等)。
  • 注意

    • 每个子进程都会重新加载类、重新分配堆空间,并与主进程通过 IPC 通信或 Socket 汇报结果;
    • 子进程数量过多,会延长测试时间,但能降低单个子进程偶发干扰对最终结果的影响。

日志示例

# Fork: 1 of 1
# Warmup: 3 iterations, 1 s each
# Measurement: 5 iterations, 1 s each

实践建议

  • 推荐至少 @Fork(1);若想更多保障结果稳定,可设置为 @Fork(3)
  • 在高性能机器上,可以结合 jvmArgsAppend 调整最大堆大小、GC 策略等,以与生产环境保持一致。

3.6 @Threads

@Threads(2)
  • 用途:指定每个子进程内并发运行的线程数;

  • 可选值

    • 固定整数,如 @Threads(1) 表示单线程测试;
    • 特殊值 Threads.MAX,表示创建与 CPU 核数相等的线程数;

注意

  • 过多线程会导致操作系统上下文切换增加,使得单线程测量结果难以辨别。
  • 过少线程无法模拟并发场景下的“线程竞争成本”。

3.7 @Group 与 @GroupThreads

@State(Scope.Group)
@Group("myGroup")
@GroupThreads(3)
  • 用途:将多个 @Benchmark 方法归为一组(Group),并在该组内部以特定线程数并发调用各方法。常用于测试“多个方法之间的竞争”或“读写混合场景”。

  • 示例

    @State(Scope.Group)
    public class MyBenchmark {@Param({"10", "100"})public int size;@Benchmark@Group("readWrite")@GroupThreads(3)public void read() {// 读逻辑,3 个线程并发地执行 read()}@Benchmark@Group("readWrite")@GroupThreads(1)public void write() {// 写逻辑,1 个线程并发地执行 write()}
    }
    
    • 上例中,总共有 4 个线程并发:3 个线程执行 read(),1 个线程执行 write(),它们会对同一份共享状态(因为 @State(Scope.Group))进行交互。

实践场景

  • 测试读写分离缓存场景;
  • 测试消息队列生产者/消费者模型;
  • 评估并发数据结构(如 ConcurrentHashMap)在不同线程配比下的吞吐。

3.8 @State

@State(Scope.Thread)
public class MyState {// 类字段可在 benchmark 方法中直接使用
}
  • 用途:声明一个“状态类”,其字段可在被测 @Benchmark 方法中共享或隔离。必须加在类上,否则无法运行。

  • Scope 可选值

    • Scope.Benchmark:一个类实例在所有线程、所有轮次共用;适合存放全局只读数据;
    • Scope.Thread:每个线程都会有自己单独的状态实例,彼此互不影响;适合存放线程本地变量,避免并发冲突;
    • Scope.Group:在带 @Group 注解的场景下,同一个组内的线程共享一个状态实例。

示例

@State(Scope.Thread)
public class JMHSample_04_DefaultState {double x = Math.PI;@Benchmarkpublic void measure() {x++;}
}
  • Scope.Thread 意味着每个线程都有自己的 x,互不干扰;
  • 如果改为 Scope.Benchmark,那么所有线程都使用同一个 x,竞态写可能会导致测量结果不稳定。

3.9 @Setup 与 @TearDown

@Setup(Level.Trial)
public void init() {// 只在 Fork 子进程启动后、预热前执行一次,进行全局初始化
}@TearDown(Level.Trial)
public void cleanup() {// 在同一个子进程所有测量结束后执行一次,用于回收资源
}
  • 用途:定义在不同“生命周期”阶段执行的初始化与清理操作,类似于 JUnit 的 @BeforeClass / @AfterClass

  • Level 可选值

    • Trial(默认):在每个 Fork 子进程内部,只执行一次;
    • Iteration:在每轮预热或测量开始前执行一次;
    • Invocation:在每次被测方法调用前执行,粒度最细,但开销极大,通常不推荐使用。

示例

@State(Scope.Benchmark)
public class MyBenchmark {private Connection conn;@Setup(Level.Trial)public void initConnection() {conn = DriverManager.getConnection(...);}@TearDown(Level.Trial)public void closeConnection() {conn.close();}@Benchmarkpublic void testQuery() {// 使用 conn 进行一次 database query}
}
  • 在每个子进程启动并完成 JIT 编译后,initConnection() 只执行一次;所有轮次共用同一个连接;
  • 测量结束后,closeConnection() 执行一次,用于关闭资源。

3.10 @Param

@State(Scope.Benchmark)
public class JMHSample_27_Params {@Param({"1", "31", "65", "101", "103"})public int arg;@Param({"0", "1", "2", "4", "8", "16", "32"})public int certainty;@Benchmarkpublic boolean bench() {return BigInteger.valueOf(arg).isProbablePrime(certainty);}
}
  • 用途:为字段提供一组“离散参数值”,JMH 会自动对所有参数组合进行笛卡尔积测试,统计每种参数组合下的测量结果。
  • 注意:如果参数组合过多(例如两个字段各 10 个取值,总计 100 个组合),测试需要同时针对 100 种组合分别执行多轮预热与测量,耗时会很长。

示例输出片段

# JMH version: 1.23
# VM version: JDK 11.0.5, Java HotSpot(TM) 64-Bit Server VM, 11.0.5+10
# Warmup: 5 iterations, 1 s each
# Measurement: 5 iterations, 1 s each
# Param: arg = 1, certainty = 0
Benchmark                        Mode  Cnt     Score     Error  Units
JMHSample_27_Params.bench       avgt    5     0.123 ±   0.005   ns/op
JMHSample_27_Params.bench       avgt    5     0.130 ±   0.007   ns/op
...
# Param: arg = 65, certainty = 16
JMHSample_27_Params.bench       avgt    5    59.456 ±   1.234   ns/op
...
  • 每对 (arg, certainty) 配置都会生成一组测量行。

3.11 @CompilerControl

@CompilerControl(CompilerControl.Mode.INLINE)
public void hotMethod() {// ...
}@CompilerControl(CompilerControl.Mode.DONT_INLINE)
public void noInlineMethod() {// ...
}
  • 用途:在 JMH 测试中,通过注解强制或禁止 JIT 编译器对某方法进行内联或编译。常见模式:

    • INLINE:强制将该方法内联到调用方,消除方法调用开销;
    • DONT_INLINE:禁止内联,使得方法调用成本不被隐藏;
    • EXCLUDE:完全禁止 JIT 对该方法进行编译,始终解释执行。

示例场景

  • 若要对比“手动位移运算 vs 除法运算”在“是否被内联”前后的性能差异,可使用 @CompilerControl 强制内联或禁止内联;
  • 分析 getter/setter 方法是否被 JIT 内联,并测量其对调用链整体耗时的影响。

4. 示例代码与分析

下面给出一个完整的 JMH 基准测试示例,比较“位移运算”和“除法运算”在相同循环次数下的性能差异(shift vs div):

import org.openjdk.jmh.annotations.*;
import java.util.concurrent.TimeUnit;@BenchmarkMode(Mode.Throughput)            // 统计吞吐量
@OutputTimeUnit(TimeUnit.MILLISECONDS)     // 吞吐以“ops/ms”输出
@State(Scope.Thread)                       // 每个线程一个独立实例
@Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS)   // 预热 3 轮,每轮 1s
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)  // 测量 5 轮,每轮 1s
@Fork(1)                                   // 每次只启用 1 个子进程
@Threads(2)                                // 每个子进程内使用 2 个并发线程
public class BenchmarkTest {@Benchmarkpublic long shift() {long t = 455565655225562L;long a = 0;for (int i = 0; i < 1000; i++) {a = t >> 30;}return a;}@Benchmarkpublic long div() {long t = 455565655225562L;long a = 0;for (int i = 0; i < 1000; i++) {a = t / 1024 / 1024 / 1024;}return a;}public static void main(String[] args) throws Exception {Options opts = new OptionsBuilder().include(BenchmarkTest.class.getSimpleName())      // 包含当前测试类.resultFormat(ResultFormatType.JSON)              // 输出 JSON 格式.build();new Runner(opts).run();}
}

4.1 关键点解读

  1. @BenchmarkMode(Mode.Throughput) + @OutputTimeUnit(TimeUnit.MILLISECONDS)

    • 测量每毫秒的“操作次数”(ops/ms),适合快速计算操作的吞吐差异;
    • 若换成 @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.NANOSECONDS),则测量每次调用的平均耗时(ns/op)。
  2. @State(Scope.Thread)

    • 每个线程都有自己的实例,此测试中两个线程并行执行各自的 shift()div(),互不干扰;
    • 若改为 Scope.Benchmark,则同一实例被两个线程共享,此处没有共享字段,不会影响结果;
  3. 循环内部写死常量 vs 变量

    • t >> 30t / 1024 / 1024 / 1024 在 JIT 优化后可能都会被内联、常量折叠;
    • 但在真实业务中,如果 t 来自方法参数或对象字段,则 JIT 可能无法在编译期完全优化,此处作为示例用于对比原始运算开销。
  4. Main 方法中的 OptionsBuilder

    • include(...):表示只运行名称匹配当前类名的基准;
    • resultFormat(ResultFormatType.JSON):让输出结果为 JSON 格式,便于后续二次处理,如可视化;

4.2 运行与结果示例

假设用 JDK 11 运行该测试,控制台会打印类似:

# JMH version: 1.23
# VM version: JDK 11.0.5, Java HotSpot(TM) 64-Bit Server VM, 11.0.5+10
# Warmup: 3 iterations, 1 s each
# Warmup Iteration   1: 35.000 ops/ms
# Warmup Iteration   2: 45.000 ops/ms
# Warmup Iteration   3: 50.000 ops/ms
# Measurement: 5 iterations, 1 s each
# Fork: 1 of 1
# Threads: 2 threads
# Benchmark: com.example.BenchmarkTest.shiftIteration   1: 480000.000 ops/ms
Iteration   2: 485000.000 ops/ms
Iteration   3: 482500.000 ops/ms
Iteration   4: 484000.000 ops/ms
Iteration   5: 483200.000 ops/msResult "com.example.BenchmarkTest.shift":482740.000 ±(99.9%) 5000.000 ops/ms [Thrpt]
# Benchmark: com.example.BenchmarkTest.divIteration   1: 478000.000 ops/ms
Iteration   2: 482000.000 ops/ms
Iteration   3: 480500.000 ops/ms
Iteration   4: 481000.000 ops/ms
Iteration   5: 480800.000 ops/msResult "com.example.BenchmarkTest.div":480460.000 ±(99.9%) 4500.000 ops/ms [Thrpt]

从示例结果可见:

  • shift() 的平均吞吐约为 482,740 ops/ms,而 div() 为 480,460 ops/ms;

  • 两者吞吐非常接近,但依旧可以通过 CI (99.9%) 判断差异;

  • 若将模式改为 AverageTime,则会输出每次平均耗时(ns/op),如下:

    Benchmark            Mode  Cnt  Score   Error  Units
    BenchmarkTest.div    avgt    5  2.072 ± 0.053  ns/op
    BenchmarkTest.shift  avgt    5  2.068 ± 0.038  ns/op
    

5. 结果可视化与二次处理

JMH 除了支持在控制台输出之外,还能将测量结果导出为通用格式,以便用第三方工具生成可视化图表。

5.1 支持的输出格式

OptionsBuilder 中可配置 .resultFormat(ResultFormatType.X),JMH 默认支持五种格式:

  1. TEXT:纯文本格式,适合查看简单日志;
  2. CSV:逗号分隔值,可直接用 Excel 或其他工具打开;
  3. SCSV:分号分隔值,与某些地区的 CSV 兼容;
  4. JSON:JSON 结构,适合与脚本或可视化工具对接;
  5. LATEX:可导入 LaTeX 文档,用于学术论文排版。

示例

Options opts = new OptionsBuilder().include(BenchmarkTest.class.getSimpleName()).resultFormat(ResultFormatType.CSV)   // 输出成 CSV 文件.result("benchmark_results.csv").build();
new Runner(opts).run();
  • 运行完成后,会在当前目录生成 benchmark_results.csv,内容包含:Benchmark 名称、Mode、Threads、Fork、Param 值、Score、Error、Units 等列;

5.2 可视化工具推荐

  1. JMH Visualizer(在线工具)

    • 网站地址: https://jmh.morethan.io/
    • 使用方式:将 JMH 导出的 JSON 文件上传,可生成不同参数组合下各模式的条形图或折线图;
    • 缺点:交互需要鼠标悬浮展示信息,对于大数据集可视化体验一般。
  2. JMH Visual Chart(开源桌面/网页工具)

    • GitHub 地址: https://github.com/PengRong/visual-jmh
    • 特色:支持 JSON/CSV 导入,自动识别不同 Benchmark 与参数,将数据按照“吞吐 vs 参数”或“延迟 vs 参数”等方式可视化;
    • 使用时将生成的 benchmark_results.json 拖入工具即可自动绘制。
  3. meta-chart(通用在线图表生成)

    • 网站地址: https://www.meta-chart.com/
    • 使用方式:先将 JMH 导出的 CSV 文件加载到 Excel,筛选出需要展示的数据(如某个方法在不同线程数下的吞吐),再将表格复制到 meta-chart,用柱状图、折线图或散点图进行展示;
    • 优点:灵活度高,可自定义图例、坐标轴、注释等;
  4. Jenkins & CI 插件

    • 如果将 JMH 测试流程集成到 Jenkins 等 CI 平台,可使用社区插件(如 “Benchmark Parser Plugin”)直接把 CSV/JSON 结果展示在 Jenkins 界面,作为每日构建的性能回归图表。

示例可视化效果(结合文字说明,省略具体截图):

  • 条形图展示 shift() vs div() 在吞吐模式下的对比:

    • 横轴:操作名称(shift、div);
    • 纵轴:ops/ms
  • 折线图展示 @Param 不同参数组合下的 AverageTime

    • 横轴:参数值(如 arg=1、31、65、101、103);
    • 纵轴:ns/op
    • 不同曲线表示不同 certainty 值。

小结

  1. 为何要使用 JMH 而非简单手工计时:JMH 能自动完成预热、迭代、进程隔离与统计分析,结果更稳定且精度高;

  2. JMH 执行流程

    • Fork:多进程隔离环境,避免单 JVM 环境干扰;
    • Warmup:驱动 JIT 编译与方法内联,进入稳态;
    • Measurement:正式测量阶段,统计吞吐或延迟;
    • Result 汇总:收集子进程数据并输出;
  3. 关键注解详解

    • @Warmup / @Measurement:配置预热与测量轮次、时长;
    • @BenchmarkMode / @OutputTimeUnit:定义统计模式与输出单位;
    • @Fork / @Threads:控制子进程数量与线程并发数;
    • @State:声明状态作用域(Benchmark/Thread/Group);
    • @Setup / @TearDown:初始化与清理动作的时机;
    • @Param:批量测试不同参数组合;
    • @CompilerControl:强制或禁止 JIT 内联与编译;
  4. 示例分析:以 shift() vs div() 的对比测试,演示完整的 JMH 配置与测量结果格式;

  5. 结果可视化与二次加工

    • 支持的导出格式:TEXT、CSV、SCSV、JSON、LATEX;
    • 常用可视化工具:JMH Visualizer、JMH Visual Chart、meta-chart 以及 Jenkins 插件;

借助 JMH,可以对“热点代码”或“关键算法”进行精准的微基准测试,量化优化前后的性能提升,避免盲目优化或主观判断。

在这里插入图片描述

http://www.xdnf.cn/news/756793.html

相关文章:

  • TCP三次握手四次挥手
  • Notepad++找回自动暂存的文件
  • 【目标检测】backbone究竟有何关键作用?
  • 一键净化Excel数据:高性能Python脚本实现多核并行清理
  • Selenium Manager中文文档
  • 【Java】JDK 命令行工具
  • 从认识AI开始-----Transformer:大模型的核心架构
  • 【Unity博客节选】Timeline 的 AnimationOutputWeightProcessor 理解
  • Leetcode 269. 火星词典
  • 湖北理元理律师事务所:个人债务管理的温度与精度
  • SCSAI平台面向对象建模技术的设计与实现
  • Spring Ai 从Demo到搭建套壳项目(一)初识与实现与deepseek对话模式
  • MATLAB实战:Arduino硬件交互项目方案
  • 【Go-补充】Sync包
  • QtWidgets,QtCore,QtGui
  • uniapp uni-id 如果是正式项目,需自行实现发送邮件的相关功能
  • RAGflow详解及实战指南
  • 深度学习中常见的超参数对系统的影响
  • Vue 3 组件化设计实践:构建可扩展、高内聚的前端体系
  • 初学大模型部署以及案例应用(windows+wsl+dify+mysql+Ollama+Xinference)
  • 「数据采集与网络爬虫(使用Python工具)」【数据分析全栈攻略:爬虫+处理+可视化+报告】
  • (javaSE)Java数组进阶:数组初始化 数组访问 数组中的jvm 空指针异常
  • 卷积神经网络(CNN)完全指南:从原理到实战
  • Java 中 MySQL 索引深度解析:面试核心知识点与实战
  • 牛顿迭代算法-深度解析
  • USART 串口通信全解析:原理、结构与代码实战
  • YOLOv11改进 | Conv/卷积篇 | 全维度动态卷积ODConv与二次创新C3k2助力YOLOv11有效涨点
  • GIS数据类型综合解析
  • 【笔记】在 MSYS2(MINGW64)中安装 Python 工具链的记录
  • 【计网】第六章(网络层)习题测试集