1、什么是JMH
JMH(Java Microbenchmark Harness)是由OpenJDK团队开发的一个用于Java微基准测试工具套件,主要是基于方法层面的基准测试,精度可以达到纳秒级。它提供了一种标准、可靠且可重复的方式来衡量Java代码的性能,包括方法调用、对象创建以及其他类型的 JVM 级别的操作。JMH 通过生成优化过的字节码来确保基准测试不受常见陷阱的影响,如热身不足、垃圾回收干扰、编译器优化等,从而产生更准确的性能指标
2、JMH主要使用场景
3、JMH常用注解
注: 因为我们主要利用JMH提供的注解来进行基准测试,因此我们有必要了解一下JMH一些常用注解
@State: 表明类的所有属性的作用域。只能用于类上。它有如下选项
@BenchmarkMode: 用于指定基准测试的执行模式,如吞吐量、平均执行时间。可用于类或者方法上,它有如下模式
@Measurement: 用于控制压测的次数、时间和批处理数量。可用于类或者方法上,它有如下参数
@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: 模式
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
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