手记

聊聊springboot项目如何利用jmh来进行基准测试

前言

1、什么是JMH

JMH(Java Microbenchmark Harness)是由OpenJDK团队开发的一个用于Java微基准测试工具套件,主要是基于方法层面的基准测试,精度可以达到纳秒级。它提供了一种标准、可靠且可重复的方式来衡量Java代码的性能,包括方法调用、对象创建以及其他类型的 JVM 级别的操作。JMH 通过生成优化过的字节码来确保基准测试不受常见陷阱的影响,如热身不足、垃圾回收干扰、编译器优化等,从而产生更准确的性能指标

2、JMH主要使用场景

  • 精确测量方法执行时间: 当你需要准确知道某个特定Java方法或代码段在不同输入、不同环境条件下的执行时间时,可以使用JMH进行基准测试。例如,你可能想比较不同字符串连接方法(如String.concat()与StringBuilder.append())的性能差异。
  • 吞吐量对比: 在评估接口实现或者算法效率时,JMH可以帮助你对比不同实现在相同工作负载下的吞吐量,即单位时间内能够处理的任务数量。
  • 响应时间和分布分析: JMH不仅提供平均执行时间的数据,还可以帮助分析请求完成的时间分布情况,比如你可以了解到多少百分比的请求能在多长时间内完成。
  • 性能优化验证: 在对代码进行性能优化后,使用JMH进行基准测试可以量化改进前后的性能差异,确保优化措施确实提高了程序的运行效率。
  • 并发和并行性能评估: 对于涉及多线程和并发操作的代码块,JMH提供了强大的工具来测量在不同并发级别下系统的性能表现。
  • JVM行为研究: 由于JMH深入到JVM层面进行测试,并且能控制垃圾收集、编译器优化等因素的影响,它对于理解JVM如何影响代码性能以及研究内存分配、垃圾回收策略等具有重要意义。
  • 跨平台可比性: 使用JMH可以在不同的Java版本、不同的操作系统和硬件配置上得到相对可比的基准测试结果,有助于在多种环境下评估代码性能的一致性。

3、JMH常用注解

注: 因为我们主要利用JMH提供的注解来进行基准测试,因此我们有必要了解一下JMH一些常用注解

@State: 表明类的所有属性的作用域。只能用于类上。它有如下选项

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

@BenchmarkMode: 用于指定基准测试的执行模式,如吞吐量、平均执行时间。可用于类或者方法上,它有如下模式

  • Throughput:整体吞吐量,每秒执行了多少次调用,单位为 ops/time
  • AverageTime:用的平均时间,每次操作的平均时间,单位为 time/op
  • SampleTime:随机取样,最后输出取样结果的分布
  • SingleShotTime:只运行一次,往往同时把 Warmup 次数设为 0,用于测试冷启动时的性能
  • All:上面的所有模式都执行一次

@Measurement: 用于控制压测的次数、时间和批处理数量。可用于类或者方法上,它有如下参数

  • iterations:测量的次数
  • time:每次测量持续的时间
  • timeUnit:时间的单位,默认秒
  • batchSize:批处理大小,每次操作调用几次方法

@Warmup: 预热,可用于类或者方法上

由于JVM会使用JIT对热点代码进行编译,因此同一份代码可能由于执行次数的增加而导致执行时间差异太大,因此我们可以让代码先预热几轮,预热时间不算入测量计时。@WarmUp 的使用和 @Measurement 一致。

@Fork: 用于指定fork出多少个子进程来执行同一基准测试方法,可用于类或者方法上。例如@Fork指定数量为2,则 JMH 会 fork 出两个进程来进行测试

@Threads: 用于指定使用多少个线程来执行基准测试方法,可用于类或者方法上。例如@Threads 指定线程数为 2 ,那么每次测量都会创建两个线程来执行基准测试方法

@OutputTimeUnit: 可以指定输出的时间单位,可用于类或者方法注解

@Param: 指定某项参数的多种情况,特别适合用来测试一个函数在不同的参数输入的情况下的性能,只能作用在字段上,使用该注解必须定义 @State 注解。

@Setup: 用于基准测试前的初始化动作,只能用于方法

@TearDown 用于基准测试后执行,主要用于资源的回收,只能用于方法

4、JMH陷阱

常见的比如死码消除。所谓的死码,是指注释的代码,不可达的代码块,可达但不被使用的代码等等。如示例下例子

 @Benchmark
    public void testMethod() {
        int a = 1;
        int b = 2;
        int sum = a + b;
    }

JVM可以检测到分配给sum的a+b的计算从未被使用。因此,JVM可以完全取消a+b的计算。它被认为是死代码。JVM然后可以检测到sum变量从未被使用,并且随后a和b也从未被使用。他们也可以被淘汰。上面的例子最终会被优化成

 @Benchmark
    public void testMethod() {
     }

