Java微基准测试工具JMH
Posted 晓锋残月
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java微基准测试工具JMH相关的知识,希望对你有一定的参考价值。
1. JMH是什么
JMH(Java Microbenchmark Harness)是由OpenJDK Developer提供的基准测试工具(基准可以理解为比较的基础,我们将这一次性能测试结果作为基准结果,下一次的测试结果将与基准数据进行比较),它是一种常用的性能测试工具,解决了基准测试中常见的一些问题,本文将针对这些问题介绍如何正确的使用JMH,以及可视化测试结果。
JMH适用于细粒度的方法测试,并不适用于系统之间的链路测试!
JMH适用于细粒度的方法测试,并不适用于系统之间的链路测试!
JMH适用于细粒度的方法测试,并不适用于系统之间的链路测试!
2. JMH有什么用
通常上JMH主要有如下的使用场景:
(1)查看多少百分比的请求在多长时间内完成
(2)查看某个方式执行时间,以及执行时间和输入之间的相关性
(3)比较两个方法的性能(例如比较序列化中的fastjson
和jackson
,字符串拼接过程中的+
和append
,还有数字相加时的串行方式和并行方式等等。)
3. JMH怎么用
3.1 使用前提准备
首先最该使用maven
来添加依赖,添加依赖如下:
<!--添加相关依赖-->
<dependencies>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>$jmh.version</version>
</dependency>
<!--注意下面给的scope必须更改为provided-->
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>$jmh.version</version>
<scope>provided</scope>
</dependency>
</dependencies>
【注意】上面的第二个依赖中,对于<scope>
为provided
,在maven
中央仓库中默认是test
方式,这里需要修改,否则报错。
3.2 案例1:HashMap设定大小和不设定大小比较
// 每个方法执行前都进行5次预热执行,每隔1秒进行一次预热操作,
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
// 预热执行结束之后进行5次实际测量执行,每隔1秒进行一次实际执行,
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) //我们此次基准测试测量的是平均响应时长,单位是us。
public class FourthBnechmark
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 //用来标记被测量的方法,只有标记了才能参与基准测试,另外方法必须public
@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(FourthBnechmark.class.getSimpleName())
.forks(1)
.build();
new Runner(opt).run();
得到的结果如下:
Benchmark Mode Cnt Score Error Units
FourthBnechmark.testHashMap avgt 5 125.645 ± 10.300 us/op
FourthBnechmark.testHashMapWithoutSize avgt 5 133.398 ± 8.203 us/op
说明:对于一个Map结构,如果创建对象时,初始时给定创建的大小,将会提高程序运行的性能。
3.3 参数解释
1.注解参数
(1)@Benchmark
@Benchmark //表示该方式是用于测试的方法,必须是public的
public void testHashMap()
// .........
@Benchmark
标签是用来标记测试方法的,只有被这个注解标记的话,该方法才会参与基准测试,但是有一个基本的原则就是被@Benchmark
标记的方法必须是public
的。
(2)@Warmup
@Warmup
用来配置预热的内容,可用于类或者方法上,越靠近执行方法的地方越准确。
// 每个方法执行前都进行5次预热执行,每隔1秒进行一次实际执行,此次基准测试测量的是平均响应时长,单位是us。
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
一般配置warmup
的参数有这些:
参数 | 作用 |
---|---|
iterations | 预热的次数。 |
time | 每次预热的时间。 |
timeUnit | 时间单位,默认是s。 |
batchSize | 批处理大小,每次操作调用几次方法。(后面用到) |
(3)@Measurement
用来控制实际执行的内容,配置的选项本warmup一样。
(4)@BenchmarkMode
@BenchmarkMode
主要是表示测量的纬度,有以下这些纬度可供选择:
参数 | 作用 |
---|---|
Mode.Throughput | 吞吐量纬度 |
Mode.AverageTime | 平均时间 |
Mode.SampleTime | 抽样检测 |
Mode.SingleShotTime | 检测一次调用 |
Mode.All | 运用所有的检测模式 在方法级别指定@BenchmarkMode的时候可以一定指定多个纬度,例如: @BenchmarkMode(Mode.Throughput, Mode.AverageTime, Mode.SampleTime, Mode.SingleShotTime),代表同时在多个纬度对目标方法进行测量。 |
(5)@OutputTimeUnit
@OutputTimeUnit
代表测量的单位,比如秒级别,毫秒级别,微妙级别等等。一般都使用微妙和毫秒级别的稍微多一点。该注解可以用在方法级别和类级别,当用在类级别的时候会被更加精确的方法级别的注解覆盖,原则就是离目标更近的注解更容易生效。
@OutputTimeUnit(TimeUnit.MICROSECONDS)
public class SecondBenchmark
// .......
表示测量的单位是us。
(6)组合参数:@State
和@Param
在很多情况下,我们需要测试不同的参数的不同结果,但是测试的了逻辑又都是一样的,这时就需要使用@Param
@Param(value = "10", "50", "100" )
private int length;
这行代码设置就会依次执行lenght=10,50,100时候的基准测试方法。
如果只是用@Param
在编译时会报错,它必须配合@State
注解使用,@State
指定了对象共享范围。
JMH为我们提供了状态的支持。该注解只能用来标注在类上,因为类作为一个属性的载体。 @State
的状态值主要有以下几种:
参数 | 说明 |
---|---|
Scope.Benchmark | 该状态的意思是会在所有的Benchmark 的工作线程中共享变量内容。 |
Scope.Group | 同一个Group 的线程可以享有同样的变量 |
Scope.Thread | 每隔线程都享有一份变量的副本,线程之间对于变量的修改不会相互影响 |
(7)初始化和销毁:@Setup
和@TearDown
但是有些情况下我们需要对参数进行一些初始化或者释放的操作,就像Spring
提供的一些init
和destory
方法一样,JHM
也提供有这样的钩子:
@Setup
必须标示在@State
注解的类内部,表示初始化操作
@TearDown
必须表示在@State
注解的类内部,表示销毁操作
@Setup
和@TearDown
提供了以下三种纬度的控制:
参数 | 说明 |
---|---|
Level.Trial | 只会在个基础测试的前后执行。包括Warmup和Measurement阶段,一共只会执行一次。 |
Level.Iteration | 每次执行记住测试方法的时候都会执行,如果Warmup 和Measurement 都配置了2次执行的话,那么@Setup 和@TearDown 配置的方法的执行次数就4次。 |
Level.Invocation | 每个方法执行的前后执行(一般不推荐这么用) |
(9)@Threads
测试线程的数量,可以配置在方法或者类上,代表执行测试的线程数量.
@Threads(4)
表示线程数是4个。
(10)@Fork
有时候想结合多轮Benchmark的测试结果进行分析,这样就可以用到@Fork
注解。
@Fork(2)
表示Benchmark的测试会运行两轮。
2.方法
在main
方法中可能存在使用下面配置
public static void main(String[] args) throws RunnerException
Options opt = new OptionsBuilder()
.include(SecondBenchmark.class.getSimpleName())
.forks(2)
.warmupIterations(5)
.measurementIterations(5)
.result("E:/myJson.json")
.resultFormat(ResultFormatType.JSON)
.output("E:/SecondBenchmark.log")
.build();
new Runner(opt).run();
(1)include
benchmark 所在的类的名字,注意这里是使用正则表达式对所有类进行匹配的。
(2)fork
进行 fork 的次数。如果 fork 数是2的话,则 JMH 会 fork 出两个进程来进行测试。
(3)warmupIterations
预热的迭代次数。
(4)measurementIterations
实际测量的迭代次数。
(5)result
和resultFormat
生成文件的格式,这里通常将两个配置一起使用,用于生成一个json文件后,从而可用于生成一个可视化图表。
(6)output
默认日志打印到控制台,上面的设置是将日志文件打印到一个外部的log文件中。
3.4 案例2:比较串行计算和并行计算性能
1.创建一个接口Calculator
public interface Calculator
/**
* calculate sum of an integer array
* @param numbers
* @return
*/
public long sum(int[] numbers);
/**
* shutdown pool or reclaim any related resources
*/
public void shutdown();
2.创建串行计算实现类SinglethreadCalculator
public class SinglethreadCalculator implements Calculator
public long sum(int[] numbers)
long total = 0L;
for (int i : numbers)
total += i;
return total;
@Override
public void shutdown()
// nothing to do
3.创建并行计算实现类(该方法可以自行分析,就是多线程来计算)
public class MultithreadCalculator implements Calculator
private final int nThreads;
private final ExecutorService pool;
public MultithreadCalculator(int nThreads)
this.nThreads = nThreads;
this.pool = Executors.newFixedThreadPool(nThreads);
private class SumTask implements Callable<Long>
private int[] numbers;
private int from;
private int to;
public SumTask(int[] numbers, int from, int to)
this.numbers = numbers;
this.from = from;
this.to = to;
public Long call() throws Exception
long total = 0L;
for (int i = from; i < to; i++)
total += numbers[i];
return total;
public long sum(int[] numbers)
int chunk = numbers.length / nThreads;
int from, to;
List<SumTask> tasks = new ArrayList<SumTask>();
for (int i = 1; i <= nThreads; i++)
if (i == nThreads)
from = (i - 1) * chunk;
to = numbers.length;
else
from = (i - 1) * chunk;
to = i * chunk;
tasks.add(new SumTask(numbers, from, to));
try
List<Future<Long>> futures = pool.invokeAll(tasks);
long total = 0L;
for (Future<Long> future : futures)
total += future.get();
return total;
catch (Exception e)
// ignore
return 0;
@Override
public void shutdown()
pool.shutdown();
4.性能测试类
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Benchmark)
public class SecondBenchmark
// @Param 可以用来指定某项参数的多种情况。特别适合用来测试一个函数在不同的参数输入的情况下的性能。
@Param("10000", "100000", "1000000")
private int length;
private int[] numbers;
private Calculator singleThreadCalc;
private Calculator multiThreadCalc;
public static void main(String[] args) throws RunnerException
Options opt = new OptionsBuilder()
.include(SecondBenchmark.class.getSimpleName())
.forks(2)
.warmupIterations(5)
.measurementIterations(5)
.result("E:/myJson.json")
.resultFormat(ResultFormatType.JSON)
.build();
new Runner(opt).run();
@Benchmark
public long singleThreadBench()
return singleThreadCalc.sum(numbers);
@Benchmark
public long multiThreadBench()
return multiThreadCalc.sum(numbers);
@Setup // @Setup 会在执行 benchmark 之前被执行,正如其名,主要用于初始化。
public void prepare()
numbers = IntStream.rangeClosed(1, length).toArray();
singleThreadCalc = new SinglethreadCalculator();
multiThreadCalc = new MultithreadCalculator(Runtime.getRuntime().availableProcessors());
@TearDown // @TearDown 和 @Setup 相对的,会在所有 benchmark 执行结束以后执行,主要用于资源的回收等。
public void shutdown()
singleThreadCalc.shutdown();
multiThreadCalc.shutdown();
在测试类中指定了输出为myJson.json
。生成的文件可以用于下一步可视化处理。其中测试结果为:
Benchmark (length) Mode Cnt Score Error Units
SecondBenchmark.multiThreadBench 10000 avgt 10 16.851 ± 0.762 us/op
SecondBenchmark.multiThreadBench 100000 avgt 10 40.134 ± 0.206 us/op
SecondBenchmark.multiThreadBench 1000000 avgt 10 153.348 ± 6.716 us/op
SecondBenchmark.singleThreadBench 10000 avgt 10 3.300 ± 0.061 us/op
SecondBenchmark.singleThreadBench 100000 avgt 10 32.805 ± 0.393 us/op
SecondBenchmark.singleThreadBench 1000000 avgt 10 350.369 ± 11.015 us/op
通过结果可以发现,当数据量在100000以下时,使用串行计算,使用时间较短。但是如果时间大于100000,那么就需要使用并行计算,此时的时间只需要串行计算时间的一半。
4. JMH可视化工具
上面的生成的myJson.json
文件,用JMH Visual Chart。可以发现串行计算刚开始时间使用较短,但是当数据量达到100000以上时,花费时间明显变长。
在上述同类产品中,还有JMH Visualizer。有兴趣可以了解下。
5. 小结
以上便是对微基准测试工具JMH的使用学习。主要途径是参考下面资料,下面资料的内容更加详细,如有兴趣,自行阅读。
参考资料:
以上是关于Java微基准测试工具JMH的主要内容,如果未能解决你的问题,请参考以下文章
JMH java Mocrobenchmark Harness 方法级精准测量工具 概述