Java 微基准测试工具 JMH

JMH(Java Microbenchmark Harness)是一个进行基准测试的工具,由 OpenJDK 团队研发,JMH 可以一个方法为维度进行吞吐量、调用时间等测试,精度可以精确到微秒级,JMH 提供注解可以更加便捷的使用。

JMH 使用注意点

  • 测试前需要预热
  • 防止无用代码进入测试方法种
  • 防止代码消除

如何使用 JMH

Maven 依赖:

<!-- JMH 基准依赖 -->
<dependency>
    <groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-core</artifactId>
    <version>1.28</version>
</dependency>

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

启动程序

此示例程序可以直接启动。

// 测试类型,例如吞吐、平均时间等
@BenchmarkMode({Mode.Throughput})
// 预热,iterations预热次数,time时间限制
@Warmup(iterations = 1, time = 2)
// 度量方式,iterations执行次数,time时间限制
@Measurement(iterations = 1, time = 2)
// 使用的线程数
@Threads(4)
// 使用几个JVM进程跑
@Fork(1)
// 成员变量共享方式
@State(value = Scope.Benchmark)
// 时间单位
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public class SimpleBenchmark {

    int cnt;

    // 初始化方法
    @Setup
    public void init() {
        cnt = 10;
    }

    @Benchmark
    public String test() {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < cnt; i ++) {
            sb.append(i + 1);
        }
        return sb.toString();
    }

    // 引导JMH启动
    public static void main(String[] args) throws Exception{
        Options op = new OptionsBuilder()
                .include(SimpleBenchmark.class.getSimpleName())
                .build();
        new Runner(op).run();
    }
}

输出示例:

# Blackhole mode: full + dont-inline hint
# Warmup: 1 iterations, 2 s each
# Measurement: 1 iterations, 2 s each
# Timeout: 10 min per iteration
# Threads: 4 threads, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: org.forza.benchmark.SimpleBenchmark.test

# Run progress: 0.00% complete, ETA 00:00:04
# Fork: 1 of 1
# Warmup Iteration   1: 32499.936 ops/ms
Iteration   1: 38976.381 ops/ms


Result "org.forza.benchmark.SimpleBenchmark.test":
  38976.381 ops/ms

# Run complete. Total time: 00:00:04

REMEMBER: 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
SimpleBenchmark.test  thrpt       38976.381          ops/ms

Process finished with exit code 0

用 maven 打后使用 java -jar 启动

需要增加一个 Maven Plugin。

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>2.4.1</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <finalName>benchmark</finalName>
                            <transformers>
                                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <mainClass>org.openjdk.jmh.Main</mainClass>
                                </transformer>
                            </transformers>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

运行打包命令,运行 JMH 测试。

maven package
java -jar target/benchmark.jar

常用注解

@BenchmarkMode

指定基准测试类型。这里选择的是 Throughput 也就是吞吐量,吞吐量在这里是单位时间内可以执行方法的次数。

  • Throughput - 整体吞吐量,例如“1 秒内可以执行多少次调用”。
  • AverageTime - 调用的平均时间,例如“每次调用平均耗时 xxx 毫秒”。
  • SampleTime - 随机取样,最后输出取样结果的分布,例如“99%的调用在 xxx 毫秒以内,99.99%的调用在 xxx 毫秒以内”
  • SingleShotTime - 以上模式都是默认一次 iteration 是 1s,唯有 SingleShotTime 是只运行一次。往往同时把 warmup 次数设为 0,用于测试冷启动时的性能。 All - 所有模式,执行测试时执行以上所有类型测试

@Warmup

用来对程序进行预热。

为什么需要预热?因为 JVM 的 JIT 机制,一个方法被调用多次之后 JVM 会尝试将其编译为机器码,从而提高其运行速度。

@Measurement

用来指定度量的方式,为基准设置默认测量参数。

参数:

  • iterations : 执行测试的轮数
  • time :每轮的时间
  • timeUnit : 时间单位,默认:秒
  • batchSize:执行方法次数

每轮时间(time)和方法执行次数(batchSize)都是限制,满足其中一个一轮就结束。

@Threads

每个 JVM 进程中的测试线程数量,根据具体情况选择,一般为 CPU 核数 * 2

@Fork

一般指定为 1,既使用一个 JVM 进程运行。 有时一个 JVM 进行执行 benchmark 会存在误差,为了消除这种误差可以指定 Fork 大于 1,此时会多次运行编写的 benchmark。

@OutputTImeUnit

输出结果的时间单位。

@Benchmark

标识一个方法是一个测试。

@Param

类成员级别注解,@Param 可以用来指定某项参数的多种情况,特别适合用来测试一个函数在不同的参数输入的情况下的性能。

@Setup

方法级别注解,标识一个初始化方法,初始化方法在 benchmark 执行前初始执行。

State

当使用 @Setup 参数的时候,必须在类上加这个参数,不然会提示无法运行。

State 用于声明某个类是一个“状态”,然后接受一个 Scope 参数用来表示该状态的共享范围。 因为很多 benchmark 会需要一些表示状态的类,JMH 允许你把这些类以依赖注入的方式注入到 benchmark 函数里。Scope 主要分为三种。

  • Thread - 该状态为每个线程独享。
  • Group - 该状态为同一个组里面所有线程共享。
  • Benchmark - 该状态在所有线程间共享。

资料