这样会影响测试结果。JMH提供了如下两种方法来避免死码。一种是将变量当成返回值返回。示例

 @Benchmark
    public int testMethod() {
        int a = 1;
        int b = 2;
        int sum = a + b;

        return sum;
    }

一种是利用Blackhole 的 consume 来避免 JIT 的优化消除。
示例:

import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.infra.Blackhole;

public class MyBenchmark {

    @Benchmark
   public void testMethod(Blackhole blackhole) {
        int a = 1;
        int b = 2;
        int sum = a + b;
        blackhole.consume(sum);
    }
}

其他陷阱还有常量折叠与常量传播、永远不要在测试中写循环、使用 Fork 隔离多个测试方法、方法内联、伪共享与缓存行、分支预测、多线程测试等,感兴趣的朋友可以阅读
https://github.com/lexburner/JMH-samples
了解全部的陷阱。

正文

通过前面的铺垫,大家对jmh应该有个大致的了解,接下来我们就来演示一下springboot项目如何利用jmh进行基准测试

1、springboot的项目中引入JMH GAV

 <properties>
        <jmh.version>1.36</jmh.version>
    </properties>
 <dependency>
            <groupId>org.openjdk.jmh</groupId>
            <artifactId>jmh-core</artifactId>
            <version>${jmh.version}</version>
        </dependency>
        <dependency>
            <groupId>org.openjdk.jmh</groupId>
            <artifactId>jmh-generator-annprocess</artifactId>
            <version>${jmh.version}</version>
            <scope>provided</scope>
        </dependency>

2、编写测试

注: 因为有前面的铺垫介绍,因此下面的例子大家应该比较容易看得懂,就不再论述,直接上代码

@Measurement(iterations = 2, time = 10)
@Warmup(iterations = 2, time = 10)
@Fork(1)
@Threads(value = 2)
@State(Scope.Benchmark)
@BenchmarkMode(Mode.All)
@OutputTimeUnit(TimeUnit.SECONDS)
public class SpringBootJmhTest {

    private ConfigurableApplicationContext context;
    private MockBizService mockBizService;


    /**
     * @Param 允许使用一份基准测试代码跑多组数据,特别适合测量方法性能和参数取值的关系
     */
    @Param({"100","500","1"})
    public long mockBizQueryTime;

    /**
     *   注意要使用 run模式启动main函数,不要使用debug模式启动。
     *  否则会报错:transport error 202: connect failed: Connection refused ERROR
     * @param args
     * @throws RunnerException
     */

    public static void main(String[] args) throws RunnerException {
        String report = DateUtil.today() + "-jmhReport.json";
        Options opt = new OptionsBuilder()
                .include(SpringBootJmhTest.class.getSimpleName())
                // 参数优先级顺序:类 < 方法 < Options
                // 因此如下配置会覆盖@Warmup配置
                .warmupIterations(1)
                .warmupTime(TimeValue.seconds(5))
                //报告输出.可以将结果上传到 https://jmh.morethan.io 或者/http://deepoove.com/jmh-visual
                // 进行分析
                .result(report)
                //报告格式
                .resultFormat(ResultFormatType.JSON).build();


          new Runner(opt).run();

    }


    /**
     * @Setup 用于基准测试前的初始化动作
     *
     * Level参数表明粒度,粒度从粗到细分别是
     *
     * Level.Trial:Benchmark级别
     * Level.Iteration:执行迭代级别
     * Level.Invocation:每次方法调用级别
     */
    @Setup(Level.Trial)
    public void setUp(){
        context = SpringApplication.run(SpringBootJmhApplication.class);
        mockBizService = context.getBean(MockBizService.class);
    }


    /**
     *
     * @Benchmark 来标记需要基准测试的方法.该方法需要为public
     * @param blackhole 的作用是:防止无用代码被JVM优化导致的基准测试结果不准确
     */
    @Benchmark
    public void testMockBizService(Blackhole blackhole) {
        blackhole.consume(mockBizService.query(mockBizQueryTime));

    }


    /**
     * @TearDown 用于基准测试后执行
     */
    @TearDown
    public void tearDown() {
        context.close();
    }

}

3、运行JMH

运行的方式常见有如下几种,一种是直接运行main函数

