1. 简介
本文聚焦 JMH(Java Microbenchmark Harness)的使用。首先我们将了解其 API 基础,然后探讨编写微基准测试时的最佳实践。
简单来说,JMH 帮我们处理了 JVM 预热和代码优化路径等复杂问题,让基准测试变得异常简单。
2. 快速上手
在 Maven 项目中,首先需要在 pom.xml 中添加依赖:
<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>
最新版 JMH Core 和 JMH Annotation Processor 可在 Maven Central 获取。
接着需要配置 Maven Compiler Plugin 启用注解处理器,否则可能遇到 Unable to find the resource: /META-INF/BenchmarkList 错误:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<source>17</source>
<target>17</target>
<annotationProcessorPaths>
<path>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.37</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
现在创建简单基准测试,使用 @Benchmark 注解标记方法(必须是 public 方法),包含该方法的类也需声明为 public:
@Benchmark
public void init() {
// Do nothing
}
添加启动类:
public class BenchmarkRunner {
public static void main(String[] args) throws Exception {
org.openjdk.jmh.Main.main(args);
}
}
⚠️ 注意:此 main 方法需通过 exec:exec
运行。若使用 exec:java
且未配置 @fork(0)
,可能报错 Could not find or load main class org.openjdk.jmh.runner.ForkedMain。有两种解决方案:
方案一:在 main 方法中动态设置 classpath
URLClassLoader classLoader = (URLClassLoader) BenchmarkRunner.class.getClassLoader();
StringBuilder classpath = new StringBuilder();
for (URL url : classLoader.getURLs()) {
classpath.append(url.getPath()).append(File.pathSeparator);
}
System.setProperty("java.class.path", classpath.toString());
方案二:通过 pom.xml 配置(推荐)
<plugin>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>build-classpath</id>
<goals>
<goal>build-classpath</goal>
</goals>
<configuration>
<includeScope>runtime</includeScope>
<outputProperty>depClasspath</outputProperty>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<configuration>
<mainClass>BenchmarkRunner</mainClass>
<systemProperties>
<systemProperty>
<key>java.class.path</key>
<value>${project.build.outputDirectory}${path.separator}${depClasspath}</value>
</systemProperty>
</systemProperties>
</configuration>
</plugin>
运行后输出结果示例:
# Run complete. Total time: 00:06:45
Benchmark Mode Cnt Score Error Units
BenchMark.init thrpt 200 3099210741.962 ± 17510507.589 ops/s
3. 基准测试类型
JMH 支持四种测试模式,通过 @BenchmarkMode 配置:
- Throughput(吞吐量)
- AverageTime(平均时间)
- SampleTime(采样时间)
- SingleShotTime(单次执行时间)
示例代码:
@Benchmark
@BenchmarkMode(Mode.AverageTime)
public void init() {
// Do nothing
}
输出将显示平均耗时(而非吞吐量):
# Run complete. Total time: 00:00:40
Benchmark Mode Cnt Score Error Units
BenchMark.init avgt 20 ≈ 10⁻⁹ s/op
4. 预热与执行配置
通过 @Fork 控制执行流程:
value
:实际执行次数warmup
:预热轮次(结果不计数)
@Benchmark
@Fork(value = 1, warmups = 2)
@BenchmarkMode(Mode.Throughput)
public void init() {
// Do nothing
}
这表示先进行 2 轮预热,再执行 1 轮正式测试。
额外可通过 @Warmup 控制预热迭代次数,例如 @Warmup(iterations = 5)
(默认 20 次)。
5. 状态管理
以密码哈希性能测试为例,展示如何使用 State 对象:
@State(Scope.Benchmark)
public class ExecutionPlan {
@Param({ "100", "200", "300", "500", "1000" })
public int iterations;
public Hasher murmur3;
public String password = "4v3rys3kur3p455w0rd";
@Setup(Level.Invocation)
public void setUp() {
murmur3 = Hashing.murmur3_128().newHasher();
}
}
基准测试方法:
@Fork(value = 1, warmups = 1)
@Benchmark
@BenchmarkMode(Mode.Throughput)
public void benchMurmur3_128(ExecutionPlan plan) {
for (int i = plan.iterations; i > 0; i--) {
plan.murmur3.putString(plan.password, Charset.defaultCharset());
}
plan.murmur3.hash();
}
关键点:
- @Param 为
iterations
注入测试参数 - @Setup(Level.Invocation) 确保每次调用前初始化新 Hasher
输出示例:
# Run complete. Total time: 00:06:47
Benchmark (iterations) Mode Cnt Score Error Units
BenchMark.benchMurmur3_128 100 thrpt 20 92463.622 ± 1672.227 ops/s
BenchMark.benchMurmur3_128 200 thrpt 20 39737.532 ± 5294.200 ops/s
BenchMark.benchMurmur3_128 300 thrpt 20 30381.144 ± 614.500 ops/s
BenchMark.benchMurmur3_128 500 thrpt 20 18315.211 ± 222.534 ops/s
BenchMark.benchMurmur3_128 1000 thrpt 20 8960.008 ± 658.524 ops/s
6. 死代码消除
⚠️ 警惕 JIT 优化对测试结果的干扰,看这个踩坑案例:
@Benchmark
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode(Mode.AverageTime)
public void doNothing() {
}
@Benchmark
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode(Mode.AverageTime)
public void objectCreation() {
new Object();
}
预期对象创建应比空操作慢,但实际输出:
Benchmark Mode Cnt Score Error Units
BenchMark.doNothing avgt 40 0.609 ± 0.006 ns/op
BenchMark.objectCreation avgt 40 0.613 ± 0.007 ns/op
这是死代码消除导致的假象:JIT 编译器直接优化掉了对象创建代码。
✅ 解决方案:让编译器认为代码有副作用
- 返回对象:
@Benchmark public Object pillarsOfCreation() { return new Object(); }
- 使用 Blackhole 消费对象:
@Benchmark public void blackHole(Blackhole blackhole) { blackhole.consume(new Object()); }
修正后输出符合预期:
Benchmark Mode Cnt Score Error Units
BenchMark.blackHole avgt 20 4.126 ± 0.173 ns/op
BenchMark.doNothing avgt 20 0.639 ± 0.012 ns/op
BenchMark.objectCreation avgt 20 0.635 ± 0.011 ns/op
BenchMark.pillarsOfCreation avgt 20 4.061 ± 0.037 ns/op
7. 常量折叠
再看一个优化陷阱:
@Benchmark
public double foldedLog() {
int x = 8;
return Math.log(x);
}
JIT 会进行常量折叠优化,直接编译为:
@Benchmark
public double foldedLog() {
return 2.0794415416798357; // Math.log(8) 的结果
}
✅ 解决方案:将常量移入 State 对象
@State(Scope.Benchmark)
public static class Log {
public int x = 8;
}
@Benchmark
public double log(Log input) {
return Math.log(input.x);
}
对比测试结果:
Benchmark Mode Cnt Score Error Units
BenchMark.foldedLog thrpt 20 449313097.433 ± 11850214.900 ops/s
BenchMark.log thrpt 20 35317997.064 ± 604370.461 ops/s
优化后的 foldedLog
吞吐量是真实计算的 log
的 12 倍,验证了常量折叠的影响。
8. 总结
本文系统介绍了 JMH 微基准测试框架的核心用法和避坑指南。掌握这些技术后,你就能写出真正可靠的 Java 性能测试代码。
完整示例代码见 GitHub 仓库。