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 CoreJMH 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();
}

关键点:

  • @Paramiterations 注入测试参数
  • @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 编译器直接优化掉了对象创建代码。

✅ 解决方案:让编译器认为代码有副作用

  1. 返回对象:
    @Benchmark
    public Object pillarsOfCreation() {
     return new Object();
    }
    
  2. 使用 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 仓库


原始标题:Microbenchmarking with Java | Baeldung

« 上一篇: Byte Buddy指南
» 下一篇: Java周报,187