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 - 该状态在所有线程间共享。