JMH 使用测试(附 测试案例)

JMH 是 Java Microbenchmark Harness 的缩写。中文意思大致是 “JAVA 微基准测试套件”
是专门用于代码微基准测试的工具套件。何谓Micro Benchmark呢?简单的来说就是基于方法层面的基准测试,精度可以达到微秒级。当你定位到热点方法,希望进一步优化方法性能的时候,就可以使用JMH对优化的结果进行量化的分析。

JMH比较典型的应用场景有:

想准确的知道某个方法需要执行多长时间,以及执行时间和输入之间的相关性;
对比接口不同实现在给定条件下的吞吐量,找到最优实现
查看多少百分比的请求在多长时间内完成

依赖

<dependencies>
    <dependency>
        <groupId>org.openjdk.jmh</groupId>
        <artifactId>jmh-core</artifactId>
        <version>1.20</version>
    </dependency>
    <dependency>
        <groupId>org.openjdk.jmh</groupId>
        <artifactId>jmh-generator-annprocess</artifactId>
        <version>1.20</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

插件

<build>
    <plugins>
        <plugin>
            <groupId>org.codehaus.mojo</groupId>
            <artifactId>exec-maven-plugin</artifactId>
            <executions>
                <execution>
                    <id>run-benchmarks</id>
                    <phase>integration-test</phase>
                    <goals>
                        <goal>exec</goal>
                    </goals>
                    <configuration>
                        <classpathScope>test</classpathScope>
                        <executable>java</executable>
                        <arguments>
                            <argument>-classpath</argument>
                            <classpath />
                            <argument>org.openjdk.jmh.Main</argument>
                            <argument>.*</argument>
                        </arguments>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

测试实例


import org.openjdk.jmh.annotations.*;
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.concurrent.TimeUnit;

@BenchmarkMode(Mode.Throughput)     //使用模式 默认是Mode.Throughput,
@Warmup(iterations = 3)     //配置预热次数,默认是每次运行1秒,运行10次,这里我们设置为3次 为什么要预热 下面会解释
//本例是一次运行5秒,总共运行3次
// 在性能对比时候,采用默认1秒即可,
// 如果我们用jvisualvm做性能监控,我们可以指定一个较长时间运行。
@Measurement(iterations = 3, time = 5, timeUnit = TimeUnit.SECONDS)
@Threads(1) // 配置同时起多少个线程执行
@Fork(1)    //代表启动多个单独的进程分别测试每个方法,我们这里指定为每个方法启动一个进程
@OutputTimeUnit(TimeUnit.SECONDS)   //OutputTimeUnit 统计结果的时间单元,这个例子TimeUnit.SECONDS
public class FirstJMHTestDemo {

    @Benchmark
    public void testStringAdd() {
        String a = "";
        for (int i = 0; i < 1000; i++) {
            a += i;
        }
        print(a);
    }

    @Benchmark
    public void testStringBuilderAdd() {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 1000; i++) {
            sb.append(i);
        }
        print(sb.toString());
    }

    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(FirstJMHTestDemo.class.getSimpleName())
                .build();
        new Runner(opt).run();
    }
    public void print(String a) {

    }

}

JMH 使用测试(附 测试案例)_第1张图片

基本概念以及常用注解:

@BenchmarkMode

JMH 使用测试(附 测试案例)_第2张图片
Throughput: 整体吞吐量,例如“1秒内可以执行多少次调用”。
AverageTime: 调用的平均时间,例如“每次调用平均耗时xxx毫秒”。
SampleTime: 随机取样,最后输出取样结果的分布,例如“99%的调用在xxx毫秒以内,99.99%的调用在xxx毫秒以内”
SingleShotTime: 以上模式都是默认一次 iteration 是 1s,唯有 SingleShotTime 是只运行一次。往往同时把 warmup 次数设为0,用于测试冷启动时的性能。
All表示统计前面的所有指标

@Warmup

配置预热次数,默认是每次运行1秒,运行10次,我们的例子是运行3次,
为什么要预热?
因为 JVM 的 JIT (Just in time)机制的存在,如果某个函数被调用多次之后,JVM 会尝试将其编译成为机器码从而提高执行速度。为了让 benchmark 的结果更加接近真实情况就需要进行预热。

@Measurement

度量,其实就是一些基本的测试参数。
iterations :进行测试的轮次
time :每轮进行的时长
timeUnit : 时长单位
都是一些基本的参数,可以根据具体情况调整。一般比较重的东西可以进行大量的测试,放到服务器上运行。
本例是一次运行5秒,总共运行3次。在性能对比时候,采用默认1秒即可,如果我们用jvisualvm做性能监控,我们可以指定一个较长时间运行。

