JMH - Java 代码性能测试的终极利器、必须掌握

JMH 介绍

那么如何对 Java 程序进行一次精准的性能测试呢?难道需要掌握很多 JVM 优化细节吗?难道要研究如何避免,并进行正确编码才能进行严格的性能测试吗?显然不是,如果是这样的话,未免过于困难了,好在有一款一款官方的微基准测试工具 - JMH.

JMH 的全名是 Java Microbenchmark Harness,它是由 Java 虚拟机团队开发的一款用于 Java 微基准测试工具。用自己开发的工具测试自己开发的另一款工具,以子之矛,攻子之盾果真手到擒来,如臂使指。使用 JMH 可以让你方便快速的进行一次严格的代码基准测试,并且有多种测试模式,多种测试维度可供选择;而且使用简单、增加注解便可启动测试。

JMH 使用

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.36</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.36</version>
</dependency>

demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@State(Scope.Thread) //表明了类中变量的作用范围
@Warmup(iterations = 3)//用于做预热配置 iterations:预热迭代的次数
@Measurement(iterations = 5) // 与预热相同 ,并且执行结果会被统计到测试结果中
public class sample {
String string = "";
StringBuilder stringBuilder = new StringBuilder();

@Benchmark// 加上这个注解就会被扫描到
@BenchmarkMode(Mode.AverageTime) // 输出平均时间
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public String stringAdd() {
for (int i = 0; i < 1000; i++) {
string = string + i;
}
return string;
}

@Benchmark // 加上这个注解就会被扫描到
@BenchmarkMode(Mode.AverageTime) // 输出平均时间
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public String stringBuilderAppend() {
for (int i = 0; i < 1000; i++) {
stringBuilder.append(i);
}
return stringBuilder.toString();
}

public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(sample.class.getSimpleName()) // 会扫描sample中@Benchmark的方法进行测试
.forks(1) // 线程数
.build();
new Runner(opt).run();
}
}

输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# JMH version: 1.36
# VM version: JDK 18.0.1.1, Java HotSpot(TM) 64-Bit Server VM, 18.0.1.1+2-6
# VM invoker: D:\program\jdk-18.0.1.1\bin\java.exe
# VM options: -javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2022.3.2\lib\idea_rt.jar=5220:C:\Program Files\JetBrains\IntelliJ IDEA 2022.3.2\bin -Dfile.encoding=UTF-8 -Dsun.stdout.encoding=UTF-8 -Dsun.stderr.encoding=UTF-8
# Blackhole mode: compiler (auto-detected, use -Djmh.blackhole.autoDetect=false to disable)
# Warmup: 3 iterations, 10 s each
# Measurement: 5 iterations, 10 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: com.panther.DB.jmh.sample.stringAdd

# Run progress: 0.00% complete, ETA 00:02:40
# Fork: 1 of 1
# Warmup Iteration 1: 60.229 ms/op
# Warmup Iteration 2: 69.095 ms/op
# Warmup Iteration 3: 105.360 ms/op
Iteration 1: 141.599 ms/op
Iteration 2: 169.866 ms/op
Iteration 3: 208.903 ms/op
Iteration 4: 287.697 ms/op
Iteration 5: 321.151 ms/op


Result "com.panther.DB.jmh.sample.stringAdd":
225.843 ±(99.9%) 294.688 ms/op [Average]
(min, avg, max) = (141.599, 225.843, 321.151), stdev = 76.529 // 执行的最小、平均、最大、误差值
CI (99.9%): [≈ 0, 520.531] (assumes normal distribution)


# JMH version: 1.36
# VM version: JDK 18.0.1.1, Java HotSpot(TM) 64-Bit Server VM, 18.0.1.1+2-6
# VM invoker: D:\program\jdk-18.0.1.1\bin\java.exe
# VM options: -javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2022.3.2\lib\idea_rt.jar=5220:C:\Program Files\JetBrains\IntelliJ IDEA 2022.3.2\bin -Dfile.encoding=UTF-8 -Dsun.stdout.encoding=UTF-8 -Dsun.stderr.encoding=UTF-8
# Blackhole mode: compiler (auto-detected, use -Djmh.blackhole.autoDetect=false to disable)
# Warmup: 3 iterations, 10 s each
# Measurement: 5 iterations, 10 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: com.panther.DB.jmh.sample.stringBuilderAppend

