我们在很多场景需要对代码进行性能的测试:
- 在进行性能优化的时候已经把瓶颈定位在了某个局部的代码,现在需要逐步进行优化,以及验证
- 对多种代码实现方案的性能比对,挑选出性能优的方案
这个时候我们就需要一款能够对Java代码/方法进行性能测试的工具。JMH(Java Microbenchmark Harness)是一款小型的Java基准测试工具。何为Microbenchmark?能够进行method级别的benchmark测试,精确到微妙级。
更多分布式系统设计资料:
github.com/xiajunhust/…
从一个示例开始
我们看如下一段测试代码,用来比较2个字符串拼接方法的性能差异:
maven依赖:
<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>
</dependency>
\
package com.alipay.worktest.jmh;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;
import org.openjdk.jmh.results.format.ResultFormatType;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import java.util.concurrent.TimeUnit;
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 3, time = 1)
@Measurement(iterations = 5, time = 5)
@Threads(4)
@Fork(1)
@State(value = Scope.Benchmark)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class StringConnectTest {
@Param(value = {"10", "50", "100"})
private int length;
@Benchmark
public void testStringAdd(Blackhole blackhole) {
String a = "";
for (int i = 0; i < length; i++) {
a += i;
}
blackhole.consume(a);
}
@Benchmark
public void testStringBuilderAdd(Blackhole blackhole) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < length; i++) {
sb.append(i);
}
blackhole.consume(sb.toString());
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(StringConnectTest.class.getSimpleName())
.result("result.json")
.resultFormat(ResultFormatType.JSON).build();
new Runner(opt).run();
}
}
\
跑出来的结果是一个JSON格式数据:
[
{
"jmhVersion" : "1.23",
"benchmark" : "com.alipay.worktest.jmh.StringConnectTest.testStringAdd",
"mode" : "avgt",
"threads" : 4,
"forks" : 1,
"jvm" : "/Library/Java/JavaVirtualMachines/jdk1.8.0_321.jdk/Contents/Home/jre/bin/java",
"jvmArgs" : [
"-javaagent:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=53328:/Applications/IntelliJ IDEA.app/Contents/bin",
"-Dfile.encoding=UTF-8"
],
"jdkVersion" : "1.8.0_321",
"vmName" : "Java HotSpot(TM) 64-Bit Server VM",
"vmVersion" : "25.321-b07",
"warmupIterations" : 3,
"warmupTime" : "1 s",
"warmupBatchSize" : 1,
"measurementIterations" : 5,
"measurementTime" : "5 s",
"measurementBatchSize" : 1,
"params" : {
"length" : "10"
},
"primaryMetric" : {
"score" : 104.37353532768762,
"scoreError" : 54.40382524005513,
"scoreConfidence" : [
49.96971008763249,
158.77736056774273
],
"scorePercentiles" : {
"0.0" : 96.11150190679969,
"50.0" : 96.59461051460224,
"90.0" : 129.00380282412652,
"95.0" : 129.00380282412652,
"99.0" : 129.00380282412652,
"99.9" : 129.00380282412652,
"99.99" : 129.00380282412652,
"99.999" : 129.00380282412652,
"99.9999" : 129.00380282412652,
"100.0" : 129.00380282412652
},
"scoreUnit" : "ns/op",
"rawData" : [
[
129.00380282412652,
103.69461443995105,
96.59461051460224,
96.11150190679969,
96.46314695295848
]
]
},
"secondaryMetrics" : {
}
},
{
"jmhVersion" : "1.23",
"benchmark" : "com.alipay.worktest.jmh.StringConnectTest.testStringAdd",
"mode" : "avgt",
"threads" : 4,
"forks" : 1,
"jvm" : "/Library/Java/JavaVirtualMachines/jdk1.8.0_321.jdk/Contents/Home/jre/bin/java",
"jvmArgs" : [
"-javaagent:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=53328:/Applications/IntelliJ IDEA.app/Contents/bin",
"-Dfile.encoding=UTF-8"
],
"jdkVersion" : "1.8.0_321",
"vmName" : "Java HotSpot(TM) 64-Bit Server VM",
"vmVersion" : "25.321-b07",
"warmupIterations" : 3,
"warmupTime" : "1 s",
"warmupBatchSize" : 1,
"measurementIterations" : 5,
"measurementTime" : "5 s",
"measurementBatchSize" : 1,
"params" : {
"length" : "50"
},
"primaryMetric" : {
"score" : 839.7015408571585,
"scoreError" : 1012.0904808153387,
"scoreConfidence" : [
-172.38893995818012,
1851.7920216724972
],
"scorePercentiles" : {
"0.0" : 695.2649561315446,
"50.0" : 720.1534550275057,
"90.0" : 1305.5735933226465,
"95.0" : 1305.5735933226465,
"99.0" : 1305.5735933226465,
"99.9" : 1305.5735933226465,
"99.99" : 1305.5735933226465,
"99.999" : 1305.5735933226465,
"99.9999" : 1305.5735933226465,
"100.0" : 1305.5735933226465
},
"scoreUnit" : "ns/op",
"rawData" : [
[
720.1534550275057,
695.2649561315446,
695.3827943248662,
782.13290547923,
1305.5735933226465
]
]
},
"secondaryMetrics" : {
}
},
{
"jmhVersion" : "1.23",
"benchmark" : "com.alipay.worktest.jmh.StringConnectTest.testStringAdd",
"mode" : "avgt",
"threads" : 4,
"forks" : 1,
"jvm" : "/Library/Java/JavaVirtualMachines/jdk1.8.0_321.jdk/Contents/Home/jre/bin/java",
"jvmArgs" : [
"-javaagent:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=53328:/Applications/IntelliJ IDEA.app/Contents/bin",
"-Dfile.encoding=UTF-8"
],
"jdkVersion" : "1.8.0_321",
"vmName" : "Java HotSpot(TM) 64-Bit Server VM",
"vmVersion" : "25.321-b07",
"warmupIterations" : 3,
"warmupTime" : "1 s",
"warmupBatchSize" : 1,
"measurementIterations" : 5,
"measurementTime" : "5 s",
"measurementBatchSize" : 1,
"params" : {
"length" : "100"
},
"primaryMetric" : {
"score" : 32956.27123766383,
"scoreError" : 267012.8315788902,
"scoreConfidence" : [
-234056.56034122637,
299969.102816554
],
"scorePercentiles" : {
"0.0" : 1841.5207789439423,
"50.0" : 1875.3263286044403,
"90.0" : 156999.3648593761,
"95.0" : 156999.3648593761,
"99.0" : 156999.3648593761,
"99.9" : 156999.3648593761,
"99.99" : 156999.3648593761,
"99.999" : 156999.3648593761,
"99.9999" : 156999.3648593761,
"100.0" : 156999.3648593761
},
"scoreUnit" : "ns/op",
"rawData" : [
[
156999.3648593761,
2223.449094824512,
1841.5207789439423,
1875.3263286044403,
1841.6951265701437
]
]
},
"secondaryMetrics" : {
}
},
{
"jmhVersion" : "1.23",
"benchmark" : "com.alipay.worktest.jmh.StringConnectTest.testStringBuilderAdd",
"mode" : "avgt",
"threads" : 4,
"forks" : 1,
"jvm" : "/Library/Java/JavaVirtualMachines/jdk1.8.0_321.jdk/Contents/Home/jre/bin/java",
"jvmArgs" : [
"-javaagent:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=53328:/Applications/IntelliJ IDEA.app/Contents/bin",
"-Dfile.encoding=UTF-8"
],
"jdkVersion" : "1.8.0_321",
"vmName" : "Java HotSpot(TM) 64-Bit Server VM",
"vmVersion" : "25.321-b07",
"warmupIterations" : 3,
"warmupTime" : "1 s",
"warmupBatchSize" : 1,
"measurementIterations" : 5,
"measurementTime" : "5 s",
"measurementBatchSize" : 1,
"params" : {
"length" : "10"
},
"primaryMetric" : {
"score" : 73.26168608974095,
"scoreError" : 6.80320933870852,
"scoreConfidence" : [
66.45847675103244,
80.06489542844946
],
"scorePercentiles" : {
"0.0" : 71.66782608452925,
"50.0" : 72.71042944925414,
"90.0" : 76.2384487740947,
"95.0" : 76.2384487740947,
"99.0" : 76.2384487740947,
"99.9" : 76.2384487740947,
"99.99" : 76.2384487740947,
"99.999" : 76.2384487740947,
"99.9999" : 76.2384487740947,
"100.0" : 76.2384487740947
},
"scoreUnit" : "ns/op",
"rawData" : [
[
71.66782608452925,
72.71042944925414,
73.31208404057011,
72.3796421002565,
76.2384487740947
]
]
},
"secondaryMetrics" : {
}
},
{
"jmhVersion" : "1.23",
"benchmark" : "com.alipay.worktest.jmh.StringConnectTest.testStringBuilderAdd",
"mode" : "avgt",
"threads" : 4,
"forks" : 1,
"jvm" : "/Library/Java/JavaVirtualMachines/jdk1.8.0_321.jdk/Contents/Home/jre/bin/java",
"jvmArgs" : [
"-javaagent:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=53328:/Applications/IntelliJ IDEA.app/Contents/bin",
"-Dfile.encoding=UTF-8"
],
"jdkVersion" : "1.8.0_321",
"vmName" : "Java HotSpot(TM) 64-Bit Server VM",
"vmVersion" : "25.321-b07",
"warmupIterations" : 3,
"warmupTime" : "1 s",
"warmupBatchSize" : 1,
"measurementIterations" : 5,
"measurementTime" : "5 s",
"measurementBatchSize" : 1,
"params" : {
"length" : "50"
},
"primaryMetric" : {
"score" : 382.5654880443052,
"scoreError" : 3.175081894080194,
"scoreConfidence" : [
379.390406150225,
385.7405699383854
],
"scorePercentiles" : {
"0.0" : 381.1825172868479,
"50.0" : 382.8092182310429,
"90.0" : 383.30706169689176,
"95.0" : 383.30706169689176,
"99.0" : 383.30706169689176,
"99.9" : 383.30706169689176,
"99.99" : 383.30706169689176,
"99.999" : 383.30706169689176,
"99.9999" : 383.30706169689176,
"100.0" : 383.30706169689176
},
"scoreUnit" : "ns/op",
"rawData" : [
[
382.8092182310429,
382.52083165866816,
383.30706169689176,
383.0078113480757,
381.1825172868479
]
]
},
"secondaryMetrics" : {
}
},
{
"jmhVersion" : "1.23",
"benchmark" : "com.alipay.worktest.jmh.StringConnectTest.testStringBuilderAdd",
"mode" : "avgt",
"threads" : 4,
"forks" : 1,
"jvm" : "/Library/Java/JavaVirtualMachines/jdk1.8.0_321.jdk/Contents/Home/jre/bin/java",
"jvmArgs" : [
"-javaagent:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=53328:/Applications/IntelliJ IDEA.app/Contents/bin",
"-Dfile.encoding=UTF-8"
],
"jdkVersion" : "1.8.0_321",
"vmName" : "Java HotSpot(TM) 64-Bit Server VM",
"vmVersion" : "25.321-b07",
"warmupIterations" : 3,
"warmupTime" : "1 s",
"warmupBatchSize" : 1,
"measurementIterations" : 5,
"measurementTime" : "5 s",
"measurementBatchSize" : 1,
"params" : {
"length" : "100"
},
"primaryMetric" : {
"score" : 778.4593415225656,
"scoreError" : 62.30734983271735,
"scoreConfidence" : [
716.1519916898483,
840.766691355283
],
"scorePercentiles" : {
"0.0" : 762.777875969865,
"50.0" : 772.686315788206,
"90.0" : 803.2454160577608,
"95.0" : 803.2454160577608,
"99.0" : 803.2454160577608,
"99.9" : 803.2454160577608,
"99.99" : 803.2454160577608,
"99.999" : 803.2454160577608,
"99.9999" : 803.2454160577608,
"100.0" : 803.2454160577608
},
"scoreUnit" : "ns/op",
"rawData" : [
[
762.777875969865,
803.2454160577608,
768.1863916764114,
772.686315788206,
785.400708120585
]
]
},
"secondaryMetrics" : {
}
}
]
更多的示例可参考:
JMH基础知识介绍
@BenchmarkMode
JMH能够以不同的模式来执行基准测试,JMH支持如下几种模式:
- Throughput:吞吐率,即每秒被测试方法被执行的次数
- Average Time:方法平均执行耗时
- Sample Time:随机采样,展示其分布
- Single Shot Time:只执行一次,获取其耗时,一般用于测试冷启动的性能。
@State
用于定义作用范围。
- Scope.Benchmark:所有测试线程共享一个实例,测试有状态实例在多线程共享下的性能
- Scope.Group:同一个线程在同一个 group 里共享实例
- Scope.Thread:默认的 State,每个测试线程分配一个实例
@Warmup
预热配置参数。
- iterations:预热的次数
- time:每次预热的时间
- timeUnit:时间的单位,默认秒
- batchSize:批处理大小,每次操作调用几次方法
@Threads
每个进程的测试线程数。
@Fork
进行 fork 的次数,可用于类或者方法上。如果 fork 数是 2 的话,则 JMH 会 fork 出两个进程来进行测试。
JMH使用注意事项
预热-JVM JIT机制
JIT是指Java中的即时编译器(Just-in-time)跟静态编译相比,即时编译器选择性的编译热点代码,省去编译时间。因此我们在进行基准测试的时候,需要进行预热,以获得真实情况下的测试结果。
JIT编译优化-死码消除
JVM可能会把从未使用的变量相关的方法的整个代码全部移除,这样会导致测试结果不准确。解决办法是一种是增加return变量来显式引用,另一种是通过 Blackhole 的 consume 来避免。
public void testMethod() {
int a = 1;
int b = 2;
int sum = a + b;
}
解决死码示例:
public void testMethod(Blackhole blackhole) {
int a = 1;
int b = 2;
int sum = a + b;
blackhole.consume(sum);
}
避免循环
JVM会对循环进行优化,这样会导致获取的测试结果不准确。
JMH结果可视化展示
JMH测试结果是一个json串,我们也可以通过一些工具进行可视化直观展示。
- JMH Visual Chart:deepoove.com/jmh-visual-…
- JMH Visualizer:jmh.morethan.io/