如示例

 /**
     *   注意要使用 run模式启动main函数,不要使用debug模式启动。
     *  否则会报错:transport error 202: connect failed: Connection refused ERROR
     * @param args
     * @throws RunnerException
     */

    public static void main(String[] args) throws RunnerException {
        String report = DateUtil.today() + "-jmhReport.json";
        Options opt = new OptionsBuilder()
                .include(SpringBootJmhTest.class.getSimpleName())
                // 参数优先级顺序:类 < 方法 < Options
                // 因此如下配置会覆盖@Warmup配置
                .warmupIterations(1)
                .warmupTime(TimeValue.seconds(5))
                //报告输出.可以将结果上传到 https://jmh.morethan.io 或者/http://deepoove.com/jmh-visual
                // 进行分析
                .result(report)
                //报告格式
                .resultFormat(ResultFormatType.JSON).build();


          new Runner(opt).run();

    }

执行main方法,记得需要使用run模式运行,如图

而不是以debug模式,否则会报

transport error 202: connect failed: Connection refused ERROR

另一种是IDE安装JMH插件,以idea为例,在plugins搜索JMH,然后安装插件,安装成功后,可以像执行单元测试那种,单独运行加@Benchmark注解的方法
示例

可以点击圈红的小图标运行,也可以选中加了@Benchmark的方法,右键run运行

还有一种是直接打成jar运行

打成jar包也有如下两种方式。一种在项目的pom引入相应的打包插件

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-shade-plugin</artifactId>
            <version>3.2.1</version>
            <executions>
                <execution>
                    <phase>package</phase>
                    <goals>
                        <goal>shade</goal>
                    </goals>
                    <configuration>
                        <finalName>springboot-jmh</finalName>
                        <transformers>
                            <transformer
                                    implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
                                <resource>META-INF/spring.handlers</resource>
                            </transformer>
                            <transformer
                                    implementation="org.springframework.boot.maven.PropertiesMergingResourceTransformer">
                                <resource>META-INF/spring.factories</resource>
                            </transformer>
                            <transformer
                                    implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
                                <resource>META-INF/spring.schemas</resource>
                            </transformer>
                            <transformer
                                    implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer" />
                            <transformer
                                    implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                <mainClass>com.github.lybgeek.jmh.SpringBootJmhTest</mainClass>
                            </transformer>
                        </transformers>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

这种插件的注意点是main函数直接指定我们要进行基准测试的main函数的类,比如

com.github.lybgeek.jmh.SpringBootJmhTest

其次因为我们springboot运行会依赖一些自动装配,因此我们也需要将相关的配置比如spring.factories装载进去。不然打包的时候可能会报

Cannot find 'resource' in class org.apache.maven.plugins.shade.resource.ManifestResourceTransformer

不过这只是其中一种解法,下边我后讲解另一种解法。

运行如下命令

mvn clean package
java -jar springboot-jmh.jar  -rf json -rff D:/jmhResult.json

其中**-rf:** 为输出的格式为json -rff: 为指定输出的位置

另外一种直接引入官方提供示例插件,该插件也是shade插件,只是此时mainclass为

org.openjdk.jmh.Main
  <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.2.1</version>
                <executions>
                    <execution>
                        <id>shade-my-jar</id>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <finalName>springboot-jmh</finalName>
                            <transformers>
                                <transformer
                                        implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <mainClass>org.openjdk.jmh.Main</mainClass>
                                </transformer>
                            </transformers>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>

这边有个小细节,是因为springboot本身也有依赖shade插件,因此我们自己的shade插件要指定id。如示例配置

 <plugins>
  <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-shade-plugin</artifactId>
    <executions>
      <execution>
        <id>shade-my-jar</id>
...

否则会和springboot默认的插件id冲突,而导致出现

Cannot find 'resource' in class org.apache.maven.plugins.shade.resource.ManifestResourceTransformer

配置完成后打包,并运行如下命令

java -jar springboot-jmh.jar SpringBootJmhTest  -rf json -rff D:/jmhResult.json

注: SpringBootJmhTest 为我们要进行JMH测试的类

以上几种执行方式如何取舍

如果是小测试,直接通过main函数或者jmh插件运行即可。如果是比较大的测试,测试时间比较长,且需要可能需要比较多的资源,可以打成jar测试

4、查看测试结果

