大家好!今天我要向大家详细介绍JMH(Java Microbenchmark Harness),这个被誉为Java性能测试的利器。无论你是想优化现有的Java代码还是开发新的项目,JMH都能够帮助你准确、可靠地测量和分析代码的性能,让我们一起来探索JMH的神奇之处吧!
一、JMH 简 介
JMH是由OpenJDK团队开发的一款专业的基准测试工具,旨在提供一个可靠的测试框架,帮助Java开发者进行代码性能的评估和优化。
JMH能够对代码进行微基准测试,以提供精确的性能数据,并帮助开发者发现潜在的性能问题。
二、为 什 么 使 用 JMH
-
高度可靠:JMH采用严格的度量方式,能够排除外部干扰因素的影响,提供准确的性能数据。
-
直观可视化:JMH提供丰富的测试结果图表和报告生成工具,让性能数据一目了然,方便开发者分析和优化。
-
灵活的配置选项:JMH提供多种注解和选项,可以根据需求进行灵活配置,满足不同类型的性能测试需求。
-
自动优化:JMH在测试执行过程中会使用Just-In-Time(JIT)编译器进行代码优化,确保测试结果准确无误。
三、JMH 的 使 用 步 骤
-
引入JMH依赖:在项目的构建工具中,引入JMH的依赖,例如Maven或Gradle等。【jdk1.8以下,,包括1.8, 1.9+自带】
<dependencies>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.23</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.23</version>
<scope>provided</scope>
</dependency>
</dependencies>
2. 编写待测试的方法:使用@Benchmark注解标记待测试的方法,确保方法的可重复执行性。
public class MyBenchmark {
@Benchmark
public void myMethod() {
// 待测试的代码逻辑
}
}
-
配置测试选项:使用各种注解来配置基准测试的参数,例如@BenchmarkMode、@Warmup、@Measurement等。
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 3, time = 1)
@Measurement(iterations = 5, time = 1)
@Fork(2)
public class MyBenchmark {
@Benchmark
public void myMethod() {
// 待测试的代码逻辑
}
}
-
运行基准测试:编写入口方法,通过mian方法来运行基准测试,生成测试结果。
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 3, time = 1)
@Measurement(iterations = 5, time = 1)
@Fork(2)
public class MyBenchmark {
@Benchmark
public void myMethod() {
// 待测试的代码逻辑
}
@Benchmark
public void myMethod1() {
// 待测试的代码逻辑
}
public static void main(String[] args) throws RunnerException {
Options options = new OptionsBuilder()
.include(MyBenchmark.class.getSimpleName())
.forks(1)
.build();
new Runner(options).run();
}
}
四、常 用 JMH 注 解 和 选 项
-
@Benchmark:标记待测试的方法,JMH会自动执行和测量该方法。
-
@BenchmarkMode:设置基准测试的模式,默认为Mode.Throughput,还有Mode.AverageTime、Mode.SampleTime等模式可选。
-
@Warmup:设置预热的迭代次数和时间,用于避免测试开始时的JIT编译影响。
-
@Measurement:设置测量的迭代次数和时间,用于最终的性能测量。
-
@Fork:设置进行测试的进程数,可以通过多次运行取得平均结果。
-
@State:定义测试状态类,可以在不同的基准测试方法之间共享状态。
-
@Setup:注解可以用于初始化操作,我们可以在基准测试执行前进行一些准备工作。
-
@TearDown: 注解可以用于清理操作,我们可以在基准测试执行后对资源进行释放。
-
@Param:用于参数化测试,给定一组参数来运行相同的测试方法。
@State(Scope.Benchmark)
@BenchmarkMode(Mode.Throughput)
@Warmup(iterations = 3, time = 1)
@Measurement(iterations = 5, time = 1)
@Fork(2)
public class MyBenchmark {
@Param({"value1", "value2", "value3"})
public String param;
@Benchmark
public void myMethod() {
// 待测试的代码逻辑
}
@Setup
public void setup() {
// 进行基准测试前的初始化操作
}
@TearDown
public void teardown() {
// 进行基准测试后的清理操作
}
}
上述代码中,我们使用了@State
注解来定义了一个测试状态类,以便在不同的基准测试方法间共享状态。
@BenchmarkMode
设置了基准测试的模式为吞吐量模式。
@Warmup
注解表示预热阶段的迭代次数为3次,每次迭代1秒。
@Measurement
注解表示正式性能测试的迭代次数为5次,每次迭代1秒。
@Fork
注解表示执行两次测试进程。
在类中,我们定义了一个被@Benchmark
注解标记的方法myMethod()
,这是我们待测试的方法。
使用@Param
注解对param
参数进行了参数化测试。
@Setup
注解表示在执行基准测试之前进行的初始化操作,
@TearDown
注解表示在执行基准测试之后进行的清理操作。
四、案例:比较String 跟 StringBulider 拼接字符串执行效率
/**
* @author: xrp
* @date: 2023/10/09/14:11
* @description
*/
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Warmup(iterations = 4, time = 2)
@Measurement(iterations = 4, time = 5)
@State(Scope.Thread)
@Slf4j
public class JmhTest {
ServerInfo serverInfo;
ServerVO finalServerVO;
EsLoginMonitorRecord esLoginMonitorRecord;
@Setup
public void setApplicationContext() {
serverInfo = new ServerInfo();
serverInfo.setServerId(1);
serverInfo.setHostName("123");
serverInfo.setMacCode("123");
finalServerVO = new ServerVO();
finalServerVO.setGroupStr("1");
finalServerVO.setTagName("123");
esLoginMonitorRecord = new EsLoginMonitorRecord();
esLoginMonitorRecord.setUserName("123");
esLoginMonitorRecord.setType("1232");
esLoginMonitorRecord.setStatus("123");
esLoginMonitorRecord.setStatus("123");
esLoginMonitorRecord.setLoginIp("123");
esLoginMonitorRecord.setLoginTime("122");
esLoginMonitorRecord.setCountry("123");
esLoginMonitorRecord.setProvince("1236");
esLoginMonitorRecord.setCity("1568");
}
@Benchmark
public void testString() {
String result = "";
for (int i = 0; i < 2000 ; i++) {
result += "serverId/"+serverInfo.getServerId()
+",serverIp/"+serverInfo.getServerIp()
+",hostName/"+serverInfo.getHostName()
+",macCode/"+serverInfo.getMacCode()
+",groupName/"+StringUtils.defaultString(finalServerVO.getGroupStr())
+",tagName/"+StringUtils.defaultString(finalServerVO.getTagName())
+",hostRemark/"+StringUtils.defaultString(serverInfo.getRemark())
+",userName/"+esLoginMonitorRecord.getUserName()
+",type/"+esLoginMonitorRecord.getType()
+",status/"+esLoginMonitorRecord.getStatus()
+",loginIp/"+esLoginMonitorRecord.getLoginIp()
+",loginTime/"+esLoginMonitorRecord.getLoginTime()
+",country/"+esLoginMonitorRecord.getCountry()
+",province/"+esLoginMonitorRecord.getProvince()
+",city/"+esLoginMonitorRecord.getCity();
}
}
@Benchmark
public void testStringBuilder() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 2000 ; i++) {
sb.append("serverId/").append(serverInfo.getServerId())
.append(",serverIp/").append(serverInfo.getServerIp()).append("(").append(serverInfo.getServerLocalIp()).append(")")
.append(",hostName/").append(serverInfo.getHostName())
.append(",macCode/").append(serverInfo.getMacCode())
.append(",groupName/").append(StringUtils.defaultString(finalServerVO.getGroupStr()))
.append(",tagName/").append(StringUtils.defaultString(finalServerVO.getTagName()))
.append(",hostRemark/").append(StringUtils.defaultString(serverInfo.getRemark()))
.append(",userName/").append(esLoginMonitorRecord.getUserName())
.append(",type/").append(esLoginMonitorRecord.getType())
.append(",status/").append(esLoginMonitorRecord.getStatus())
.append(",loginIp/").append(esLoginMonitorRecord.getLoginIp())
.append(",loginTime/").append(esLoginMonitorRecord.getLoginTime())
.append(",country/").append(esLoginMonitorRecord.getCountry())
.append(",province/").append(esLoginMonitorRecord.getProvince())
.append(",city/").append(esLoginMonitorRecord.getCity());
}
}
public static void main(String[] args) throws RunnerException {
Options optionsBuilder = new OptionsBuilder()
.include(JmhTest.class.getSimpleName())
.result("L:\\1.json")
.resultFormat(ResultFormatType.JSON)
.forks(1)
.build();
new Runner(optionsBuilder).run();
}
}
在案例中可以看到先用了 @Setup 注解初始化了一些数据 提供测试类使用 @BenchmarkMode(Mode.AverageTime) 采用的是计算平均耗时模式,单位毫秒 @OutputTimeUnit(TimeUnit.MILLISECONDS) 看看输出结果
在结果输出方式中,我增加了一个json文件进行输出数据,我们可以把json文件上传到分析网站,可以更加直观的得出结论
分析网站 http://deepoove.com/jmh-visual-chart/
可以看出,使用String进行直接拼接大量字符串性能是非常差的,这就是为什么要求使用StringBuilder的原因
六、总结
JMH是一款功能强大的Java性能测试工具,它能够帮助开发者准确、可靠地评估代码的性能,并为性能优化提供参考。
它的使用简单灵活,提供了丰富的注解和选项供开发者配置。
通过使用JMH,我们可以更好地了解代码的性能瓶颈,并进行相应的优化。让我们拿起JMH这个“性能宝剑”,驰骋在Java的性能战场,开创出更优秀的Java应用!