聊聊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 Visual Chart:hhttp://deepoove.com/jmh-visual-chart/
- Visualizer:https://jmh.morethan.io/
总结
本文主要大致讲下如何使用jmh。jmh的详细案例,可以查看官网
https://github.com/openjdk/jmh
或者查看下面博主写的文章
https://cloud.tencent.com/developer/article/1760933
demo链接
https://github.com/lyb-geek/springboot-learning/tree/master/springboot-jmh
参考文档
https://jenkov.com/tutorials/java-performance/jmh.html
https://www.cnblogs.com/wupeixuan/p/13091381.html