Benchmark                                                        (mockBizQueryTime)    Mode  Cnt   Score    Error  Units
SpringBootJmhTest.testMockBizService                                            100   thrpt    2  18.554           ops/s
SpringBootJmhTest.testMockBizService                                            500   thrpt    2   3.935           ops/s
SpringBootJmhTest.testMockBizService                                              1   thrpt    2   1.986           ops/s
SpringBootJmhTest.testMockBizService                                            100    avgt    2   0.108            s/op
SpringBootJmhTest.testMockBizService                                            500    avgt    2   0.509            s/op
SpringBootJmhTest.testMockBizService                                              1    avgt    2   1.005            s/op
SpringBootJmhTest.testMockBizService                                            100  sample  370   0.108 ±  0.001   s/op
SpringBootJmhTest.testMockBizService:testMockBizService·p0.00                   100  sample        0.103            s/op
SpringBootJmhTest.testMockBizService:testMockBizService·p0.50                   100  sample        0.108            s/op
SpringBootJmhTest.testMockBizService:testMockBizService·p0.90                   100  sample        0.109            s/op
SpringBootJmhTest.testMockBizService:testMockBizService·p0.95                   100  sample        0.109            s/op
SpringBootJmhTest.testMockBizService:testMockBizService·p0.99                   100  sample        0.109            s/op
SpringBootJmhTest.testMockBizService:testMockBizService·p0.999                  100  sample        0.109            s/op
SpringBootJmhTest.testMockBizService:testMockBizService·p0.9999                 100  sample        0.109            s/op
SpringBootJmhTest.testMockBizService:testMockBizService·p1.00                   100  sample        0.109            s/op
SpringBootJmhTest.testMockBizService                                            500  sample   78   0.507 ±  0.001   s/op
SpringBootJmhTest.testMockBizService:testMockBizService·p0.00                   500  sample        0.500            s/op
SpringBootJmhTest.testMockBizService:testMockBizService·p0.50                   500  sample        0.508            s/op
SpringBootJmhTest.testMockBizService:testMockBizService·p0.90                   500  sample        0.510            s/op
SpringBootJmhTest.testMockBizService:testMockBizService·p0.95                   500  sample        0.511            s/op
SpringBootJmhTest.testMockBizService:testMockBizService·p0.99                   500  sample        0.513            s/op
SpringBootJmhTest.testMockBizService:testMockBizService·p0.999                  500  sample        0.513            s/op
SpringBootJmhTest.testMockBizService:testMockBizService·p0.9999                 500  sample        0.513            s/op
SpringBootJmhTest.testMockBizService:testMockBizService·p1.00                   500  sample        0.513            s/op
SpringBootJmhTest.testMockBizService                                              1  sample   38   1.005 ±  0.003   s/op
SpringBootJmhTest.testMockBizService:testMockBizService·p0.00                     1  sample        0.999            s/op
SpringBootJmhTest.testMockBizService:testMockBizService·p0.50                     1  sample        1.002            s/op
SpringBootJmhTest.testMockBizService:testMockBizService·p0.90                     1  sample        1.014            s/op
SpringBootJmhTest.testMockBizService:testMockBizService·p0.95                     1  sample        1.014            s/op
SpringBootJmhTest.testMockBizService:testMockBizService·p0.99                     1  sample        1.014            s/op
SpringBootJmhTest.testMockBizService:testMockBizService·p0.999                    1  sample        1.014            s/op
SpringBootJmhTest.testMockBizService:testMockBizService·p0.9999                   1  sample        1.014            s/op
SpringBootJmhTest.testMockBizService:testMockBizService·p1.00                     1  sample        1.014            s/op
SpringBootJmhTest.testMockBizService                                            100      ss    2   0.110            s/op
SpringBootJmhTest.testMockBizService                                            500      ss    2   0.508            s/op
SpringBootJmhTest.testMockBizService                                              1      ss    2   1.010            s/op

报告的参数解读如下

Mode: 模式

  • thrpt:吞吐量
  • avgt:每次请求的平均耗时
  • sample:请求样本数量,这次压测一共发了多少个请求
  • ss:除去冷启动,一共执行了多少轮

Cnt: 基准测试执行的迭代次数或者样本数量

Score: 是性能测试结果的主要度量单位。它代表了基准测试方法的吞吐量或者执行速度,具体含义取决于你选择的@BenchmarkMode。

例如你设置了 @BenchmarkMode(Mode.Throughput),那么 Score 将表示每秒可以执行该操作的次数(ops/s),即吞吐量。 - 若设置为 @BenchmarkMode(Mode.AverageTime),则 Score 表示的是平均每个操作所需的时间(如ns/op、ms/op等),数值越小通常意味着性能越好

Errors: 通常指的是执行过程中统计性能指标时的误差范围。由于JMH基于统计学原理进行性能测量,因此其结果会受到随机性和系统噪声的影响

Units: 通常指的是度量基准测试结果时使用的单位。根据你选择的@BenchmarkMode不同,报告中的单位也会有所变化

5、jmh测试结果可视化

我们可以将生成jmh的json结果上传到如下网站,进行可视化分析

总结

本文主要大致讲下如何使用jmh。jmh的详细案例,可以查看官网
https://github.com/openjdk/jmh
或者查看下面博主写的文章
https://cloud.tencent.com/developer/article/1760933

demo链接

参考文档

0人推荐
随时随地看视频
慕课网APP