什么是JMH
JMH(Java Microbenchmark Harness)是用于代码微基准测试的工具套件,主要是基于方法层面的基准测试,精度可以达到纳秒级。该工具是由 Oracle 内部实现 JIT 的大牛们编写的,他们应该比任何人都了解 JIT 以及 JVM 对于基准测试的影响。
官网: http://openjdk.java.net/projects/code-tools/jmh/
当你定位到热点方法,希望进一步优化方法性能的时候,就可以使用 JMH 对优化的结果进行量化的分析。
JMH 比较典型的应用场景如下:
- 想准确地知道某个方法需要执行多长时间,以及执行时间和输入之间的相关性
- 对比接口不同实现在给定条件下的吞吐量
- 查看多少百分比的请求在多长时间内完成
下面我们以forEach和parallelStream两种方法遍历List为例子使用 JMH 做基准测试。
创建JMH测试工程
1. 创建Maven项目,添加依赖
<dependencies>
<!-- https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-core -->
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.21</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-generator-annprocess -->
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.21</version>
<scope>test</scope>
</dependency>
</dependencies>
2. idea安装JMH插件 JMH plugin v1.0.3
3. 由于用到了注解,打开运行程序注解配置
compiler -> Annotation Processors -> Enable Annotation Processing
4. 创建一个 JMH 测试类
创建测试类TestJMH 用来判断 forEach 和 parallelStream() 两种遍历方式哪个耗时更短,具体代码如下所示:
@BenchmarkMode(Mode.AverageTime)//
@Warmup(iterations = 3, time = 1)// 先预热3轮
@Measurement(iterations = 5, time = 5)// 进行5轮测试
@Threads(4)//4线程,同步迭代
@Fork(1)// Fork进行的数目
@State(value = Scope.Benchmark)// 每个Benchmark分配一个实例
@OutputTimeUnit(TimeUnit.MILLISECONDS)//结果所使用的时间单位:毫秒
public class TestJMH {
@Param({"100","1000", "10000"}) // 定义3个参数,之后会分别对这3个参数进行测试
private int n;
static List<Integer> nums = new ArrayList<>();
@Setup(Level.Trial) // 初始化方法,在全部Benchmark运行之前进行
public void init() {
Random r = new Random();
for (int i= 0;i<n;i++)nums.add(1000000+r.nextInt(1000000));
}
@Benchmark
public void foreach(){
nums.forEach(v->isPrime(v));
}
@Benchmark
public void parallel(){
nums.parallelStream().forEach(JMH_01::isPrime);
}
static boolean isPrime(int num){
for(int i=2; i<=num/2; i++) {
if(num % i == 0) return false;
}
return true;
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(TestJMH.class.getSimpleName())
.result("result.json")
.resultFormat(ResultFormatType.JSON).build();
new Runner(opt).run();
}
}
5. 运行测试类
运行过程中遇到下面的错误:
ERROR: org.openjdk.jmh.runner.RunnerException: ERROR: Exception while trying to acquire the JMH lock (C:\WINDOWS\/jmh.lock): C:\WINDOWS\jmh.lock (拒绝访问。), exiting. Use -Djmh.ignoreLock=true to forcefully continue.
at org.openjdk.jmh.runner.Runner.run(Runner.java:216)
at org.openjdk.jmh.Main.main(Main.java:71)
从日志中可以看出这个错误是因为JMH运行需要访问系统的TMP目录,解决办法是:
打开RunConfiguration -> Environment Variables -> include system environment viables
6. 阅读测试报告
# testForEach方法参数为10000时的测试结果
# JMH version: 1.21
# VM version: JDK 1.8.0_181, Java HotSpot(TM) 64-Bit Server VM, 25.181-b13
# VM invoker: D:\Program Files\Java\jdk1.8.0_181\jre\bin\java.exe
# VM options: -javaagent:D:\Program Files\JetBrains\IntelliJ IDEA 2019.3.1\lib\idea_rt.jar=59425:D:\Program Files\JetBrains\IntelliJ IDEA 2019.3.1\bin -Dfile.encoding=UTF-8
# Warmup: 3 iterations, 1 s each
# Measurement: 5 iterations, 5 s each
# Timeout: 10 min per iteration
# Threads: 4 threads, will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: com.jmh.TestJMH.foreach
# Parameters: (n = 10000)
# Run progress: 33.33% complete, ETA 00:02:00
# Fork: 1 of 1
# Warmup Iteration 1: 2246.782 ±(99.9%) 140.714 ms/op
# Warmup Iteration 2: 2186.002 ±(99.9%) 575.266 ms/op
# Warmup Iteration 3: 2482.109 ±(99.9%) 276.591 ms/op
Iteration 1: 2238.171 ±(99.9%) 288.741 ms/op
Iteration 2: 2395.052 ±(99.9%) 311.287 ms/op
Iteration 3: 2223.965 ±(99.9%) 133.162 ms/op
Iteration 4: 2246.373 ±(99.9%) 242.648 ms/op
Iteration 5: 2421.587 ±(99.9%) 498.462 ms/op
Result "com.jmh.TestJMH.foreach":
2305.030 ±(99.9%) 366.175 ms/op [Average]
(min, avg, max) = (2223.965, 2305.030, 2421.587), stdev = 95.095
CI (99.9%): [1938.854, 2671.205] (assumes normal distribution)
# testParallel方法参数为10000时的测试结果
# JMH version: 1.21
# VM version: JDK 1.8.0_181, Java HotSpot(TM) 64-Bit Server VM, 25.181-b13
# VM invoker: D:\Program Files\Java\jdk1.8.0_181\jre\bin\java.exe
# VM options: -javaagent:D:\Program Files\JetBrains\IntelliJ IDEA 2019.3.1\lib\idea_rt.jar=59425:D:\Program Files\JetBrains\IntelliJ IDEA 2019.3.1\bin -Dfile.encoding=UTF-8
# Warmup: 3 iterations, 1 s each
# Measurement: 5 iterations, 5 s each
# Timeout: 10 min per iteration
# Threads: 4 threads, will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: com.jmh.TestJMH.parallel
# Parameters: (n = 10000)
# Run progress: 83.33% complete, ETA 00:00:36
# Fork: 1 of 1
# Warmup Iteration 1: 1776.363 ±(99.9%) 2714.679 ms/op
# Warmup Iteration 2: 1599.224 ±(99.9%) 403.961 ms/op
# Warmup Iteration 3: 1843.330 ±(99.9%) 2049.332 ms/op
Iteration 1: 1652.597 ±(99.9%) 1265.922 ms/op
Iteration 2: 1663.783 ±(99.9%) 1086.725 ms/op
Iteration 3: 1645.228 ±(99.9%) 841.788 ms/op
Iteration 4: 1736.031 ±(99.9%) 2900.647 ms/op
Iteration 5: 1637.706 ±(99.9%) 1329.241 ms/op
Result "com.jmh.TestJMH.parallel":
1667.069 ±(99.9%) 153.001 ms/op [Average]
(min, avg, max) = (1637.706, 1667.069, 1736.031), stdev = 39.734
CI (99.9%): [1514.068, 1820.070] (assumes normal distribution)
# 汇总结果
# Run complete. Total time: 00:02:48
Benchmark (n) Mode Cnt Score Error Units
TestJMH.foreach 100 avgt 5 15.150 ± 4.440 ms/op
TestJMH.foreach 1000 avgt 5 192.093 ± 41.756 ms/op
TestJMH.foreach 10000 avgt 5 2305.030 ± 366.175 ms/op
TestJMH.parallel 100 avgt 5 19.001 ± 1.687 ms/op
TestJMH.parallel 1000 avgt 5 196.770 ± 37.387 ms/op
TestJMH.parallel 10000 avgt 5 1667.069 ± 153.001 ms/op
Benchmark result is saved to result.json
Process finished with exit code 0
从最终汇总结果分析得出:在List集合size越大时 parallel 比 foreach 性能更好。
JMH可视化
为了更加直观的查看测试结果,我们可以借助可视化工具JMH Visual chart 进行可视化
GitHub地址:https://github.com/Sayi/jmh-visual-chart
导入测试生成的结果数据result.json
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
为统计结果的时间单位,可用于类或者方法注解,最小可以精确到NANOSECONDS(纳秒)级别
@Warmup
预热所需要配置的一些基本测试参数,可用于类或者方法上。一般前几次进行程序测试的时候都会比较慢,所以要让程序进行几轮预热,保证测试的准确性。参数如下所示:
- iterations:预热的次数
- time:每次预热的时间
- timeUnit:时间的单位,默认秒
- batchSize:批处理大小,每次操作调用几次方法
为什么需要预热?
因为 JVM 的 JIT 机制的存在,如果某个函数被调用多次之后,JVM 会尝试将其编译为机器码,从而提高执行速度,所以为了让 benchmark 的结果更加接近真实情况就需要进行预热。
@Measurement
实际调用方法的次数,可用于类或者方法上,参数和 @Warmup 相同。
@Threads
每个进程中的测试线程,可用于类或者方法上。
@Fork
进行 fork 的次数,可用于类或者方法上。如果 fork 数是 2 的话,则 JMH 会 fork 出两个进程来进行测试。
@Param
指定某项参数的多种情况,特别适合用来测试一个函数在不同的参数输入的情况下的性能,只能作用在字段上,使用该注解必须定义 @State 注解。
总结
本文主要从概念和简单样例介绍了性能基准测试工具 JMH,它可以通过对某个方法或者类进行精准测试来规避由 JVM 中的 JIT 或者其他优化对性能测试造成的影响。
只需要将待测的业务逻辑用 @Benchmark 注解标识,就可以让 JMH 的注解处理器自动生成真正的性能测试代码,以及相应的性能测试结果,个人觉得JMH在性能测试场景下是一个非常简单实用的工具。