简介
JMH (Java Microbenchmark Harness) 是用于代码微基准测试的工具套件,主要是基于方法层面的基准测试,精度可以达到纳秒级。该工具是由 Oracle 内部实现 JIT 的大牛们编写的,他们应该比任何人都了解 JIT 以及 JVM 对于基准测试的影响。JMH 不止能对 Java 语言做基准测试,还能对运行在 JVM 上的其他语言做基准测试。
当你定位到热点方法,希望进一步优化方法性能的时候,就可以使用 JMH 对优化的结果进行量化的分析。
JMH 比较典型的应用场景如下:
- 想准确地知道某个方法需要执行多长时间,以及执行时间和输入之间的相关性
- 对比接口不同实现在给定条件下的吞吐量
- 查看多少百分比的请求在多长时间内完成
JMH基础
@BenchmarkMode
用来配置 Mode 选项,可用于类或者方法上,这个注解的 value 是一个数组,可以把几种 Mode 集合在一起执行,如:@BenchmarkMode({Mode.SampleTime, Mode.AverageTime})
,还可以设置为 Mode.All
,即全部执行一遍。
- Throughput:整体吞吐量,每秒执行了多少次调用,单位为
ops/time
- AverageTime:用的平均时间,每次操作的平均时间,单位为
time/op
- SampleTime:随机取样,最后输出取样结果的分布
- SingleShotTime:只运行一次,往往同时把 Warmup 次数设为 0,用于测试冷启动时的性能
- All:上面的所有模式都执行一次
@State
通过 State 可以指定一个对象的作用范围,JMH 根据 scope 来进行实例化和共享操作。@State 可以被继承使用,如果父类定义了该注解,子类则无需定义。由于 JMH 允许多线程同时执行测试,不同的选项含义如下:
- Scope.Benchmark:所有测试线程共享一个实例,测试有状态实例在多线程共享下的性能
- Scope.Group:同一个线程在同一个 group 里共享实例
- Scope.Thread:默认的 State,每个测试线程分配一个实例
@OutputTimeUnit
为统计结果的时间单位,可用于类或者方法注解
@Warmup
预热所需要配置的一些基本测试参数,可用于类或者方法上。一般前几次进行程序测试的时候都会比较慢,所以要让程序进行几轮预热,保证测试的准确性。参数如下所示:
- iterations:预热的次数
- time:每次预热的时间
- timeUnit:时间的单位,默认秒
- batchSize:批处理大小,每次操作调用几次方法
为什么需要预热?
因为 JVM 的 JIT 机制的存在,如果某个函数被调用多次之后,JVM 会尝试将其编译为机器码,从而提高执行速度,所以为了让 benchmark 的结果更加接近真实情况就需要进行预热。
@Measurement
实际调用方法所需要配置的一些基本测试参数,可用于类或者方法上,参数和 @Warmup
相同。
@Threads
每个进程中的测试线程,可用于类或者方法上。
@Fork
进行 fork 的次数,可用于类或者方法上。如果 fork 数是 2 的话,则 JMH 会 fork 出两个进程来进行测试。
@Param
指定某项参数的多种情况,特别适合用来测试一个函数在不同的参数输入的情况下的性能,只能作用在字段上,使用该注解必须定义 @State 注解。
添加依赖
1 | // https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-core |
编写测试代码
创建一个 JMH 测试类,用来判断 +
和 StringBuilder.append()
两种字符串拼接哪个耗时更短
1 | /** |
在 main () 函数中,首先对测试用例进行配置,使用 Builder 模式配置测试,将配置参数存入 Options 对象,并使用 Options 对象构造 Runner 启动测试。
结果分析
1 | # JMH version: 1.23 |
该部分为测试的基本信息,比如使用的 Java 路径,预热代码的迭代次数,测量代码的迭代次数,使用的线程数量,测试的统计单位等。
1 | # Warmup Iteration 1: 1083.569 ±(99.9%) 393.884 ns/op |
该部分为每一次热身中的性能指标,预热测试不会作为最终的统计结果。预热的目的是让 JVM 对被测代码进行足够多的优化,比如,在预热后,被测代码应该得到了充分的 JIT 编译和优化。
1 | Iteration 1: 810.667 ±(99.9%) 51.505 ns/op |
该部分显示测量迭代的情况,每一次迭代都显示了当前的执行速率,即一个操作所花费的时间。在进行 5 次迭代后,进行统计,在本例中,length 为 100 的情况下 testStringBuilderAdd
方法的平均执行花费时间为 819.329 ns
,误差为 72.698 ns
。
1 | Benchmark (length) Mode Cnt Score Error Units |
结果表明,在拼接字符次数越多的情况下,StringBuilder.append()
的性能就更好。
IDEA插件执行测试
通过 IDEA 安装 JMH 插件使 JMH 更容易实现基准测试,在 IDEA 中点击 File->Settings...->Plugins
,然后搜索 jmh,选择安装 JMH plugin。
这个插件能够以 JUnit 相同的方式使用 JMH,主要功能如下:
- 自动生成带有
@Benchmark
的方法 - 像 JUnit 一样,运行单独的 Benchmark 方法
- 运行类中所有的 Benchmark 方法
比如可以通过右键点击 Generate...
,选择操作 Generate JMH benchmark
就可以生成一个带有 @Benchmark
的方法。
还有将光标移动到方法声明并调用 Run 操作就运行一个单独的 Benchmark 方法。
将光标移到类名所在行,右键点击 Run 运行,该类下的所有被 @Benchmark
注解的方法都会被执行。
JMH可视化
除此以外,如果你想将测试结果以图表的形式可视化,可以试下这些网站:
JMH Visual Chart
:http://deepoove.com/jmh-visual-chart/JMH Visualizer
:https://jmh.morethan.io/
比如将上面测试例子结果的 json 文件导入,就可以实现可视化: