SpringBoot集成jmh进行进行基准性能测试

1.什么是jmh?

JMH是Java Microbenchmark Harness的简称,一个针对Java做基准测试的工具,是由开发JVM的那群人开发的。想准确的对一段代码做基准性能测试并不容易,因为JVM层面在编译期、运行时对代码做很多优化,但是当代码块处于整个系统中运行时这些优化并不一定会生效,从而产生错误的基准测试结果,而这个问题就是JMH要解决的。

Benchmark基本概念

Benchmark State

有时候我们在做基准测试的时候会需要使用一些变量、字段,@State注解是用来配置这些变量的生命周期,@State注解可以放在类上,然后在基准测试方法中可以通过参数的方式把该类对象作为参数使用。@State支持的生命周期类型:

  • Benchmark: 整个基准测试的生命周期,多个线程共用同一份实例对象。该类内部的@Setup @TearDown注解的方法可能会被任一个线程执行,但是只会执行一次。
  • Group: 每一个Group内部共享同一个实例,需要配合@Group @GroupThread使用。该类内部的@Setup @TearDown注解的方法可能会该Group内的任一个线程执行,但是只会执行一次。
  • **Thread:**每个线程的实例都是不同的、唯一的。该类内部的@Setup @TearDown注解的方法只会被当前线程执行,而且只会执行一次。

被@State标示的类必须满足如下两个要求:

  • 类必须是public的
  • 必须有无参构造函数

State Object @Setup @TearDown

在@Scope注解标示的类的方法上可以添加@Setup和@TearDwon注解。@Setup:用来标示在Benchmark方法使用State对象之前需要执行的操作。@TearDown:用来标示在Benchmark方法之后需要对State对象执行的操作。 @Setup、@TearDown支持设置Level级别,Level有三个值:

  • Trial: 每次benchmark前/后执行一次,每次benchmark会包含多轮(Iteration)
  • Iteration: 每轮执行前/后执行一次
  • Invocation: 每次调用测试的方法前/后都执行一次,这个执行频率会很高,一般用不上。

Fork

@Fork注解用来设置启动的JVM进程数量,多个进程是串行的方式启动的,多个进程可以减少偶发因素对测试结果的影响。

Thread

@Thread用来配置执行测试启动的线程数量

Warmup

@Warmup 用来配置预热的时间,如下所示配置预热五轮,每轮1second,也就是说总共会预热5s左右,在这5s内会不停的循环调用测试方法,但是预热时的数据不作为测试结果参考。

2.代码工程

实验目的

  1. 测试排序的吞吐量
  2. 测试字符串连接的的平均耗时

pom.xml

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>springboot-demo</artifactId>
        <groupId>com.et</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>JMH</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-autoconfigure</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.openjdk.jmh</groupId>
            <artifactId>jmh-core</artifactId>
            <version>1.33</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.openjdk.jmh</groupId>
            <artifactId>jmh-generator-annprocess</artifactId>
            <version>1.33</version>
            <scope>test</scope>
        </dependency>

    </dependencies>
</project>

测试排序的吞吐量

java 复制代码
package com.et.jmh;

import org.junit.Test;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;

@State(Scope.Thread)
public class MyBenchmark {
    private List<String> list;

    @Setup
    public void setup() {
        list = new ArrayList<>();
        for (int i = 0; i < 10000; i++) {
            list.add(UUID.randomUUID().toString());
        }
    }

    @TearDown
    public void tearDown() {
        list = null;
    }

    @Benchmark
    public void testSort() {
        Collections.sort(list);
    }
   @Test
   public void testMyBenchmark() throws Exception {
      Options options = new OptionsBuilder()
            .include(MyBenchmark.class.getSimpleName())
            .forks(1)
            .threads(1)
            .warmupIterations(5)
            .measurementIterations(5)
            .mode(Mode.Throughput)
            .build();

      new Runner(options).run();
   }
}

启动运行

yaml 复制代码
# Run progress: 0.00% complete, ETA 00:01:40
# Fork: 1 of 1
# Warmup Iteration 1: 16881.851 ops/s
# Warmup Iteration 2: 17844.093 ops/s
# Warmup Iteration 3: 17076.747 ops/s
# Warmup Iteration 4: 16877.725 ops/s
# Warmup Iteration 5: 16480.613 ops/s
Iteration 1: 16697.580 ops/s
Iteration 2: 16620.370 ops/s
Iteration 3: 15644.794 ops/s
Iteration 4: 16373.904 ops/s
Iteration 5: 16518.710 ops/s


Result "com.et.jmh.MyBenchmark.testSort":
 16371.072 ±(99.9%) 1631.467 ops/s [Average]
 (min, avg, max) = (15644.794, 16371.072, 16697.580), stdev = 423.687
 CI (99.9%): [14739.605, 18002.538] (assumes normal distribution)


# Run complete. Total time: 00:01:45

REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
experiments, perform baseline and negative tests that provide experimental control, make sure
the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
Do not assume the numbers tell you what you want them to tell.

