JMH,即(Java Microbenchmark Harness),是专门用于JAVA代码微基准测试的工具套件。何谓Micro Benchmark呢?简单的来说就是基于方法层面的基准测试,精度可以达到微秒级。当你定位到热点方法,希望进一步优化方法性能的时候,就可以使用JMH对优化的结果进行量化的分析。
基准测试:是指通过设计科学的测试方法、测试工具和测试系统,实现对一类测试对象的某项性能指标进行定量的和可对比的测试。比如鲁大师、安兔兔,都是按一定的基准或者在特定条件下去测试某一对象的的性能,比如显卡、IO、CPU之类的。
JMH比较典型的应用场景有:
想准确的知道某个方法需要执行多长时间,以及执行时间和输入之间的相关性。
对比接口不同实现在给定条件下的吞吐量,找到最优实现。
查看多少百分比的请求在多长时间内完成。
1 JMH入门案例
1.1 Maven依赖
JMH是 被作为JDK9而自带的,但是我们可以通过导入相关依赖或者jar包来使用。
<!--JMH--><!--https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-core--><dependency><groupId>org.openjdk.jmh</groupId><artifactId>jmh-core</artifactId><version>1.21</version></dependency><dependency><groupId>org.openjdk.jmh</groupId><artifactId>jmh-generator-annprocess</artifactId><version>1.21</version></dependency>
1.2 程序编写
@BenchmarkMode(Mode.AverageTime)@OutputTimeUnit(TimeUnit.NANOSECONDS)publicclassHelloJMH{/***字符串拼接StringBuilder基准测试*/@BenchmarkpublicvoidtestStringBuilder(){StringBuilderstr=newStringBuilder();for(inti=0;i<1000;i++){str.append(i);}Strings=str.toString();}/***字符串拼接直接相加基准测试*/@BenchmarkpublicvoidtestStringAdd(){Stringstr="";for(inti=0;i<1000;i++){str=str+i;}}publicstaticvoidmain(String[]args)throwsRunnerException{Optionsoptions=newOptionsBuilder().include(HelloJMH.class.getSimpleName())//包含的方法.forks(1)//分出几个进程单独测试.build();newRunner(options).run();}}
@Benchmark注解表示该方法是需要进行benchmark测试的方法。
@BenchmarkMode表示JMH测量方式和角度,本次是测量平均时间。
@OutputTimeUnit表示benchmark 结果所使用的时间单位,可用于类或者方法注解,使用java.util.concurrent.TimeUnit中的标准时间单位。
在 Main 方法中,通过Runner 类去运行Options 实例即可。官方提供了一个OptionsBuilder对象去流式构建。OptionsBuilder的其他配置信息在下面讲。
1.3 测试结果
1)#JMHversion:1.212)#VMversion:JDK1.8.0_144,JavaHotSpot(TM)64-BitServerVM,25.144-b013)#VMinvoker:C:\ProgramFiles\Java\jdk1.8.0_144\jre\bin\java.exe4)#VMoptions:-javaagent:D:\soft\IntelliJIDEA2019.3\lib\idea_rt.jar=61956:D:\soft\IntelliJIDEA2019.3\bin-Dfile.encoding=UTF-85)#Warmup:5iterations,10seach6)#Measurement:5iterations,10seach7)#Timeout:10minperiteration8)#Threads:1thread,willsynchronizeiterations9)#Benchmarkmode:Averagetime,time/op10)#Benchmark:com.thread.test.JMH.HelloJMH.testStringAdd11)#Runprogress:0.00%complete,ETA00:03:2012)#Fork:1of113)#WarmupIteration1:506360.123ns/op14)#WarmupIteration2:460295.578ns/op15)#WarmupIteration3:492550.630ns/op16)#WarmupIteration4:482141.558ns/op17)#WarmupIteration5:469897.660ns/op18)Iteration1:443427.726ns/op19)Iteration2:456970.538ns/op20)Iteration3:440686.491ns/op21)Iteration4:451894.998ns/op22)Iteration5:432889.165ns/op23)Result"com.thread.test.JMH.HelloJMH.testStringAdd":a)445173.784±(99.9%)36450.901ns/op[Average]b)(min,avg,max)=(432889.165,445173.784,456970.538),stdev=9466.183c)CI(99.9%):[408722.883,481624.685](assumesnormaldistribution)24)#JMHversion:1.2125)#VMversion:JDK1.8.0_144,JavaHotSpot(TM)64-BitServerVM,25.144-b0126)#VMinvoker:C:\ProgramFiles\Java\jdk1.8.0_144\jre\bin\java.exe27)#VMoptions:-javaagent:D:\soft\IntelliJIDEA2019.3\lib\idea_rt.jar=61956:D:\soft\IntelliJIDEA2019.3\bin-Dfile.encoding=UTF-828)#Warmup:5iterations,10seach//预热次数29)#Measurement:5iterations,10seach//度量次数30)#Timeout:10minperiteration31)#Threads:1thread,willsynchronizeiterations32)#Benchmarkmode:Averagetime,time/op33)#Benchmark:com.thread.test.JMH.HelloJMH.testStringBuilder34)#Runprogress:50.00%complete,ETA00:01:4035)#Fork:1of136)#WarmupIteration1:10372.126ns/op37)#WarmupIteration2:10301.755ns/op38)#WarmupIteration3:10006.275ns/op39)#WarmupIteration4:9778.343ns/op40)#WarmupIteration5:9868.092ns/op41)Iteration1:9641.269ns/op42)Iteration2:10259.971ns/op43)Iteration3:9844.944ns/op44)Iteration4:9704.533ns/op45)Iteration5:9711.980ns/op46)Result"com.thread.test.JMH.HelloJMH.testStringBuilder":a)9832.539±(99.9%)963.347ns/op[Average]b)(min,avg,max)=(9641.269,9832.539,10259.971),stdev=250.178c)CI(99.9%):[8869.193,10795.886](assumesnormaldistribution)47)#Runcomplete.Totaltime:00:03:2148)REMEMBER:Thenumbersbelowarejustdata.Togainreusableinsights,youneedtofollowupon49)whythenumbersarethewaytheyare.Useprofilers(see-prof,-lprof),designfactorial50)experiments,performbaselineandnegativeteststhatprovideexperimentalcontrol,makesure51)thebenchmarkingenvironmentissafeonJVM/OS/HWlevel,askforreviewsfromthedomainexperts.52)Donotassumethenumberstellyouwhatyouwantthemtotell.53)BenchmarkModeCntScoreErrorUnits54)JMH.HelloJMH.testStringAddavgt5445173.784±36450.901ns/op55)JMH.HelloJMH.testStringBuilderavgt59832.539±963.347ns/op
解释:
第1-10行表示测试的基本信息,比如,使用的Java路径,预热代码的迭代次数,测量代码的迭代次数,使用的线程数量,测试的统计单位等。
从第13行开始显示了每次预热迭代的结果,预热迭代不会作为最终的统计结果。预热的目的是让Java虚拟机对被测代码进行足够多的优化,比如,在预热后被测代码应该得到了充分的JIT编译和优化。
从第18行开始显示每次基准测试迭代的结果,每一次迭代都显示了当前的执行速率,即一个操作所花费的时间。
在进行5次迭代后,进行统计,结果在Result后。Result第一段结果告诉了我们最大值、最小值、平均值的信息。第二段是最主要的信息。在本例中,第54、55行显示了testStringBuilder和testStringAdd函数的平均执行花费时间和误差时间。从结果可以看出,大量字符串拼接式时,使用StringBuilder效率更高。
2 JMH的基本概念和配置
2.1 测试模式(Mode)
Mode表示JMH的测量方式和角度,共有4种,吞吐量和方法执行的平均时间是最为常用的统计方式。可通过@BenchmarkMode注解配置。
Throughput: 整体吞吐量, 表示1秒内可以执行多少次调用。如下:
1)BenchmarkModeCntScoreErrorUnits2)JMH.HelloJMH.testStringAddthrpt5≈10⁻⁵ops/ns3)JMH.HelloJMH.testStringBuilderthrpt5≈10⁻⁴ops/ns
AverageTime: 调用的平均时间, 指每一次调用所需要的时间,即案例中的模式。
SampleTime:随机取样,最后输出取样结果的分布,例如“99%的调用在XXX 毫秒以内,99.99%的调用在XXX 毫秒以内”。如下:
1)BenchmarkModeCntScoreErrorUnits2)JMH.HelloJMH.testStringAddsample110636451524.056±1674.469ns/op3)JMH.HelloJMH.testStringAdd:testStringAdd·p0.00sample307712.000ns/op4)JMH.HelloJMH.testStringAdd:testStringAdd·p0.50sample392192.000ns/op5)JMH.HelloJMH.testStringAdd:testStringAdd·p0.90sample558080.000ns/op6)JMH.HelloJMH.testStringAdd:testStringAdd·p0.95sample649216.000ns/op7)JMH.HelloJMH.testStringAdd:testStringAdd·p0.99sample1337344.000ns/op8)JMH.HelloJMH.testStringAdd:testStringAdd·p0.999sample2023424.000ns/op9)JMH.HelloJMH.testStringAdd:testStringAdd·p0.9999sample2742493.594ns/op10)JMH.HelloJMH.testStringAdd:testStringAdd·p1.00sample3420160.000ns/op11)JMH.HelloJMH.testStringBuildersample122858710293.875±39.332ns/op12)JMH.HelloJMH.testStringBuilder:testStringBuilder·p0.00sample8688.000ns/op13)JMH.HelloJMH.testStringBuilder:testStringBuilder·p0.50sample9600.000ns/op14)JMH.HelloJMH.testStringBuilder:testStringBuilder·p0.90sample10592.000ns/op15)JMH.HelloJMH.testStringBuilder:testStringBuilder·p0.95sample11600.000ns/op16)JMH.HelloJMH.testStringBuilder:testStringBuilder·p0.99sample21280.000ns/op17)JMH.HelloJMH.testStringBuilder:testStringBuilder·p0.999sample71552.000ns/op18)JMH.HelloJMH.testStringBuilder:testStringBuilder·p0.9999sample695296.000ns/op19)JMH.HelloJMH.testStringBuilder:testStringBuilder·p1.00sample2019328.000ns/op
SingleShotTime: 以上模式都是默认一次Iteration是1秒,唯有SingleShotTime 只运行一次。往往同时把warmup 次数设为0, 用于测试冷启动时的性能。
All: 顾名思义,所有模式,这个在内部测试中常用。
2.2 迭代(Iteration)
迭代是JMH的一次测量的单位。在大部分测量模式下,一次迭代表示1秒。在这一秒内会不间断调用被测方法,并采样计算吞吐量、平均时间等。
2.3 预热(Warmup)
Warmup 是指在实际进行 benchmark 前先进行预热的行为。
为什么需要预热?因为 JVM 的 JIT 机制的存在,如果某个函数被调用多次之后,JVM 会尝试将其编译成为机器码从而提高执行速度。为了让 benchmark 的结果更加接近真实情况就需要进行预热。
由于Java 虚拟机的JIT 的存在,同一个方法在JIT编译前后的时间将会不同。通常只考虑方法在JIT编译后的性能。使用 -Xint 参数可以关闭JIT优化。
2.4 状态(State)
@State注解,作用在类上。通过State 可以指定一个对象的作用范围,范围主要有三种:
Scope.Thread:默认的State,每个测试线程分配一个实例,也就是一个对象只会被一个线程访问。在多线程池测试时,会为每一个线程生成一个对象;
Scope.Benchmark:所有测试线程共享一个实例,用于测试有状态实例在多线程共享下的性能
Scope.Group:每个线程组共享一个实例;
2.5 配置类(Options/OptionsBuilder)
在测试开始前, 首先要对测试进行配置。通常需要指定一些参数, 比如指定测试类(include) 、使用的进程个数(fork) 、预热迭代次数(warmuplterations) 。在配置启动测试时, 需要使用配置类。
OptionsBuilder的常用方法及对应的注解形式如下:
方法名参数作用对应注解include接受一个字符串表达式,表示需要测试的类和方法。指定要运行的基准测试类和方法-exclude接受一个字符串表达式,表示不需要测试的类和方法指定不要运行的基准测试类方法-warmupIterations预热的迭代次数指定预热的迭代次数@WarmupwarmupBatchSize预热批量的大小指定预热批量的大小@WarmupwarmupForks预热模式:INDI,BULK,BULK_INDI指定预热模式@WarmupwarmupMode预热的模式指定预热的模式@WarmupwarmupTime预热的时间指定预热的时间@WarmupmeasurementIterations测试的迭代次数指定测试的迭代次数@MeasurementmeasurementBatchSize测试批量的大小指定测试批量的大小@MeasurementmeasurementTime测试的时间指定测试的时间@Measurementmode测试模式: Throughput(吞吐量), AverageTime(平均时间),SampleTime(在测试中,随机进行采样执行的时间),SingleShotTime(在每次执行中计算耗时),All(所有)指定测试模式@BenchmarkMode--可用于类或者方法上Fork子进程数threads每个方法开启线程数量多线程测试@Threads,可用在方法或者类上2.6 其他注解
@OutputTimeUnit:benchmark 结果所使用的时间单位,可用于类或者方法注解,使用java.util.concurrent.TimeUnit中的标准时间单位。
@Setup:方法注解,会在执行 benchmark 之前被执行,正如其名,主要用于初始化。
@TearDown:方法注解,与@Setup相对的,会在所有benchmark执行结束以后执行,主要用于资源的回收等。
@Param:成员注解,可以用来指定某项参数的多种情况。特别适合用来测试一个函数在不同的参数输入的情况下的性能。@Param注解接收一个String数组,在@setup方法执行前转化为为对应的数据类型。多个@Param注解的成员之间是乘积关系,譬如有两个用@Param注解的字段,第一个有5个值,第二个字段有2个值,那么每个测试方法会跑5*2=10次。
参考资料:
《实战Java高并发程序设计》
如果有什么不懂或者需要交流,可以留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!