@Threads

每个进程中同时起多少个线程执行,这个非常好理解,默认值是Runtime.getRuntime().availableProcessors(),根据具体情况选择,一般为cpu乘以2。
本例启动1个线程同时执行

@Fork

代表启动多个单独的进程分别测试每个方法,我们这里指定为每个方法启动一个进程。

JVM因为使用了profile-guided optimization而“臭名昭著”,这对于微基准测试来说十分不友好,因为不同测试方法的profile混杂在一起,“互相伤害”彼此的测试结果。对于每个@Benchmark方法使用一个独立的进程可以解决这个问题,这也是JMH的默认选项。注意不要设置为0,设置为n则会启动n个进程执行测试(似乎也没有太大意义)。fork选项也可以通过方法注解以及启动参数来设置。

@OutputTimeUnit

统计结果的时间单元,这个例子TimeUnit.SECONDS,我们在运行后会看到输出结果是统计每秒的吞吐量

@Benchmark

方法级注解,表示该方法是需要进行 benchmark 的对象,用法和 JUnit 的 @Test 类似。

@State

当使用@Setup参数的时候,必须在类上加这个参数,不然会提示无法运行。

State 用于声明某个类是一个“状态”,然后接受一个 Scope 参数用来表示该状态的共享范围。 因为很多 benchmark 会需要一些表示状态的类,JMH 允许你把这些类以依赖注入的方式注入到 benchmark 函数里。Scope 主要分为三种。

Thread: 该状态为每个线程独享。
Group: 该状态为同一个组里面所有线程共享。
Benchmark: 该状态在所有线程间共享。

@Param

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

@Setup

方法级注解,这个注解的作用就是我们需要在测试之前进行一些准备工作,比如对一些数据的初始化之类的

@TearDown

方法级注解,这个注解的作用就是我们需要在测试之后进行一些结束工作,比如关闭线程池,数据库连接等的,主要用于资源的回收等。

@Group

方法注解,可以把多个 benchmark 定义为同一个 group,则它们会被同时执行,譬如用来模拟生产者-消费者读写速度不一致情况下的表现。可以参考如下例子

@Level

用于控制 @Setup,@TearDown 的调用时机,默认是 Level.Trial。
Trial:每个benchmark方法前后;
Iteration:每个benchmark方法每次迭代前后;
Invocation:每个benchmark方法每次调用前后,谨慎使用,需留意javadoc注释;

第二个测试 :


import org.openjdk.jmh.annotations.*;
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.concurrent.TimeUnit;

@BenchmarkMode(Mode.AverageTime)     //使用模式 默认是Mode.Throughput,
@Warmup(iterations = 3)     //配置预热次数,默认是每次运行1秒,运行10次,这里我们设置为3次 为什么要预热 下面会解释
//本例是一次运行2秒,总共运行3次
// 在性能对比时候,采用默认1秒即可,
// 如果我们用jvisualvm做性能监控,我们可以指定一个较长时间运行。
@Measurement(iterations = 3, time = 2, timeUnit = TimeUnit.SECONDS)
@Fork(1)    //代表启动多个单独的进程分别测试每个方法,我们这里指定为每个方法启动一个进程
@OutputTimeUnit(TimeUnit.MICROSECONDS)   //OutputTimeUnit 统计结果的时间单元,这个例子TimeUnit.MICROSECONDS 微秒
@State(Scope.Benchmark)
public class TwoJMHTestDemo {
    @Param({"100", "1000", "10000"})
    public int len;

    @Benchmark
    @Threads(1) // 配置同时起多少个线程执行
    public void testStringAdd() {
        String a = "";
        for (int i = 0; i < len; i++) {
            a += i;
        }
        print(a);
    }

    @Benchmark
    @Threads(1) // 配置同时起多少个线程执行
    public void testStringBuilderAdd() {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < len; i++) {
            sb.append(i);
        }
        print(sb.toString());
    }

    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(TwoJMHTestDemo.class.getSimpleName())
                .build();
        new Runner(opt).run();
    }
    public void print(String a) {

    }

}

结果
JMH 使用测试(附 测试案例)_第3张图片

人生,看似很长,切实很短。每天提高一小点,坚持下来,你会发现你已经跨了一大步。相信自己,所有皆有可能!

Life seems to be long and really short. Improve a little bit every day and stick to it, and you will find that you have taken a big step. Believe in yourself, everything is possible!

你可能感兴趣的:(工具)