# Run progress: 50.00% complete, ETA 00:01:21
# Fork: 1 of 1
# Warmup Iteration 1: 1.724 ms/op
# Warmup Iteration 2: 4.094 ms/op
# Warmup Iteration 3: 4.558 ms/op
Iteration 1: 5.293 ms/op
Iteration 2: 5.077 ms/op
Iteration 3: 6.040 ms/op
Iteration 4: 6.449 ms/op
Iteration 5: 7.213 ms/op


Result "com.panther.DB.jmh.sample.stringBuilderAppend":
6.014 ±(99.9%) 3.349 ms/op [Average]
(min, avg, max) = (5.077, 6.014, 7.213), stdev = 0.870
CI (99.9%): [2.666, 9.363] (assumes normal distribution)


# Run complete. Total time: 00:02:43

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.

NOTE: Current JVM experimentally supports Compiler Blackholes, and they are in use. Please exercise
extra caution when trusting the results, look into the generated code to check the benchmark still
works, and factor in a small probability of new VM bugs. Additionally, while comparisons between
different JVMs are already problematic, the performance difference caused by different Blackhole
modes can be very significant. Please make sure you use the consistent Blackhole mode for comparisons.

Benchmark Mode Cnt Score Error Units
DB.jmh.sample.stringAdd avgt 5 225.843 ± 294.688 ms/op
DB.jmh.sample.stringBuilderAppend avgt 5 6.014 ± 3.349 ms/op

Process finished with exit code 0

Result “com.panther.DB.jmh.sample.stringAdd“:
225.843 ±(99.9%) 294.688 ms/op [Average]
(min, avg, max) = (141.599, 225.843, 321.151), stdev = 76.529 // 执行的最小、平均、最大、误差值

Result “com.panther.DB.jmh.sample.stringBuilderAppend”:
6.014 ±(99.9%) 3.349 ms/op [Average]
(min, avg, max) = (5.077, 6.014, 7.213), stdev = 0.870

String 拼接的平均时间: 225.843ms

StringBuilder 拼接的平均时间: 6.014

常用注解

  1. @Benchmark 用于方法上且该方法必须使用public修饰,表明该方法为基准测试方法。
  2. @BenchmarkMode 用于方法或类上,表明测试指标
    • Mode.Throughput,吞吐量,单位时间内执行的次数;
    • Mode.AverageTime,平均时间,执行方法的平均耗时;
    • Mode.SampleTime,操作时间采样,并输出结果分布;
    • Mode.SingleShotTime,单次操作时间,通常在不进行预热时测试冷启动的时间。
  3. @Warmup 用于方法或类上,用于做预热配置
    • iterations,预热迭代的次数;
    • time,每个预热迭代的时间;
    • timeUnit,时间单位;
    • batchSize,每个操作调用的次数。
  4. @State用于类上,表明了类中变量的作用范围
    • Scope.Benchmark,每个测试方法中使用一个变量;
    • Scope.Group,每个分组中使用同一个变量;
    • Scope.Thread,每个线程中使用同一个变量。
  5. @Setup用于方法上,基准测试前的初始化操作
    • Level.Trial,所有基准测试执行时;
    • Level.Iteration,每次迭代时;
    • Level.Invocation,每次方法调用时。
  6. @TearDown用于方法上,与Setup的作用相反,是基准测试后的操作
  7. @Threads用于方法和类上,指定基准测试中的并行线程数
  8. @Fork用于方法和类上,指定基准测试中Fork的子进程。Fork提供了6个参数:
    • value,表示Fork出的子进程数量;
    • warmups,预热次数;
    • jvm,JVM的位置;
    • jvmArgs,需要替换的JVM参数;
    • jvmArgsPrepend,需要添加的JVM参数;
    • jvmArgsAppend,需要追加的JVM参数。
  9. CompilerControl用于方法,构造器或类上,指定编译方式
    • BREAK,将断点插入到编译后的代码;
    • PRINT,打印方法及其配置;
    • EXCLUDE,禁止编译;
    • INLINE,使用内联;
    • DONT_INLINE,禁止内联;
    • COMPILE_ONLY,仅编译。