概述
Java Microbenchmark Harness,简称JMH,由OpenJDK开发,用来构建、运行和分析Java或其他JVM语言代码的微基准测试框架。适合于方法级别的细粒度测试,并不适用于系统之间的链路测试。
为什么需要JMH,也就是需求产生
- 死码消除:所谓死码,是指注释的代码,不可达的代码块,可达但不被使用的代码等;
- 常量折叠与常量传播:常量折叠(Constant Folding)是一个在编译时期简化常数的一个过程,若是一个变数从未被修改也可作为常数,或直接将一个变数被明确地被标注为常数。
Maven
JMH是一个工具包,pom文件中引入两个依赖
jmh-core
、
jmh-generator-annprocess
即可:
<dependency><groupId>org.openjdk.jmh</groupId><artifactId>jmh-core</artifactId></dependency><dependency><groupId>org.openjdk.jmh</groupId><artifactId>jmh-generator-annprocess</artifactId></dependency>
通过
maven archtype
可以很快的创建一个JMH工程:
mvn archetype:generate
-DinteractiveMode=false
-DarchetypeGroupId=org.openjdk.jmh
-DarchetypeArtifactId=jmh-java-benchmark-archetype
-DarchetypeVersion=1.36-DgroupId=org.johnny.sample.jmh
-DartifactId=logging
-Dversion=1.0
生成的maven工程里,自带maven-shade-plugin插件。
流程
一个JMH测试流程如下:
注解
@Benchmark
是用来标记测试方法的,只有被这个注解标记的话,该方法才会参与基准测试,被@Benchmark标记的必须是public方法。
@Warmup
用来配置预热的内容,可用于类或方法上,越靠近执行方法的地方越准确,Warmup参数:
- iterations:预热次数
- time:每次预热时间
- timeUnit:时间单位,默认是s
- batchSize:批处理大小,每次操作调用几次方法
@Measurement
配置选项和@Warmup一样,用来控制实际执行的内容。不同于预热,它指的是真正的迭代次数。
@BenchmarkMode
用于评估吞吐率的测量纬度:
- Mode.Throughput:单位时间的操作数
- Mode.AverageTime:平均时间
- Mode.SampleTime:时间采样
- Mode.SingleShotTime:单词操作时间
- Mode.All:运用所有的检测模式
在方法级别指定@BenchmarkMode时可以一次指定多个纬度,例如:
@BenchmarkMode({Mode.Throughput, Mode.AverageTime, Mode.SingleShotTime})
,代表同时在多个纬度对目标方法进行测量。
@OutputTimeUnit
代表测量的单位,如秒,毫秒,微妙等。一般都使用微妙和毫秒,可用在方法和类级别,当用在类级别时会被更加精确的方法级别的注解覆盖,原则就是离目标更近的注解更容易生效。
@State
很多时候需要维护一些状态内容,比如在多线程时会维护一个共享的状态,这个状态值可能会在每个线程中都一样,也有可能是每个线程都有自己的状态,JMH提供状态的支持。该注解只能用来标注在类上,因为类作为一个属性的载体。@State枚举值:
- Scope.Benchmark:测试类级别共享,在所有Benchmark的工作线程中共享变量内容
- Scope.Group:同一个Group的线程可以享有同样的变量
- Scope.Thread:每个线程都享有一份变量的副本,线程之间对于变量的修改不会相互影响
@Threads
面向线程,此注解可用于开启并行测试。可配置在方法或类上,代表执行测试的线程数量。配置
Threads.MAX
,则使用和处理器核数相同的线程数。
通常看到这里会比较迷惑Iteration和Invocation区别,在配置Warmup的时候默认时间是1s,即1s的执行作为一个Iteration,假设每次方法的执行是100ms的话,那么1个Iteration就代表10个Invocation。
@Fork
value一般设置成1,表示只使用一个进程进行测试;如果这个数字大于1,表示会启用新的进程进行测试;设置成0,程序依然会运行,不过这样是在用户的JVM进程上运行,可以看下下面的提示,但不推荐这么做。可适当增加fork数,来减少测试的误差。
每个fork进程是单独运行在 Proccess 进程里的,这样就可以做完全的环境隔离,避免交叉影响。它的输入输出流,通过Socket连接的模式,发送到执行终端。
参数jvmArgsAppend,可以通过它传递一些JVM的参数:
@Fork(value = 3, jvmArgsAppend = {"-Xmx2048m", "-server", "-XX:+AggressiveOpts"})
@Group
只能用于方法上,用来把测试方法进行归类。与之相关的@GroupThreads 注解,会在这个归类的基础上,再进行一些线程方面的设置。
@GroupThreads
@Timeout
@Param
只能修饰字段,用来测试不同的参数对程序性能的影响。配合@State注解,可以同时制定这些参数的执行范围。
@Setup和@TearDown
@Setup用于基准测试前的初始化动作,@TearDown用于基准测试后的销毁动作,都是用于添加一些全局的配置。初始化和销毁的动作都只会执行一次。这两个注解,同样有一个Level值,标明方法运行的时机,有三个取值:
- Trial:默认级别,Benchmark级别,包括Warmup和Measurement阶段,只会执行一次
- Iteration:每次迭代都会运行,如果Warmup和Measurement都配置2次执行的话,则@Setup和@TearDown配置的方法的执行次数为4次
- Invocation:每次方法调用都会运行,粒度最细
参考源码:
publicenumLevel{Trial,Iteration,Invocation,}
@CompilerControl
可用于控制方法内联,参考注解内部枚举类:
enumMode{BREAK("break"),PRINT("print"),EXCLUDE("exclude"),INLINE("inline"),DONT_INLINE("dontinline"),COMPILE_ONLY("compileonly"),;privatefinalString command;}
枚举项:
- BREAK:将断点插入到编译代码中
- PRINT:打印方法及其配置文件
- EXCLUDE:禁止方法编译
- INLINE:强制使用内联
- DONT_INLINE:强制不能使用方法内联
- COMPILE_ONLY:仅编译此方法
@AuxCounters
用于定义辅助计数器,这些计数器可以在基准测试方法执行过程中更新。JMH在报告基准测试结果时,会包括这些计数器的值。计数器可以用于跟踪诸如:
- 操作执行的次数
- 特定条件的发生次数
- 内部状态变化的次数
计数器的值独立于主测量数据,因此不会影响基准测试的主要性能指标(如时间、吞吐量等)的测量。
@OperationsPerInvocation
用于指定在一次Invocation中执行的Operation的次数,默认为1(见源码
int value() default 1;
)。在某些情况下,可能希望在每次Invocation中执行多次Operation:
- 减小测量的噪声:通过增加每次Invocation中操作的次数,可以减小每次Invocation的开销相对于操作时间的比例,从而获得更精确的测量结果。
- 批量操作:当一个操作的执行时间非常短时,将多个操作批量执行可以减少由于微小时间测量误差引入的噪声
概念
- Operation:一个基本操作单位,通常是基准测试代码中的一个方法调用或一组指令的执行
- Invocation:一次Operation的实际执行(具体执行实例),JMH将一个Operation分解为多个Invocation,以便更精细地控制测量的执行次数和时间
- Iteration:一组Invocation的集合。Iteration分为预热迭代和实际测量(Measurement)迭代
- Trial:一次完整的基准测试执行,包括多个Iteration。一个Trial可能包括多次预热Iteration和测量Iteration。每次Trial运行结果都会被记录下来
- Fork:JMH支持在一个新的JVM进程中启动基准测试,即一个Fork。Fork用于隔离不同试验之间的干扰,确保测试结果的独立性。Fork相当于新的JVM进程中的一个Trial。Fork实际上是多个Trial的一个容器,通常每个Fork都包含相同的Trial设置
- Warm-up:预热,因JIT机制的存在,如果某个函数被调用多次之后,JVM会尝试将其编译成为机器码从而提高执行速度。用于让JVM达到稳定状态,使得测试结果更加接近真实情况
- Measurement:实际测量迭代,用于收集性能数据
Operation和Invocation
Operation是定义的一组操作,而Invocation是这些操作的具体执行实例。可以理解为Operation是一个抽象的概念,而Invocation是Operation的具体执行。Invocation粒度更小,一个Operation可以被执行多次,每次执行称为一次Invocation。参考上面提到的@OperationsPerInvocation。
运行
Runner & OptionsBuilder
Runner的run方法即为启动基准测试,而启动测试前需要配置基准测试的Options信息,此配置信息可以通过OptionsBuilder来构建,OptionsBuilder即为构造器模式的实例。
输出结果
如GitHub上面的
JMHSample_01_HelloWorld
实例,输出如下:
BenchmarkModeCntScoreErrorUnitstestHashMapavgt5147.865± 81.128us/optestHashMapWithoutSizeavgt5224.897± 102.342us/op
分别定义两个基准测试的方法testHashMapWithoutSize和testHashMap,执行流程是:每个方法执行前都进行5次预热执行,每隔1秒进行一次预热操作,预热执行结束之后进行5次实际测量执行,每隔1秒进行一次实际执行,此次基准测试测量的是平均响应时长,单位是us。
导出
Options opt =newOptionsBuilder().resultFormat(ResultFormatType.JSON).build();
JMH支持以下5种格式的结果:
- TEXT:文本
- CSV:Comma Separated Values文件,可称为轻量级的Excel文件,可用Excel打开、预览、编辑
- SCSV:导出scsv等格式的文件,Sealed CSV
- JSON:最常见
- LATEX:一种基于ΤΕΧ的排版系统
一般可导出为CSV格式文件,然后借助于Excel进行图形分析。
CSV和SCSV
- CSV:与Excel兼容的文件,可使用Excel打开它,并将所有数据排序到列中。可在QuickScan程序中打开(查看),但不能编辑
- SCSV:可使用QuickScan程序打开和编辑。DataLog实际上是一个.scsv文件。可用Excel打开,但是数据未按列排序,需要手动排序
图形界面
将测试执行结果数据文件进行二次加工,进行图形化展示
- JMH Visualizer,开源工具,把执行结果JSON文件上传到此地址,即可得到
- jmh-visual-chart,开源工具,上传JSON文件即可
- meta-chart,开源工具,基于CSV来生成图形
IDEA插件
IDEA可安装JMH插件。
Jenkins插件
进阶
方法内联
如果JVM监测到一些小方法被频繁的执行,会进行JIT编译和内联优化,即把方法的调用替换成方法体本身:
privateintadd4(int x1,int x2,int x3,int x4){returnadd2(x1, x2)+add2(x3, x4);}privateintadd2(int x1,int x2){return x1 + x2;}
运行一段时间后JVM会把add2方法去掉,把代码翻译成:
privateintadd4(int x1,int x2,int x3,int x4){return x1 + x2 + x3 + x4;}
循环优化
虽然可以在Benchmark中定义循环逻辑,但这么做其实是不合适的,因为编译器可能会将循环进行展开或做一些其他方面的循环优化,JHM建议不要在Beanchmark中使用循环,可以结合
@BenchmarkMode(Mode.SingleShotTime)
和
@Measurement(batchSize = N)
来实现循环。
Blackhole
会消费传进来的值,不提供任何信息来确定这些值是否在之后被实际使用。Blackhole处理的事情主要有以下几种:
- 死代码消除:入参应该在每次都被用到,因此编译器就不会把这些参数优化为常量或者在计算的过程中对他们进行其他优化
- 处理内存壁:我们需要尽可能减少写的量,因为它会干扰缓存,污染写缓冲区等。 这很可能导致过早地撞到内存壁
生成类
实战类如下:
@State(Scope.Thread)@BenchmarkMode(Mode.AverageTime)@OutputTimeUnit(TimeUnit.NANOSECONDS)publicclassDateBenchMark{@BenchmarkpubliclongrunCalendar(){returnCalendar.getInstance().getTimeInMillis();}@BenchmarkpubliclongrunJoda(){returnnewDateTime().getMillis();}@BenchmarkpubliclongrunSystem(){returnSystem.currentTimeMillis();}publicstaticvoidmain(String[] args)throwsRunnerException{Options opt =newOptionsBuilder().include(DateBenchMark.class.getSimpleName()).forks(1).measurementIterations(3).measurementTime(TimeValue.milliseconds(1)).warmupIterations(3).warmupTime(TimeValue.seconds(1)).build();newRunner(opt).run();}
如果嫌OptionsBuilder链式赋值方式太麻烦,也可使用在类上加注解方式,少写几行代码:
@Fork(1)@Warmup(iterations =3, time =1)@Measurement(iterations =3, time =1, timeUnit =TimeUnit.MILLISECONDS)
在target目录下会生成一个包(截图放两个,注意区分):
上面截图里,generated是早期版本(具体不是很清楚)的JMH生成的,下面这个jhm_generated是JMH-1.37版本生成的。包下面会有若干个类,类的数量与类里面的测试方法数量有关:
不难发现,生产类的前缀都是JMH测试类,即前文的
DateBenchMark
。经过测试,有4个类即jmhType、B1、B2、B3是固定不变的。其中
<jmhClass>_jmhType_B1
内容如下:
publicclassDateBenchMark_jmhType_B1extendsbenchmark.DateBenchMark{// 256个byte,有省略byte b1_000,...,b1_255;}
其中
<jmhClass>_jmhType_B3
内容和B1一样,如下:
publicclassDateBenchMark_jmhType_B3extendsDateBenchMark_jmhType_B2{// 256个byte,有省略byte b3_000,...,b3_255;}
其中
<jmhClass>_jmhType_B2
内容如下:
publicclassDateBenchMark_jmhType_B2extendsDateBenchMark_jmhType_B1{publicvolatileint setupTrialMutex;publicvolatileint tearTrialMutex;publicfinalstaticAtomicIntegerFieldUpdater<DateBenchMark_jmhType_B2> setupTrialMutexUpdater =AtomicIntegerFieldUpdater.newUpdater(DateBenchMark_jmhType_B2.class,"setupTrialMutex");publicfinalstaticAtomicIntegerFieldUpdater<DateBenchMark_jmhType_B2> tearTrialMutexUpdater =AtomicIntegerFieldUpdater.newUpdater(DateBenchMark_jmhType_B2.class,"tearTrialMutex");publicvolatileint setupIterationMutex;publicvolatileint tearIterationMutex;publicfinalstaticAtomicIntegerFieldUpdater<DateBenchMark_jmhType_B2> setupIterationMutexUpdater =AtomicIntegerFieldUpdater.newUpdater(DateBenchMark_jmhType_B2.class,"setupIterationMutex");publicfinalstaticAtomicIntegerFieldUpdater<DateBenchMark_jmhType_B2> tearIterationMutexUpdater =AtomicIntegerFieldUpdater.newUpdater(DateBenchMark_jmhType_B2.class,"tearIterationMutex");publicvolatileint setupInvocationMutex;publicvolatileint tearInvocationMutex;publicfinalstaticAtomicIntegerFieldUpdater<DateBenchMark_jmhType_B2> setupInvocationMutexUpdater =AtomicIntegerFieldUpdater.newUpdater(DateBenchMark_jmhType_B2.class,"setupInvocationMutex");publicfinalstaticAtomicIntegerFieldUpdater<DateBenchMark_jmhType_B2> tearInvocationMutexUpdater =AtomicIntegerFieldUpdater.newUpdater(DateBenchMark_jmhType_B2.class,"tearInvocationMutex");}
代码初步解析:分别针对Trial、Iteration、Invocation三种Level枚举值,基于volatile和AtomicIntegerFieldUpdater,生成setupMutex和tearMutex,
而
<jmhClass>_jmhType
是一个空类:
publicclassDateBenchMark_jmhTypeextendsDateBenchMark_jmhType_B3{}
然后就是测试方法生成的类,有几个测试方法就有几个类,且生成类结果一模一样。其中
<jmhClass>_<methodName>_jmhTest
内容如下:
publicfinalclassDateBenchMark_runSystem_jmhTest{// 256个byte,有省略byte p000,...,p255;int startRndMask;BenchmarkParams benchmarkParams;IterationParams iterationParams;ThreadParams threadParams;Blackhole blackhole;Control notifyControl;// 1.方法名不一样publicBenchmarkTaskResultrunSystem_Throughput(InfraControl control,ThreadParams threadParams)throwsThrowable{this.benchmarkParams = control.benchmarkParams;this.iterationParams = control.iterationParams;this.threadParams = threadParams;this.notifyControl = control.notifyControl;if(this.blackhole ==null){this.blackhole =newBlackhole("Today's password is swordfish. I understand instantiating Blackholes directly is dangerous.");}if(threadParams.getSubgroupIndex()==0){RawResults res =newRawResults();DateBenchMark_jmhType l_datebenchmark0_0 =_jmh_tryInit_f_datebenchmark0_0(control);
control.preSetup();
control.announceWarmupReady();while(control.warmupShouldWait){
blackhole.consume(l_datebenchmark0_0.runSystem());if(control.shouldYield)Thread.yield();
res.allOps++;}
notifyControl.startMeasurement =true;// 2.调用方法不一样runSystem_thrpt_jmhStub(control, res, benchmarkParams, iterationParams, threadParams, blackhole, notifyControl, startRndMask, l_datebenchmark0_0);
notifyControl.stopMeasurement =true;
control.announceWarmdownReady();try{while(control.warmdownShouldWait){
blackhole.consume(l_datebenchmark0_0.runSystem());if(control.shouldYield)Thread.yield();
res.allOps++;}}catch(Throwable e){if(!(e instanceofInterruptedException))throw e;}
control.preTearDown();if(control.isLastIteration()){
f_datebenchmark0_0 =null;}
res.allOps += res.measuredOps;int batchSize = iterationParams.getBatchSize();int opsPerInv = benchmarkParams.getOpsPerInvocation();
res.allOps *= opsPerInv;
res.allOps /= batchSize;
res.measuredOps *= opsPerInv;
res.measuredOps /= batchSize;BenchmarkTaskResult results =newBenchmarkTaskResult((long)res.allOps,(long)res.measuredOps);// 3.添加的Result不一样
results.add(newThroughputResult(ResultRole.PRIMARY,"runSystem", res.measuredOps, res.getTime(), benchmarkParams.getTimeUnit()));this.blackhole.evaporate("Yes, I am Stephen Hawking, and know a thing or two about black holes.");return results;}elsethrownewIllegalStateException("Harness failed to distribute threads among groups properly");}// 4.仅定义的方法名不一样publicstaticvoidrunSystem_thrpt_jmhStub(InfraControl control,RawResults result,BenchmarkParams benchmarkParams,IterationParams iterationParams,ThreadParams threadParams,Blackhole blackhole,Control notifyControl,int startRndMask,DateBenchMark_jmhType l_datebenchmark0_0)throwsThrowable{long operations =0;long realTime =0;
result.startTime =System.nanoTime();do{
blackhole.consume(l_datebenchmark0_0.runSystem());
operations++;}while(!control.isDone);
result.stopTime =System.nanoTime();
result.realTime = realTime;
result.measuredOps = operations;}publicBenchmarkTaskResultrunSystem_AverageTime(InfraControl control,ThreadParams threadParams)throwsThrowable{}// 方法名不一样,参数一模一样,省略publicstaticvoidrunSystem_avgt_jmhStub()throwsThrowable{}publicBenchmarkTaskResultrunSystem_SampleTime(InfraControl control,ThreadParams threadParams)throwsThrowable{// 省略和方法runSystem_Throughput相同的赋值if(threadParams.getSubgroupIndex()==0){RawResults res =newRawResults();DateBenchMark_jmhType l_datebenchmark0_0 =_jmh_tryInit_f_datebenchmark0_0(control);
control.preSetup();
control.announceWarmupReady();while(control.warmupShouldWait){
blackhole.consume(l_datebenchmark0_0.runSystem());if(control.shouldYield)Thread.yield();
res.allOps++;}
notifyControl.startMeasurement =true;int targetSamples =(int)(control.getDuration(TimeUnit.MILLISECONDS)*20);// at max, 20 timestamps per millisecondint batchSize = iterationParams.getBatchSize();int opsPerInv = benchmarkParams.getOpsPerInvocation();SampleBuffer buffer =newSampleBuffer();runSystem_sample_jmhStub(control, res, benchmarkParams, iterationParams, threadParams, blackhole, notifyControl, startRndMask, buffer, targetSamples, opsPerInv, batchSize, l_datebenchmark0_0);
notifyControl.stopMeasurement =true;
control.announceWarmdownReady();try{while(control.warmdownShouldWait){
blackhole.consume(l_datebenchmark0_0.runSystem());if(control.shouldYield)Thread.yield();
res.allOps++;}}catch(Throwable e){if(!(e instanceofInterruptedException))throw e;}
control.preTearDown();if(control.isLastIteration()){
f_datebenchmark0_0 =null;}
res.allOps += res.measuredOps * batchSize;
res.allOps *= opsPerInv;
res.allOps /= batchSize;
res.measuredOps *= opsPerInv;BenchmarkTaskResult results =newBenchmarkTaskResult((long)res.allOps,(long)res.measuredOps);
results.add(newSampleTimeResult(ResultRole.PRIMARY,"runSystem", buffer, benchmarkParams.getTimeUnit()));// 省略相同的blackhole.evaporate及返回结果}// 省略相同的throw}publicstaticvoidrunSystem_sample_jmhStub(InfraControl control,RawResults result,BenchmarkParams benchmarkParams,IterationParams iterationParams,ThreadParams threadParams,Blackhole blackhole,Control notifyControl,int startRndMask,SampleBuffer buffer,int targetSamples,long opsPerInv,int batchSize,DateBenchMark_jmhType l_datebenchmark0_0)throwsThrowable{long realTime =0;long operations =0;int rnd =(int)System.nanoTime();int rndMask = startRndMask;long time =0;int currentStride =0;do{
rnd =(rnd *1664525+1013904223);boolean sample =(rnd & rndMask)==0;if(sample){
time =System.nanoTime();}for(int b =0; b < batchSize; b++){if(control.volatileSpoiler)return;
blackhole.consume(l_datebenchmark0_0.runSystem());}if(sample){
buffer.add((System.nanoTime()- time)/ opsPerInv);if(currentStride++> targetSamples){
buffer.half();
currentStride =0;
rndMask =(rndMask <<1)+1;}}
operations++;}while(!control.isDone);
startRndMask =Math.max(startRndMask, rndMask);
result.realTime = realTime;
result.measuredOps = operations;}publicBenchmarkTaskResultrunSystem_SingleShotTime(InfraControl control,ThreadParams threadParams)throwsThrowable{// 省略和方法runSystem_Throughput相同的赋值if(threadParams.getSubgroupIndex()==0){DateBenchMark_jmhType l_datebenchmark0_0 =_jmh_tryInit_f_datebenchmark0_0(control);
control.preSetup();
notifyControl.startMeasurement =true;RawResults res =newRawResults();int batchSize = iterationParams.getBatchSize();runSystem_ss_jmhStub(control, res, benchmarkParams, iterationParams, threadParams, blackhole, notifyControl, startRndMask, batchSize, l_datebenchmark0_0);
control.preTearDown();if(control.isLastIteration()){
f_datebenchmark0_0 =null;}int opsPerInv = control.benchmarkParams.getOpsPerInvocation();long totalOps = opsPerInv;BenchmarkTaskResult results =newBenchmarkTaskResult(totalOps, totalOps);
results.add(newSingleShotResult(ResultRole.PRIMARY,"runSystem", res.getTime(), totalOps, benchmarkParams.getTimeUnit()));// 省略相同的blackhole.evaporate及返回结果}// 省略相同的throw}publicstaticvoidrunSystem_ss_jmhStub(InfraControl control,RawResults result,BenchmarkParams benchmarkParams,IterationParams iterationParams,ThreadParams threadParams,Blackhole blackhole,Control notifyControl,int startRndMask,int batchSize,DateBenchMark_jmhType l_datebenchmark0_0)throwsThrowable{long realTime =0;
result.startTime =System.nanoTime();for(int b =0; b < batchSize; b++){if(control.volatileSpoiler)return;
blackhole.consume(l_datebenchmark0_0.runSystem());}
result.stopTime =System.nanoTime();
result.realTime = realTime;}DateBenchMark_jmhType f_datebenchmark0_0;DateBenchMark_jmhType_jmh_tryInit_f_datebenchmark0_0(InfraControl control)throwsThrowable{if(control.isFailing)thrownewFailureAssistException();DateBenchMark_jmhType val = f_datebenchmark0_0;if(val ==null){
val =newDateBenchMark_jmhType();
f_datebenchmark0_0 = val;}return val;}}
前面指明DateBenchMark使用
@BenchmarkMode(Mode.AverageTime)
,但生成类却对4个枚举值各生成2个方法,方法命名规则是
<methodName>_<Mode.name>
(方法名+枚举值)以及
<methodName>_<Mode.shortLabel>_jmhStub
(方法名+枚举.shortLabel字段+jmhStub)。
其中,
runSystem_Throughput
和
runSystem_AverageTime
几乎一模一样,
runSystem_thrpt_jmhStub
和
runSystem_avgt_jmhStub
几乎一模一样。
执行结果解读
参考
- JMH-Samples
- SCSV
版权归原作者 johnny233 所有, 如有侵权,请联系我们删除。