0%

无比强大的JMH基准测试工具使用

如果你想测试某个业务的吞吐量,想做不同的方法实现的性能对比,那么使用JMH就对了。

JMH可以做什么

JMH是一个工具包,主要用来测试一些方法的性能,可以根据不同的参数以不同的单位进行计算。JMH不止能对Java语言做基准测试,还能对运行在JVM上的其他语言做基准测试。而且可以分析到纳秒级别。

如何使用JMH

引入依赖

以1.19版本为例

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

demo演示

比较map不同方式初始化的性能

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
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
public class JMHSample_01_HelloWorld {

static class Demo {
int id;
String name;
public Demo(int id, String name) {
this.id = id;
this.name = name;
}
}

static List<Demo> demoList;
static {
demoList = new ArrayList();
for (int i = 0; i < 10000; i ++) {
demoList.add(new Demo(i, "test"));
}
}

@Benchmark
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
public void testHashMapWithoutSize() {
Map map = new HashMap();
for (Demo demo : demoList) {
map.put(demo.id, demo.name);
}
}

@Benchmark
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
public void testHashMap() {
Map map = new HashMap((int)(demoList.size() / 0.75f) + 1);
for (Demo demo : demoList) {
map.put(demo.id, demo.name);
}
}

//运行
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(JMHSample_01_HelloWorld.class.getSimpleName())
.forks(1)
.build();

new Runner(opt).run();
}
}

解释:分别定义两个基准测试的方法testHashMapWithoutSize和
testHashMap,这两个基准测试方法执行流程是:每个方法执行前都进行5次预热执行,每隔1秒进行一次预热操作,预热执行结束之后进行5次实际测量执行,每隔1秒进行一次实际执行,我们此次基准测试测量的是平均响应时长,单位是us。

ps: 引用的JMH版本不同,需要使用的注解有所不同,比如:0.7.1版本, @GenerateMicroBenchmark会对应折1.19版本的@Benchmark

与压测工具jMeter的不同

JMH和jMeter的使用场景还是有很大的不同的,jMeter更多的是对rest api进行压测,而JMH关注的粒度更细,它更多的是发现某块性能槽点代码,然后对优化方案进行基准测试对比。比如json序列化方案对比,bean copy方案对比等。

JMH适合细粒度的方法测试

经常使用的注解介绍

@Benchmark

@Benchmark标签是用来标记测试方法的,只有被这个注解标记的话,该方法才会参与基准测试,但是有一个基本的原则就是被@Benchmark标记的方法必须是public的。

@Warmup

@Warmup用来配置预热的内容,可用于类或者方法上,越靠近执行方法的地方越准确。一般配置warmup的参数有这些:

  • iterations:预热的次数。
  • time:每次预热的时间。
  • timeUnit:时间单位,默认是s。
  • batchSize:批处理大小,每次操作调用几次方法。
    为什么需要预热?因为 JVM 的 JIT 机制的存在,如果某个函数被调用多次之后,JVM 会尝试将其编译成为机器码从而提高执行速度。为了让 Benchmark 的结果更加接近真实情况就需要进行预热。

@Measurement

指定迭代的次数,每次迭代的运行时间和每次迭代测试调用的数量。

@BenchmarkMode

Mode 表示 JMH 进行 Benchmark 时所使用的模式。通常是测量的维度不同,或是测量的方式不同。目前 JMH 共有四种模式:

  • Mode.Throughput: 整体吞吐量,例如“1秒内可以执行多少次调用”,单位是操作数/时间。
  • Mode.AverageTime: 调用的平均时间,例如“每次调用平均耗时xxx毫秒”,单位是时间/操作数。
  • Mode.SampleTime: 随机取样,最后输出取样结果的分布,例如“99%的调用在xxx毫秒以内,99.99%的调用在xxx毫秒以内”
  • Mode.SingleShotTime: 以上模式都是默认一次 iteration 是 1s,唯有 SingleShotTime 是只运行一次。
  • Mode.All 运用所有的检测模式

ps:在方法级别指定@BenchmarkMode的时候可以一定指定多个纬度,例如:
@BenchmarkMode({Mode.Throughput, Mode.AverageTime, Mode.SampleTime, Mode.SingleShotTime}),代表同时在多个纬度对目标方法进行测量。

@OutputTimeUnit

@OutputTimeUnit代表测量的单位,比如秒级别,毫秒级别,微妙级别等等。一般都使用微妙和毫秒级别的稍微多一点。该注解可以用在方法级别和类级别,当用在类级别的时候会被更加精确的方法级别的注解覆盖,原则就是离目标更近的注解更容易生效。

@Iteration

Iteration 是 JMH 进行测试的最小单位。在大部分模式下,一次 iteration 代表的是一秒,JMH 会在这一秒内不断调用需要 Benchmark 的方法,然后根据模式对其采样,计算吞吐量,计算平均执行时间等。

@State

类注解,JMH测试类必须使用 @State 注解,它定义了一个类实例的生命周期,可以类比 Spring Bean 的 Scope。 @State的状态值主要有以下几种:

  • Scope.Thread:默认的 State,每个测试线程分配一个实例;
  • Scope.Benchmark:所有测试线程共享一个实例,用于测试有状态实例在多线程共享下的性能;
  • Scope.Group:每个线程组共享一个实例;

@Fork

进行 fork 的次数。如果 fork 数是2的话,则 JMH 会 fork 出两个进程来进行测试。

@Setup

方法注解,会在执行 benchmark 之前被执行,主要用于初始化。

@TearDown

方法注解,与@Setup 相对的,会在所有 benchmark 执行结束以后执行,主要用于资源的回收等。
@Setup/@TearDown注解使用Level参数来指定何时调用fixture:

  • Level.Trial: 默认level。全部benchmark运行(一组迭代)之前/之后
  • Level.Iteration: 一次迭代之前/之后(一组调用)
  • Level.Invocation: 每个方法调用之前/之后(不推荐使用,除非你清楚这样做的目的)

@Param

成员注解,可以用来指定某项参数的多种情况。特别适合用来测试一个函数在不同的参数输入的情况下的性能。@Param 注解接收一个String数组,在 @Setup 方法执行前转化为为对应的数据类型。多个 @Param 注解的成员之间是乘积关系,譬如有两个用 @Param 注解的字段,第一个有5个值,第二个字段有2个值,那么每个测试方法会跑5*2=10次。

@Threads

测试线程的数量,可以配置在方法或者类上,代表执行测试的线程数量。

参考资料

官网:http://openjdk.java.net/projects/code-tools/jmh/
demo:http://hg.openjdk.java.net/code-tools/jmh/file/tip/jmh-samples/src/main/java/org/openjdk/jmh/samples/