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

Java微基准测试工具JMH

Java微基准测试工具JMH(Java MicroBenchmark Harness)负责JVM预热和代码优化路径等工作,使基准测试尽可能简单。

JVM的即时编译器会对代码进行优化,这可能会影响性能测试的结果。JMH通过控制测试环境(预热、多轮迭代、多进程测试等机制),确保测试结果的准确性。

一、快速开始

Maven依赖:

<dependency><groupId>org.openjdk.jmh</groupId><artifactId>jmh-core</artifactId><version>1.37</version>
</dependency>
<dependency><groupId>org.openjdk.jmh</groupId><artifactId>jmh-generator-annprocess</artifactId><version>1.37</version>
</dependency>

启动类:

package cn.ken;import org.openjdk.jmh.profile.GCProfiler;
import org.openjdk.jmh.results.format.ResultFormatType;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;/*** @author Ken-Chy129* @date 2025/5/18*/
public class BenchmarkRunner {public static void main(String[] args) throws RunnerException {Options opt = new OptionsBuilder().include(StringConcatBenchmark.class.getSimpleName()) // 指定基准测试类.addProfiler(GCProfiler.class) // 添加性能剖析工具.result("result.json") // 输出结果.resultFormat(ResultFormatType.JSON) // 结果类型.build();new Runner(opt).run(); // 运行基准测试}}

基准测试类:

package cn.ken;import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;import java.util.concurrent.TimeUnit;/*** @author Ken-Chy129* @date 2025/5/18*/
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 3, time = 1)
@Measurement(iterations = 5, time = 5)
@Threads(4)
@Fork(1)
@State(value = Scope.Benchmark)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class MyBenchmark {@Benchmarkpublic void measureSimpleMath(Blackhole blackhole) {// 基准代码blackhole.consume(add(1, 2));}private int add(int a, int b) {return a + b;}
}

二、核心概念和注解

常用注解

  • @Benchmark:用于标记需要跑基准测试的方法
  • @BenchmarkMode:测试模式
    • Throughout:吞吐量,单位时间内可以完成的操作数
    • AverageTime:平均时间,完成一次操作所需的平均时间
    • SampleTime:基于采样的事件,提供统计分布数据
    • SingleShotTime:单次执行事件,用于测试冷启动性能
    • ALL:运行所有模式
  • @State:状态对象自然地封装了基准正在处理的状态,通常作为参数注入到Benchmark方法中,JMH负责对其进行实例化和共享。状态对象的范围定义了它在工作线程之间共享的程度。该注解用于标识测试状态的生命周期和作用域
    • Scope.Thread:每个线程一个实例
    • Scope.Benchmark:所有线程共享一个实例
    • Scope.Group:每个线程组共享一个实例
  • @Warmup:预热,可以指定预热迭代次数和每次迭代的运行时间
  • @Measurement:指定正式测试的迭代次数和每次迭代的运行时间
  • @OutputTimeUnit:指定测试结果的时间单位
  • @Threads:指定测试方法运行的线程数
  • @Params:为基准测试方法提供参数,允许在单个测试中运行多个参数集
  • @Fork:指定测试运行在不同的JVM进程中,以避免测试间的相互影响。通常设置为1
  • @AuxCounters:提供额外的性能计数器
  • @CompilerControl:控制JVM的编译优化行为
  • Blackhole:JMH提供的一个机制,用于“吞噬”测试方法的输出,防止JVM的死代码消除优化

性能剖析工具

JMH内置了多个性能剖析工具,可以查看基准测试的消耗在什么地方。常见如下:

  • ClassloaderProfiler:类加载剖析
  • CompilerProfiler:JIT 编译剖析
  • GCProfiler:GC 剖析
  • StackProfiler:栈剖析
  • PausesProfiler:停顿剖析

三、JMH 陷阱

在使用JMH的过程中,需要避免一些会影响测试结果的陷阱。

循环优化

我们可能会通过循环来实现多次调用基准方法,然而JVM非常擅长优化循环,这可能回到则最终的测试结果并不准确。
如果我们希望运行多次基准方法,不应该在方法内使用循环,而是可以通过@OperationsPerInvocation注解来告诉JMH每次迭代应该执行多少次操作。

@Benchmark
@OperationsPerInvocation(1000)
public void measureLoop() {// ...
}

消除死代码

对于某些计算的结果如果没有使用,JVM可能会认为该计算是死代码并将其消除,从而导致基准测试在JVM优化后没有留下任何代码,导致结果有很大的偏差。
因此对于该类测试,我们可以通过如下两种方法避免被识别为死代码:

  1. 从基准测试方法返回代码的结果
  2. 将计算出的值传递到JMH提供的Blackhole中
import org.openjdk.jmh.annotations.Benchmark;public class MyBenchmark {@Benchmarkpublic int testMethod1() {int a = 1;int b = 2;int sum = a + b;return sum;}@Benchmarkpublic void testMethod2(Blackhole blackhole) {int a = 1;int b = 2;int sum = a + b;blackhole.consume(sum);}
}

常量折叠

常量折叠是一中常见的JVM优化。基于常量的计算无论执行多少次通常都会得到导致完全相同的结果,因此JVM可能会检测到之后直接使用计算结果替换该计算。
如下示例:

import org.openjdk.jmh.annotations.Benchmark;public class MyBenchmark {@Benchmarkpublic int testMethod1() {int a = 1;int b = 2;int sum = a + b;return sum;}@Benchmarkpublic int testMethod2() {int sum = 3;return sum;}}

JVM会检测到sum的值是两个常量的值,从而直接将方法1优化成方法2。为了避免常量折叠,我们输入的值不应该是个硬编码的常量,而应该来自一个状态对象。如下:

import org.openjdk.jmh.annotations.*;public class MyBenchmark {@State(Scope.Thread)public static class MyState {public int a = 1;public int b = 2;}@Benchmark public int testMethod(MyState state) {int sum = state.a + state.b;return sum;}
}

其实 JVM 做的优化操作远不止上面这些,还有比如常量传播(Constant Propagation)、循环展开(Loop Unwinding)、循环表达式外提(Loop Expression Hoisting)、消除公共子表达式(Common Subexpression Elimination)、本块重排序(Basic Block Reordering)、范围检查消除(Range Check Elimination)等。

四、结果分析

运行之后可以得到如下的测试结果:

# JMH version: 1.37
# VM version: JDK 1.8.0_412, OpenJDK 64-Bit Server VM, 25.412-b08
# VM invoker: D:\Java\Jdk\corretto-1.8.0_412\jre\bin\java.exe
# VM options: -javaagent:D:\JetBrains\IntelliJ IDEA 2024.2.0.2\lib\idea_rt.jar=56800:D:\JetBrains\IntelliJ IDEA 2024.2.0.2\bin -Dfile.encoding=UTF-8
# Blackhole mode: full + dont-inline hint (auto-detected, use -Djmh.blackhole.autoDetect=false to disable)
# Warmup: 3 iterations, 1 s each
# Measurement: 5 iterations, 5 s each
# Timeout: 10 min per iteration
# Threads: 4 threads, will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: cn.ken.MyBenchmark.measureSimpleMath# Run progress: 0.00% complete, ETA 00:00:28
# Fork: 1 of 1
# Warmup Iteration   1: 1.339 ±(99.9%) 0.250 ns/op
# Warmup Iteration   2: 1.443 ±(99.9%) 0.218 ns/op
# Warmup Iteration   3: 1.062 ±(99.9%) 0.259 ns/op
Iteration   1: 1.035 ±(99.9%) 0.073 ns/op
Iteration   2: 1.036 ±(99.9%) 0.080 ns/op
Iteration   3: 1.043 ±(99.9%) 0.084 ns/op
Iteration   4: 1.098 ±(99.9%) 0.039 ns/op
Iteration   5: 1.813 ±(99.9%) 0.010 ns/opResult "cn.ken.MyBenchmark.measureSimpleMath":1.205 ±(99.9%) 1.311 ns/op [Average](min, avg, max) = (1.035, 1.205, 1.813), stdev = 0.341CI (99.9%): [≈ 0, 2.517] (assumes normal distribution)# Run complete. Total time: 00:00:28REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
experiments, perform baseline and negative tests that provide experimental control, make sure
the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
Do not assume the numbers tell you what you want them to tell.Benchmark                      Mode  Cnt  Score   Error  Units
MyBenchmark.measureSimpleMath  avgt    5  1.205 ± 1.311  ns/opBenchmark result is saved to result.json

可以得到如下信息:

  • Score(分数):1.205表示每次操作的平均时间是1.205纳秒
  • Error(误差):±1.311表示测试结果的误差率为1.311%,误差越小,测试结果越可靠

如果测试结果的误差很小(例如±0.01%),那么测试结果比较稳定和可靠。如果误差较高,可能需要增加迭代次数或者预热次数来降低误差。

除此以外,如果想将测试结果以图表的形式可视化,也可以通过一些工具实现:JMH Visualizer
只需要将上述测试生成的json结果文件导入,就可以实现可视化。

五、测试Demo

官方提供了许多测试样例:code-tools/jmh: 2be2df7dbaf8 /jmh-samples/src/main/java/org/openjdk/jmh/samples/

此处提供一个字符串拼接测试,代码如下:

package cn.ken;import org.openjdk.jmh.annotations.*;import java.util.concurrent.TimeUnit;/*** @author Ken-Chy129* @date 2025/5/18*/
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 5, time = 1)
@Fork(1)
public class StringConcatBenchmark {@Benchmarkpublic String concatByPlus() {String str = "";for (int i = 0; i < 100; i++) {str += i;}return str;}@Benchmarkpublic String concatByStringBuilder() {StringBuilder str = new StringBuilder();for (int i = 0; i < 100; i++) {str.append(i);}return str.toString();}@Benchmarkpublic String concatByStringBuffer() {StringBuffer str = new StringBuffer();for (int i = 0; i < 100; i++) {str.append(i);}return str.toString();}
}

测试结果:

Benchmark                                                        Mode  Cnt      Score     Error   Units
StringConcatBenchmark.concatByPlus                              thrpt    5    756.185 ±  29.003  ops/ms
StringConcatBenchmark.concatByPlus:gc.alloc.rate                thrpt    5  16225.526 ± 619.623  MB/sec
StringConcatBenchmark.concatByPlus:gc.alloc.rate.norm           thrpt    5  22504.001 ±   0.001    B/op
StringConcatBenchmark.concatByPlus:gc.count                     thrpt    5     55.000            counts
StringConcatBenchmark.concatByPlus:gc.time                      thrpt    5     45.000                ms
StringConcatBenchmark.concatByStringBuffer                      thrpt    5   1999.696 ±  93.388  ops/ms
StringConcatBenchmark.concatByStringBuffer:gc.alloc.rate        thrpt    5   3127.063 ± 146.275  MB/sec
StringConcatBenchmark.concatByStringBuffer:gc.alloc.rate.norm   thrpt    5   1640.000 ±   0.001    B/op
StringConcatBenchmark.concatByStringBuffer:gc.count             thrpt    5     75.000            counts
StringConcatBenchmark.concatByStringBuffer:gc.time              thrpt    5     44.000                ms
StringConcatBenchmark.concatByStringBuilder                     thrpt    5   2888.358 ± 268.186  ops/ms
StringConcatBenchmark.concatByStringBuilder:gc.alloc.rate       thrpt    5   4450.531 ± 413.167  MB/sec
StringConcatBenchmark.concatByStringBuilder:gc.alloc.rate.norm  thrpt    5   1616.000 ±   0.001    B/op
StringConcatBenchmark.concatByStringBuilder:gc.count            thrpt    5     54.000            counts
StringConcatBenchmark.concatByStringBuilder:gc.time             thrpt    5     36.000                ms

可以看到通过加号进行字符串拼接的吞吐量最低,通过StringBuilder进行字符串拼接的吞吐量最高。

对比编译生成的字节码文件:

// access flags 0x1public concatByPlus()Ljava/lang/String;@Lorg/openjdk/jmh/annotations/Benchmark;()L0LINENUMBER 20 L0LDC ""ASTORE 1L1LINENUMBER 21 L1ICONST_0ISTORE 2L2FRAME APPEND [java/lang/String I]ILOAD 2BIPUSH 100IF_ICMPGE L3L4LINENUMBER 22 L4NEW java/lang/StringBuilderDUPINVOKESPECIAL java/lang/StringBuilder.<init> ()VALOAD 1INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;ILOAD 2INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder;INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;ASTORE 1L5LINENUMBER 21 L5IINC 2 1GOTO L2L3LINENUMBER 24 L3FRAME CHOP 1ALOAD 1ARETURNL6LOCALVARIABLE i I L2 L3 2LOCALVARIABLE this Lcn/ken/StringConcatBenchmark; L0 L6 0LOCALVARIABLE str Ljava/lang/String; L1 L6 1MAXSTACK = 2MAXLOCALS = 3

可以看到通过加号拼接的字符串,编译之后会在循环内重复创建StringBuilder对象,因此会带来很大的性能损耗,故吞吐量远少于其他两种方式,产生的需要回收的对象也远超其他两种方式。

相关文章:

