Java中使用JMH(Java Microbenchmark Harness 微基准测试框架)进行性能测试和优化

场景

Jmeter进行http接口压力测试:

https://www.cnblogs.com/badaoliumangqizhi/p/16301432.html

JMH

JMH,全称Java Microbenchmark Harness (微基准测试框架),是专门用于Java代码微基准测试的一套测试工具API,

是由Java虚拟机团队开发的的,一般用于代码的性能调优。

MicroBenchmark就是在method层面上的benchmark,精度可以精确到微秒级、甚至可以达到纳秒级别,

适用于 java 以及其他基于 JVM 的语言。与Apache JMeter 不同,JMH 测试的对象可以是任一方法,颗粒度更小,

而不仅限于接口以及API层面。

JMH比较典型的应用场景

想要知道某个函数需要执行多长时间,以及执行时间和输入之间的相关性

想要对比接口不同实现在给定条件下的吞吐量大小

想要知道百分之N的请求在多长时间内完成

想要找出了热点函数,需要对热点函数进行进一步优化时

针对于函数的多种实现方式(例如JSON序列化/反序列化有Jackson和Gson实现),不知道哪种实现性能更好

JMH官方仓库:

GitHub - openjdk/jmh: https://openjdk.org/projects/code-tools/jmh

JMH官网示例demo:

code-tools/jmh: 2be2df7dbaf8 jmh-samples/src/main/java/org/openjdk/jmh/samples/JMHSample_01_HelloWorld.java

注:

博客:
霸道流氓气质的博客_CSDN博客-C#,架构之路,SpringBoot领域博主

实现

1、首先如果jdk小于1.9,则需要添加依赖

        <dependency>
            <groupId>org.openjdk.jmh</groupId>
            <artifactId>jmh-core</artifactId>
            <version>1.23</version>
        </dependency>
        <dependency>
            <groupId>org.openjdk.jmh</groupId>
            <artifactId>jmh-generator-annprocess</artifactId>
            <version>1.23</version>
        </dependency>

2、参考上面官网提供helloworld的demo

package org.openjdk.jmh.samples;

import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

public class JMHSample_01_HelloWorld {

    @Benchmark
    public void wellHelloThere() {
      
    }

    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();
    }

}

可知需要新建类,并在main方法中这样写去启动基准测试和执行测试。

然后需要进行测试的方法添加@Benchmark注解。

3、按照上面官网提供的示例,对比ArrayList与LinkedList在头部进行添加时的性能测试添加一些细节

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.concurrent.TimeUnit;

//测试完成时间
@BenchmarkMode(Mode.AverageTime)
//设置统计结果的时间单位
@OutputTimeUnit(TimeUnit.NANOSECONDS)
//预热所需要配置的一些基本测试参数
@Warmup(iterations = 2,time = 1,timeUnit = TimeUnit.SECONDS)
//测试次数和时间
@Measurement(iterations = 5,time = 5,timeUnit = TimeUnit.SECONDS)
//fork一个线程
@Fork(1)
//通过 State 可以指定一个对象的作用范围
@State(Scope.Thread)
public class ArrayListWithLinkedListTestHeaderAdd {

    private static final int maxSize = 10000; //测试循环次数
    private static final int operationSize = 100; //操作次数

    private static ArrayList<Integer> arrayList;
    private static LinkedList<Integer> linkedList;

    public static void main(String[] args) throws RunnerException {
        //启动基准测试
        Options opt = new OptionsBuilder()
                .include(ArrayListWithLinkedListTestHeaderAdd.class.getSimpleName())  //要导入的测试类
                .build();
        //执行测试
        new Runner(opt).run();
    }

    //@Setup作用于方法上,用于测试前的初始化工作
    @Setup
    public void init(){
        arrayList = new ArrayList<Integer>();
        linkedList = new LinkedList<Integer>();
        for (int i = 0; i < maxSize; i++) {
            arrayList.add(i);
            linkedList.add(i);
        }
    }

    //用于回收某些资源
    @TearDown
    public void finish(){
       
    }

    @Benchmark
    public void addArrayByFirst(Blackhole blackhole){
        for (int i = 0; i < operationSize; i++) {
            arrayList.add(i,i);
        }
        //为了避免JIT忽略未被使用的结果计算/为了避免死码消除问题
        blackhole.consume(arrayList);
    }

    @Benchmark
    public void addLinkedByFirst(Blackhole blackhole){
        for (int i = 0; i < operationSize; i++) {
            linkedList.add(i,i);
        }
        //为了避免JIT忽略未被使用的结果计算/为了避免死码消除问题
        blackhole.consume(linkedList);
    }
}

这里加了一些注解,并配置了一些参数。

4、JMH部分注解说明

@Benchmark

需要测试的方法,添加该注解。

@BenchmarkMode

用来配置 Mode 选项,可用于类或者方法上,这个注解的 value 是一个数组,可以把几种 Mode 集合在一起执行,如:

@BenchmarkMode({Mode.SampleTime, Mode.AverageTime}),还可以设置为 Mode.All,即全部执行一遍。

Throughput:整体吞吐量,每秒执行了多少次调用,单位为 ops/time

AverageTime:用的平均时间,每次操作的平均时间,单位为 time/op

SampleTime:随机取样,最后输出取样结果的分布

SingleShotTime:只运行一次,往往同时把 Warmup 次数设为 0,用于测试冷启动时的性能

All:上面的所有模式都执行一次

@State

通过 State 可以指定一个对象的作用范围,JMH 根据 scope 来进行实例化和共享操作。

@State 可以被继承使用,如果父类定义了该注解,子类则无需定义。

由于 JMH 允许多线程同时执行测试,不同的选项含义如下:

Scope.Benchmark:所有测试线程共享一个实例,测试有状态实例在多线程共享下的性能

Scope.Group:同一个线程在同一个 group 里共享实例

Scope.Thread:默认的 State,每个测试线程分配一个实例

@OutputTimeUnit

为统计结果的时间单位,可用于类或者方法注解

@Warmup

预热所需要配置的一些基本测试参数,可用于类或者方法上。一般前几次进行程序测试的时候都会比较慢,

所以要让程序进行几轮预热,保证测试的准确性。参数如下所示:

iterations:预热的次数

time:每次预热的时间

timeUnit:时间的单位,默认秒

batchSize:批处理大小,每次操作调用几次方法

为什么需要预热?

因为 JVM 的 JIT 机制的存在,如果某个函数被调用多次之后,JVM 会尝试将其编译为机器码,从而提高执行速度,

所以为了让 benchmark 的结果更加接近真实情况就需要进行预热。

@Measurement

实际调用方法所需要配置的一些基本测试参数,可用于类或者方法上,参数和 @Warmup 相同。

@Threads

每个进程中的测试线程,可用于类或者方法上。

@Fork

进行 fork 的次数,可用于类或者方法上。如果 fork 数是 2 的话,则 JMH 会 fork 出两个进程来进行测试。

@Param

指定某项参数的多种情况,特别适合用来测试一个函数在不同的参数输入的情况下的性能,只能作用在字段上,

使用该注解必须定义@State 注解。

@Setup

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

@TearDown

方法注解,与@Setup 相对的,会在所有 benchmark 执行结束以后执行,主要用于资源的回收等

5、运行上面的main方法,对比测试两个方法的性能

猜你喜欢

转载自blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/131723751