JMH基准测试
一、JMH
JMH即Java Microbenchmark Harness,是Java用来做基准测试的一个工具,该工具由OpenJDK提供并维护,测试结果可信度高。
基准测试Benchmark是测量、评估软件性能指标的一种测试,对某个特定目标场景的某项性能指标进行定量的和可对比的测试。
1.1 为什么要用JMH
if 快还是 switch 快?HashMap 的初始化 size 要不要指定,指定之后性能可以提高多少?各种序列化方法哪个耗时更短?
无论出自何种原因需要进行性能评估,量化指标总是必要的。
在大部分场合,简单地回答谁快谁慢是远远不够的,如何将程序性能量化呢?
这就需要我们的主角 JMH 登场了!
二、前期准备
2.1 引入依赖
1<dependencies>
2 <dependency>
3 <groupId>org.openjdk.jmh</groupId>
4 <artifactId>jmh-core</artifactId>
5 <version>1.23</version>
6 </dependency>
7
8 <dependency>
9 <groupId>org.openjdk.jmh</groupId>
10 <artifactId>jmh-generator-annprocess</artifactId>
11 <version>1.23</version>
12 </dependency>
13</dependencies>2.2 安装插件
jmh-java-microbenchmark-harness

当安装插件后,只要被@Benchmark就会认为是一个test方法,出现执行按钮

当然也可以不安装插件,如下代码也会进行输出
1 public static void main(String[] args) throws Exception {
2 Options opt = new OptionsBuilder()
3 .include(BenchmarkTest.class.getSimpleName())
4 .resultFormat(ResultFormatType.JSON)
5 .build();
6new Runner(opt).run();
7}三、使用说明
在正式使用前,先介绍下JMH基准测试中可以使用的注解。

3.1 @BenchmarkMode
用来配置 Mode 选项,可用于类或者方法上,这个注解的 value 是一个数组,可以把几种 Mode 集合在一起执行,如:@BenchmarkMode({Mode.SampleTime, Mode.AverageTime}),还可以设置为 Mode.All,即全部执行一遍。
- Throughput:整体吞吐量,每秒执行了多少次调用,单位为 ops/time
- AverageTime:用的平均时间,每次操作的平均时间,单位为 time/op
- SampleTime:随机取样,最后输出取样结果的分布
- SingleShotTime:只运行一次,往往同时把 Warmup 次数设为 0,用于测试冷启动时的性能
- All:上面的所有模式都执行一次
3.2 @State
通过 State 可以指定一个对象的作用范围,JMH 根据 scope 来进行实例化和共享操作。@State 可以被继承使用,如果父类定义了该注解,子类则无需定义。由于 JMH 允许多线程同时执行测试,不同的选项含义如下
- Scope.Benchmark:所有测试线程共享一个实例,测试有状态实例在多线程共享下的性能
- Scope.Group:同一个线程在同一个 group 里共享实例
- Scope.Thread:默认的 State,每个测试线程分配一个实例
3.3 @OutputTimeUnit
为统计结果的时间单位,可用于类或者方法注解
3.4 @Warmup
预热所需要配置的一些基本测试参数,可用于类或者方法上。一般前几次进行程序测试的时候都会比较慢,所以要让程序进行几轮预热,保证测试的准确性。参数如下所示:
- iterations:预热的次数
- time:每次预热的时间
- timeUnit:时间的单位,默认秒
- batchSize:批处理大小,每次操作调用几次方法
为什么需要预热?
因为 JVM 的 JIT 机制的存在,如果某个函数被调用多次之后,JVM 会尝试将其编译为机器码,从而提高执行速度,所以为了让 benchmark 的结果更加接近真实情况就需要进行预热。
3.5 @Measurement
实际调用方法所需要配置的一些基本测试参数,可用于类或者方法上,参数和 @Warmup 相同。
- iterations:执行的次数
- time:每次执行时间
- timeUnit:时间的单位,默认秒
- batchSize:批处理大小,每次操作调用几次方法
3.6 @Threads
配置同时起多少个线程执行,默认值世 Runtime.getRuntime().availableProcessors()
3.7 @Fork
进行 fork 的次数,可用于类或者方法上。如果 fork 数是 2 的话,则 JMH 会 fork 出两个进程来进行测试。
3.8 @Param
指定某项参数的多种情况,特别适合用来测试一个函数在不同的参数输入的情况下的性能,只能作用在字段上,使用该注解必须定义 @State 注解。
四、场景案例
4.1 for循环性能
循环100w次,并输出。预热2次,执行2次,2个线程,1个进程,统计结果单位毫秒。