  • inverse-design-of-grating-coupler-3d
  • el-scrollbar 获取滚动条高度 并将滚动条保持在低端
  • Vue 3.0 中的slot及使用场景
  • 【Odoo】Pycharm导入运行Odoo15
  • LOF算法(局部异常因子)python实现代码
  • 自适应Prompt技术:让LLM精准理解用户意图的进阶策略
  • 大模型为什么学新忘旧(大模型为什么会有灾难性遗忘)?
  • 当AI自我纠错:一个简单的“Wait“提示如何让模型思考更深、推理更强
  • ProfibusDP转ModbusRTU的实用攻略
  • MT4量化交易的书籍
  • 合并K个升序链表
  • jenkins pipeline实现CI/CD
  • Java中的伪共享(False Sharing):隐藏的性能杀手与高并发优化实战
  • 安卓应用层抓包通杀脚本 r0capture 详解
  • 贝叶斯公式:用新证据更新旧判断: P(B∣A)⋅P(A)
  • Java正则表达式:从基础到高级应用全解析
  • 第4章 部署与固件发布:OTA、版本管理与制品仓库
  • Python爬虫实战:通过PyExecJS库实现逆向解密
  • 深度估计中为什么需要已知相机基线(known camera baseline)?
  • C++23 放宽范围适配器以允许仅移动类型(P2494R2)
  • 从《缶翁的世界》看吴昌硕等湖州籍书画家对海派的影响
  • 上市公司重大资产重组新规九要点:引入私募“反向挂钩”,压缩审核流程
  • 终于,俄罗斯和乌克兰谈上了
  • 讲座预告|以危机为视角解读全球治理
  • “AD365特应性皮炎疾病教育项目”启动,助力提升认知与规范诊疗
  • 农行再回应客户办理业务期间离世:亲属连续三次输错密码,理解亲属悲痛,将协助做好善后