Benchmark Mode Cnt Score Error Units
MyBenchmark.testSort thrpt 5 16371.072 ± 1631.467 ops/s

从上面的日志我们大致可以了解到 JMH的基准测试主要经历了下面几个过程:

  1. 打印本次测试的配置,warmup:5轮;measurement:5轮;每轮:10s;启动1个线程做测试;基准测试指标:吞吐量(throughput,单位是s);测试方法MyBenchmark.testSort
  2. 启动一个JVM进程做基准测试(也可以设置启动多个进程,减少随机因素的误差影响)
  3. 在JVM进程中先执行了5轮的预热(warmup),每轮10s,总共50s的预热时间。预热的数据不作为基准测试的参考。
  4. 测试了5轮,每轮10s,总共50s的测试时间
  5. 汇总测试数据、生成结果报表。最终结论是吞吐量(16371.072 ± 1631.467ops/s),其中16371.072 是结果,1631.467是误差范围。

字符串连接的的平均耗时

JMH benchmark支持如下几种测试模式:

  • Throughput: 吞吐量,测试每秒可以执行操作的次数
  • Average Time: 平均耗时,测试单次操作的平均耗时
  • **Sample Time:**采样耗时,测试单次操作的耗时,包括最大、最小耗时,已经百分位耗时等
  • Single Shot Time: 只计算一次的耗时,一般用来测试冷启动的性能(不设置JVM预热)
  • All: 测试上面的所有指标

默认的benchmark mode是Throughput,可以通过注解的方式设置BenchmarkMode,注解支持放在类或方法上。如下所示设置了Throughput和SampleTime两个Benchmark mode。

typescript 复制代码
package com.et.jmh;

import org.junit.Test;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

@State(Scope.Thread)
public class StringConcatBenchmark {
    private String str1;
    private String str2;

    @Setup
    public void setup() {
        str1 = "Hello";
        str2 = "World";
    }

    @TearDown
    public void tearDown() {
        str1 = null;
        str2 = null;
    }

    @Benchmark
    public String testStringConcat() {
        return str1 + " " + str2;
    }
   @Test
   public void testStringConcatBenchmark() throws Exception {
      Options options = new OptionsBuilder()
            .include(StringConcatBenchmark.class.getSimpleName())
            .forks(1)
            .threads(1)
            .warmupIterations(5)
            .measurementIterations(5)
            .mode(Mode.AverageTime)
            .build();

      new Runner(options).run();
   }
}

启动测试

bash 复制代码
# Run progress: 0.00% complete, ETA 00:01:40
# Fork: 1 of 1
# Warmup Iteration 1: ≈ 10⁻⁸ s/op
# Warmup Iteration 2: ≈ 10⁻⁸ s/op
# Warmup Iteration 3: ≈ 10⁻⁸ s/op
# Warmup Iteration 4: ≈ 10⁻⁸ s/op
# Warmup Iteration 5: ≈ 10⁻⁸ s/op
Iteration 1: ≈ 10⁻⁸ s/op
Iteration 2: ≈ 10⁻⁸ s/op
Iteration 3: ≈ 10⁻⁸ s/op
Iteration 4: ≈ 10⁻⁸ s/op
Iteration 5: ≈ 10⁻⁸ s/op


Result "com.et.jmh.StringConcatBenchmark.testStringConcat":
 ≈ 10⁻⁸ s/op


# Run complete. Total time: 00:01:45

REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
experiments, perform baseline and negative tests that provide experimental control, make sure
the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
Do not assume the numbers tell you what you want them to tell.

Benchmark Mode Cnt Score Error Units
StringConcatBenchmark.testStringConcat avgt 5 ≈ 10⁻⁸ s/op

以上只是一些关键代码,所有代码请参见下面代码仓库

代码仓库

3.引用

相关推荐
儿时可乖了5 分钟前
使用 Java 操作 SQLite 数据库
java·数据库·sqlite
ruleslol6 分钟前
java基础概念37:正则表达式2-爬虫
java
xmh-sxh-131423 分钟前
jdk各个版本介绍
java
XINGTECODE37 分钟前
海盗王集成网关和商城服务端功能golang版
开发语言·后端·golang
天天扭码42 分钟前
五天SpringCloud计划——DAY2之单体架构和微服务架构的选择和转换原则
java·spring cloud·微服务·架构
程序猿进阶43 分钟前
堆外内存泄露排查经历
java·jvm·后端·面试·性能优化·oom·内存泄露
FIN技术铺1 小时前
Spring Boot框架Starter组件整理
java·spring boot·后端
小曲程序1 小时前
vue3 封装request请求
java·前端·typescript·vue
凡人的AI工具箱1 小时前
15分钟学 Go 第 60 天 :综合项目展示 - 构建微服务电商平台(完整示例25000字)
开发语言·后端·微服务·架构·golang
陈王卜1 小时前
django+boostrap实现发布博客权限控制
java·前端·django