JMH(Java Microbenchmark Harness)是一个 Java 工具,用于构建、运行和分析用 Java 和其他针对 JVM 的语言编写的 纳米/微米/毫/宏观 基准测试,而且是由Java虚拟机团队开发的。简单说,就是用来测量代码运行性能。
JMeter可能是最常用的性能测试工具。它既支持图形界面,也支持命令行,属于黑盒测试的范畴,对非开发人员比较友好,上手也非常容易。
图形界面一般用于编写、调试测试用例,而实际的性能测试建议还是在命令行下运行。
很多场景下JMeter和JMH都可以做性能测试,但是对于严格意义上的基准测试来说,只有JMH才适合。JMeter的测试结果精度相对JVM较低、所以JMeter不适合于类级别的基准测试,更适合于对精度要求不高、耗时相对较长的操作。
JMeter测试精度差: JMeter自身框架比较重,举个例子:使用JMH测试一个方法,平均耗时0.01ms,而使用JMeter测试的结果平均耗时20ms,相差200倍。
JMeter内置很多采样器 :JMeter内置了支持多种网络协议的采样器,可以在不写Java代码的情况下实现很多复杂的测试。JMeter支持集群的方式运行,方便模拟多用户、高并发压力测试。
JMeter适合一些相对耗时的集成功能测试,如API接口的测试。JMH适合于类或者方法的单元测试。
JMH非常好的一点是我们非常灵活: 我们可以手动来编写测试的代码。
比如
1 2 3 4 @Benchmark public void test () { this .controller.getRandomPoet(); }
准备工作
导入依赖
1 2 3 4 5 6 7 8 9 10 11 12 <dependency > <groupId > org.openjdk.jmh</groupId > <artifactId > jmh-core</artifactId > <version > 1.22</version > <scope > provided</scope > </dependency > <dependency > <groupId > org.openjdk.jmh</groupId > <artifactId > jmh-generator-annprocess</artifactId > <version > 1.22</version > <scope > provided</scope > </dependency >
部分注解/字段含义
注解
说明
@BenchmarkMode
JMH进行Benchmark时使用的模式。包括Throughput, AverageTime, SampleTime和SingleShotTime
@OutputTimeUnit
输出的时间单位
@Iteration
JMH测试的最小单位。一次iteration代表的是一秒,JMH在这一秒内不断调用需要Benchmark的方法。
@WarmUp
预热行为。在实际进行Benchmark前先进行预热,让Benchmark结果更接近真实情况。
@State
定义类实例的生命周期,类似于Spring Bean的Scope。包括Scope.Thread, Scope.Benchmark和Scope.Group
@Fork
进行fork的次数。如果fork数是2的话,则JMH会fork出两个进程来进行测试。
@Measurement
提供真正的测试阶段参数。指定迭代的次数,每次迭代的运行时间和每次迭代测试调用的数量。
@Setup
在执行benchmark之前被执行,主要用于初始化。
@TearDown
在所有benchmark执行结束以后执行,主要用于资源的回收等。
@Benchmark
表示该方法需要进行benchmark的对象。
@Param
用来指定某项参数的多种情况。特别适合用来测试一个函数在不同的参数输入的情况下的性能。
BenchMark基准测试
首先给出 SortBenchMarkTest
, 表示我们需要测试的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @State(Scope.Thread) public class SortBenchMarkTest { 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); } }
接着创建 MyBenchmarkTest
, 是来执行测试的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class MyBenchmarkTest { @Test public void testMyBenchmark () throws Exception { Options options = new OptionsBuilder () .include(SortBenchMarkTest.class.getSimpleName()) .forks(1 ) .threads(1 ) .warmupIterations(5 ) .measurementIterations(5 ) .mode(Mode.AverageTime) .build(); new Runner (options).run(); } }
我们也可以把测试的结果保存到文件中, 并且通过平台进行分析:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 private Options getOptions () { String resultFilePrefix = this .getClass().getSimpleName() + "jmh-result" ; ResultFormatType resultsFileOutputType = ResultFormatType.JSON; return new OptionsBuilder () .include("\\." + this .getClass().getSimpleName() + "\\." ) .shouldDoGC(true ) .shouldFailOnError(true ) .forks(1 ) .threads(1 ) .warmupIterations(0 ) .measurementIterations(1 ) .timeUnit(TimeUnit.MICROSECONDS) .resultFormat(resultsFileOutputType) .result(buildResultsFileName(resultFilePrefix, resultsFileOutputType)) .shouldFailOnError(true ) .jvmArgs("-server" ) .build(); } private static String buildResultsFileName (String resultFilePrefix, ResultFormatType resultType) { String suffix = switch (resultType) { case CSV -> ".csv" ; case SCSV -> ".scsv" ; case LATEX -> ".tex" ; case JSON -> ".json" ; default -> ".json" ; }; return String.format("../target/%s%s" , resultFilePrefix, suffix); }
测试结果 (SortStringBenchmarkTestjmh-result.json
) 如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 [ { "jmhVersion" : "1.22" , "benchmark" : "com.dhx.apiinterface.jmh.SortStringBenchmarkTest.testSort" , "mode" : "avgt" , "threads" : 1 , "forks" : 1 , "jvm" : "D:\\jdk17_\\bin\\java.exe" , "jvmArgs" : [ "-server" ] , "jdkVersion" : "17.0.6" , "vmName" : "Java HotSpot(TM) 64-Bit Server VM" , "vmVersion" : "17.0.6+9-LTS-190" , "warmupIterations" : 0 , "warmupTime" : "10 s" , "warmupBatchSize" : 1 , "measurementIterations" : 1 , "measurementTime" : "10 s" , "measurementBatchSize" : 1 , "primaryMetric" : { "score" : 36.30124459659404 , "scoreError" : "NaN" , "scoreConfidence" : [ "NaN" , "NaN" ] , "scorePercentiles" : { "0.0" : 36.30124459659404 , "50.0" : 36.30124459659404 , "90.0" : 36.30124459659404 , "95.0" : 36.30124459659404 , "99.0" : 36.30124459659404 , "99.9" : 36.30124459659404 , "99.99" : 36.30124459659404 , "99.999" : 36.30124459659404 , "99.9999" : 36.30124459659404 , "100.0" : 36.30124459659404 } , "scoreUnit" : "us/op" , "rawData" : [ [ 36.30124459659404 ] ] } , "secondaryMetrics" : { } } ]
其实可以写到一个类里面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 @State(Scope.Benchmark) @BenchmarkMode(Mode.AverageTime) @Timeout(time = 120) public class SortStringBenchmarkTest { 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 { new Runner (getOptions()).run(); } private Options getOptions () { String resultFilePrefix = this .getClass().getSimpleName() + "jmh-result" ; ResultFormatType resultsFileOutputType = ResultFormatType.JSON; return new OptionsBuilder () .include("\\." + this .getClass().getSimpleName() + "\\." ) .shouldDoGC(true ) .shouldFailOnError(true ) .forks(1 ) .threads(1 ) .warmupIterations(0 ) .measurementIterations(1 ) .timeUnit(TimeUnit.MICROSECONDS) .resultFormat(resultsFileOutputType) .result(buildResultsFileName(resultFilePrefix, resultsFileOutputType)) .shouldFailOnError(true ) .jvmArgs("-server" ) .build(); } private static String buildResultsFileName (String resultFilePrefix, ResultFormatType resultType) { String suffix = switch (resultType) { case CSV -> ".csv" ; case SCSV -> ".scsv" ; case LATEX -> ".tex" ; case JSON -> ".json" ; default -> ".json" ; }; return String.format("target/%s%s" , resultFilePrefix, suffix); } }
查看target下的测试数据文件
访问 https://jmh.morethan.io/# 进行测试结果分析:
抽离公共代码
上面的代码, 我们可以抽离出一个抽象类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public abstract class BaseBenchMarkTest { protected Options getOptions () { String resultFilePrefix = this .getClass().getSimpleName() + "jmh-result" ; ResultFormatType resultsFileOutputType = ResultFormatType.JSON; return new OptionsBuilder () .include("\\." + this .getClass().getSimpleName() + "\\." ) .shouldDoGC(true ) .shouldFailOnError(true ) .forks(1 ) .threads(1 ) .warmupIterations(5 ) .measurementIterations(10 ) .timeUnit(TimeUnit.MICROSECONDS) .resultFormat(resultsFileOutputType) .result(buildResultsFileName(resultFilePrefix, resultsFileOutputType)) .shouldFailOnError(true ) .jvmArgs("-server" ) .build(); } private static String buildResultsFileName (String resultFilePrefix, ResultFormatType resultType) { String suffix = switch (resultType) { case CSV -> ".csv" ; case SCSV -> ".scsv" ; case LATEX -> ".tex" ; case JSON -> ".json" ; default -> ".json" ; }; return String.format("target/%s%s" , resultFilePrefix, suffix); } }
修改之后的测试代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public class SortStringBenchmarkTest extends BaseBenchMarkTest { 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 { new Runner (getOptions()).run(); } }
SpringBoot中使用BenchMark进行测试
对于SpringBoot中的测试, 关键在于需要用到Bean , 此时需要我们手动获取上下文信息并getBean()
获取上下文 :
1 2 3 4 5 6 7 8 9 10 11 12 ConfigurableApplicationContext context; @Setup public void setup () { context = SpringApplication.run(ApiInterfaceApplication.class); } @TearDown public void tearDown () { context.close(); }
获取Bean:
1 this .controller = this .context.getBean(Version1Controller.class);
进行测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class RandomPoetBenchmarkTest extends SpringBaseBenchMarkTest { private Version1Controller controller; @Benchmark public void test () { this .controller.getRandomPoet(); } @BeforeEach public void getBean () { this .controller = this .context.getBean(Version1Controller.class); } @Test public void executeJmhRunner () throws RunnerException, IOException { new Runner (getOptions()).run(); } }
测试信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 Iteration 1: 1.392 ±(99.9%) 0.055 ms/op Iteration 2: 1.248 ±(99.9%) 0.068 ms/op Iteration 3: 1.158 ±(99.9%) 0.088 ms/op Iteration 4: [2024-01-03 23:15:15.583] [] INFO o.a.d.r.t.n.NettyServerHandler ==> [DUBBO] The connection of /100.122.217.6:56510 -> /100.72.207.93:20880 is established., dubbo version: 3.1.6, current host: 192.168.166.1 1.100 ±(99.9%) 0.042 ms/op Iteration 5: 1.096 ±(99.9%) 0.046 ms/op Iteration 6: 1.193 ±(99.9%) 0.038 ms/op Iteration 7: 1.454 ±(99.9%) 0.058 ms/op Iteration 8: 1.329 ±(99.9%) 0.037 ms/op Iteration 9: 1.212 ±(99.9%) 0.075 ms/op Iteration 10: 1.242 ±(99.9%) 0.047 ms/op Iteration 11: 1.300 ±(99.9%) 0.074 ms/op Iteration 12: 1.193 ±(99.9%) 0.058 ms/op Iteration 13: 1.264 ±(99.9%) 0.033 ms/op Iteration 14: 1.228 ±(99.9%) 0.074 ms/op Iteration 15: 1.394 ±(99.9%) 0.036 ms/op Iteration 16: 1.413 ±(99.9%) 0.079 ms/op Iteration 17: 1.184 ±(99.9%) 0.052 ms/op Iteration 18: 1.170 ±(99.9%) 0.044 ms/op Iteration 19: 1.306 ±(99.9%) 0.074 ms/op Result "com.dhx.apiinterface.jmh.NewV1ApiBenchmarkTest.test" : 1.257 ±(99.9%) 0.088 ms/op [Average] (min, avg, max) = (1.096, 1.257, 1.454), stdev = 0.101 CI (99.9%): [1.169, 1.345] (assumes normal distribution) 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 NewV1ApiBenchmarkTest.test avgt 20 1.257 ± 0.088 ms/op
Reference