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)比较两个方法的性能(例如比较序列化中的fastjsonjackson,字符串拼接过程中的+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提供的一些initdestory方法一样,JHM也提供有这样的钩子:

@Setup 必须标示在@State注解的类内部,表示初始化操作

@TearDown 必须表示在@State注解的类内部,表示销毁操作

@Setup@TearDown提供了以下三种纬度的控制:

参数说明
Level.Trial只会在个基础测试的前后执行。包括Warmup和Measurement阶段,一共只会执行一次。
Level.Iteration每次执行记住测试方法的时候都会执行,如果WarmupMeasurement都配置了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)resultresultFormat

生成文件的格式,这里通常将两个配置一起使用,用于生成一个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 方法级精准测量工具 概述

Java微基准测试工具JMH

Java微基准测试工具JMH

Java优化实战「微基准系列」带你脚踏实地的进行开发和使用JMH测试和提升应用程序和服务指南

Java基准测试工具JMH使用

Java基准测试工具JMH使用