| 方法 | 平均耗时 |
|---|---|
| 普通for循环 | 7898ms |
| 增强for循环 | 8078ms |
| 迭代器 | 8314ms |
| Stream循环 | 9085ms |
1
2import org.openjdk.jmh.annotations.*;
3import org.openjdk.jmh.results.format.ResultFormatType;
4import org.openjdk.jmh.runner.Runner;
5import org.openjdk.jmh.runner.options.Options;
6import org.openjdk.jmh.runner.options.OptionsBuilder;
7
8import java.util.ArrayList;
9import java.util.Iterator;
10import java.util.List;
11import java.util.concurrent.TimeUnit;
12
13@BenchmarkMode(Mode.AverageTime)
14@OutputTimeUnit(TimeUnit.MILLISECONDS)
15@Warmup(iterations = 2)
16@Measurement(iterations = 2)
17@Threads(2)
18@Fork(1)
19@State(Scope.Thread)
20public class ForBenchmarkTest {
21
22 private static List<Integer> integers;
23
24static {
25 integers = generate(1000000);
26}
27
28 private static List<Integer> generate(Integer max) {2
29 List<Integer> result = new ArrayList<>(max);
30for (int i = 0; i < max; i++) {
31 result.add(i);
32}
33 return result;
34}
35
36 @Benchmark
37 public void forTest() {
38 for (int i = 0; i < integers.size(); i++) {
39 System.out.println(integers.get(i));
40}
41 }
42
43 @Benchmark
44 public void forEachTest() {
45 for (Integer integer : integers) {
46 System.out.println(integer);
47}
48 }
49
50 @Benchmark
51 public void iteratorTest() {
52 Iterator<Integer> iterator = integers.iterator();
53while (iterator.hasNext()) {
54 System.out.println(iterator.next());
55}
56 }
57
58 @Benchmark
59 public void streamForEachTest() {
60 integers.forEach(System.out::println);
61}
62
63 public static void main(String[] args) throws Exception {
64 Options opt = new OptionsBuilder()
65 .include(ForBenchmarkTest.class.getSimpleName())
66 .resultFormat(ResultFormatType.JSON)
67 .build();
68new Runner(opt).run();
69}
70}
714.2 反射性能

通过上面数据可以得到。反射缺失很耗时。开启安全检查比不开启耗时, 如果反射在加上缓存,会比较好一点。
- metaClass1k 是1k次操作,反射+缓存
- metaClass1w 是1w次操作,反射+缓存
- test1k 是1k次正常操作
- test1w 是1w次正常操作
- testReflection1k 是1k次操作,反射
- testReflection1w 是1w次操作,反射
- testReflectionAccessible1k 是1k次操作,反射,关闭安全检查
- testReflectionAccessible1k 是1w次操作,反射,关闭安全检查
1@BenchmarkMode(Mode.AverageTime)
2@OutputTimeUnit(TimeUnit.SECONDS)
3@Warmup(iterations = 5, time = 1)
4@Measurement(iterations = 10, time = 1)
5@Threads(2)
6@Fork(1)
7@State(Scope.Thread)
8public class ReflectionBenchmarkTest {
9
10 private static class Mode {
11 private Integer age;
12
13public void setAge(Integer age) {
14 this.age = age;
15}
16 }
17
18 @Benchmark
19 public void test1k() {
20 Mode mode = new Mode();
21Loops.loop(1000, mode::setAge);
22}
23
24 @Benchmark
25 public void testReflection1k() {
26 Mode mode = new Mode();
27Loops.loop(1000, i -> {
28 Method setAge = null;
29 try {
30 setAge = mode.getClass().getMethod("setAge", Integer.class);
31 setAge.invoke(mode, i);
32 } catch (Throwable e) {
33 throw new RuntimeException(e);
34 }
35 });
36}
37
38 @Benchmark
39 public void testReflectionAccessible1k() {
40 Mode mode = new Mode();
41Loops.loop(1000, i -> {
42 Method setAge = null;
43 try {
44 setAge = mode.getClass().getMethod("setAge", Integer.class);
45 setAge.setAccessible(true);
46 setAge.invoke(mode, i);
47 } catch (Throwable e) {
48 throw new RuntimeException(e);
49 }
50 });
51}
52
53 @Benchmark
54 public void test1w() {
55 Mode mode = new Mode();
56Loops.loop(10000, mode::setAge);
57}
58
59 @Benchmark
60 public void testReflection1w() {
61 Mode mode = new Mode();
62Loops.loop(10000, i -> {
63 Method setAge = null;
64 try {
65 setAge = mode.getClass().getMethod("setAge", Integer.class);
66 setAge.invoke(mode, i);
67 } catch (Throwable e) {
68 throw new RuntimeException(e);
69 }
70 });
71}
72
73 @Benchmark
74 public void testReflectionAccessible1w() {
75 Mode mode = new Mode();
76Loops.loop(10000, i -> {
77 Method setAge = null;
78 try {
79 setAge = mode.getClass().getMethod("setAge", Integer.class);
80 setAge.setAccessible(true);
81 setAge.invoke(mode, i);
82 } catch (Throwable e) {
83 throw new RuntimeException(e);
84 }
85 });
86}
87
88 @Benchmark
89 public void metaClass1k() {
90 MetaClass metaClass = MetaClass.forClass(Mode.class, new DefaultReflectorFactory());
91Mode mode = new Mode();
92Loops.loop(10000, i -> {
93 try {
94 Invoker setAge = metaClass.getSetInvoker("age");
95 setAge.invoke(mode, new Object[]{i});
96 } catch (Throwable e) {
97 throw new RuntimeException(e);
98 }
99 });
100}
101
102 @Benchmark
103 public void metaClass1w() {
104 MetaClass metaClass = MetaClass.forClass(Mode.class, new DefaultReflectorFactory());
105Mode mode = new Mode();
106Loops.loop(10000, i -> {
107 try {
108 Invoker setAge = metaClass.getSetInvoker("age");
109 setAge.invoke(mode, new Object[]{i});
110 } catch (Throwable e) {
111 throw new RuntimeException(e);
112 }
113 });
114}
115
116
117 public static void main(String[] args) throws Exception {
118 Options opt = new OptionsBuilder()
119 .include(ReflectionBenchmarkTest.class.getSimpleName())
120 .resultFormat(ResultFormatType.JSON)
121 .build();
122new Runner(opt).run();
123}
124}
125