文章目录
前言
JMH(Java Microbenchmark Harness),是 OpenJDK 团队开发的一款基准测试工具,一般用于代码的性能比较和调优,精度甚至可以达到纳秒级别,适用于 java 以及其他基于 JVM 的语言。
github地址:https://github.com/openjdk/jmh
官方使用例子:https://github.com/openjdk/jmh/tree/master/jmh-samples/src/main/java/org/openjdk/jmh/samples
pom 依赖:
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.35</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.35</version>
</dependency>
一、配置参数
1.1 注解列表
注解名 | 作用域 | 作用 |
---|---|---|
AuxCounters | TYPE | 辅助计数器,可以统计 @State 修饰的对象中的 public 属性被执行的情况。实验性API,将来可能删除 |
Benchmark | METHOD | 标记为基准测试方法,和 junit @Test 类似 |
BenchmarkMode | TYPE,METHOD | 指明了基准测试的模式, 模式可以任意组合,详细见下方 |
CompilerControl | TYPE,METHOD,CONSTRUCTOR | 编译控制选项,是否使用编译优化 |
Fork | TYPE,METHOD | fork出 jvm 子进程进行测试,一般设置为1 |
Group | METHOD | 控制多线程组 |
GroupThreads | METHOD | 设置参与组的线程数量 |
Measurement | TYPE,METHOD | 设置默认测量参数 |
OperationsPerInvocation | TYPE,METHOD | 设置单个 benchmark 方法 op 个数(默认1个benchmark一个op) |
OutputTimeUnit | TYPE.METHOD | 指定输出的时间单位,可以传入 java.util.concurrent.TimeUnit 中的时间单位,最小可以到纳秒级别 |
Param | FIELD | 允许使用一个 Benchmark 方法跑多组数据,特别适合测量方法性能和参数取值的关系 |
Setup | METHOD | 用于基准测试前的初始化动作,可通过参数 level 确定粒度, 具体见下方 |
State | TYPE | 声明某个类是一个“状态”,然后接受一个 Scope 参数用来表示该状态的共享范围, 当使用 @Setup 的时候,必须在类上加这个参数,不然会提示无法运行。参数设置见下方 |
TearDown | METHOD | 用于基准测试后的动作 |
Threads | TYPE,METHOD | 设置线程数 |
Timeout | TYPE,METHOD | 设置默认超时参数,java.util.concurrent.TimeUnit |
Warmup | TYPE.METHOD | 设置默认预热参数,详细见下方 |
-
AuxCounters
- Type.EVENTS: 统计发生的次数
- Type.OPERATIONS:按指定的格式统计,如按吞吐量统计
-
BenchmarkMode
- Mode.Throughput :吞吐量,单位时间内执行的次数,默认值
- Mode.AverageTime:平均时间,一次执行需要的单位时间,其实是吞吐量的倒数
- Mode.SampleTime:是基于采样的执行时间,采样频率由JMH自动控制,同时结果中也会统计出p90、p95的时间
- Mode.SingleShotTime:单次执行时间,只执行一次,可用于冷启动的测试
-
CompilerControl
-
Mode.BREAK:在生成的编译代码插入断点
-
Mode.PRINT:打印方法及配置文件
-
Mode.EXCLUDE:从编译中排除该方法
-
Mode.INLINE:强制使用内联
-
Mode.DONT_INLINE:强制跳过内联
-
Mode.COMPILE_ONLY:仅仅编译方法,其它啥都不干
-
-
Measurement
- iterations:测量迭代次数,不是方法执行次数
- time:每次迭代时间
- timeUnit:时间单位
- batchSize:一次迭代方法需要执行次数
-
Setup/TearDown
- Level.Trial:Benchmark级别
- Level.Iteration:执行迭代级别
- Level.Invocation:每次方法调用级别
-
State
- Scope.Thread:作用域为线程
- Scope.Benchmark:作用域为本次JMH测试,线程共享
- Scope.Group:作用域为组
-
WarmUp
预热是因为 JVM 的 JIT 机制的存在,如果某个函数被调用多次之后,JVM 会尝试将其编译成为机器码从而提高执行速度。为了让 benchmark 的结果更加接近真实情况就需要进行预热。
- iterations:预热迭代次数
- time:每次迭代时间
- timeUnit:时间单位
- batchSize:一次迭代方法需要执行次数
1.2 非注解配置
除了上述使用注解进行配置,还有一些参数可以通过 OptionsBuilder 这个类进行配置
- include 配置参与基准测试的类,参数是类的简单名称,不包含包名
- exclude 排除的方法名,include 会默认导入所有 @Benchmark public 方法
- addProfiler 添加分析器,能够得到更多关于 jvm 的信息。jmh 自身提供了很多分析器:如 GCProfiler, StackProfiler, ClassloaderProfiler 等等
- detectJvmArgs 从父jvm检测参数,会覆盖jvmArgs
- jvmArgs fork jvm 参数
- shouldDoGC 在mesurement 迭代之间是否GC
- verbosity 控制输出信息的级别
1.3 概念
- JMH使用OPS来表示吞吐量,OPS,Opeartion Per Second,是衡量性能的重要指标,指得是每秒操作量。数值越大,性能越好。类似的概念还有TPS,表示每秒的事务完成量,QPS,每秒的查询量。
- 如果对每次执行时间进行升序排序,取出总数的99%的最大执行时间作为 p99 的值,p99 通常是衡量系统性能重要指标,表示99%的请求的响应时间不超过某个值,类似的还有p95,p90, p999
- 测试时间,测试时间 = (测试方法数量) * (warmup迭代次数 * 时间 + measurement迭代次数 * 时间) * (@Param参数个数的乘积) * (forks)
二、简单例子
2.1 throught(吞吐量)
先用一个最简单的例子做测试, 计算 testThrought 这个方法1s执行多少次,就是计算吞吐量
package com.aabond.demo.jmh;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
public class JmhDemo {
@Benchmark
public void testThrought() throws InterruptedException {
Thread.sleep(1000);
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(JmhDemo.class.getSimpleName())
.forks(1)
.build();
new Runner(opt).run();
}
}
Benchmark Mode Cnt Score Error Units
JmhDemo.testThrought thrpt 5 0.991 ± 0.004 ops/s
代码中,可以看出 testThrought 肯定会耗费1s左右的时间,结果正如所预料的一样。
上述代码只是用了最基本的默认配置,更多参数配置可以通过注解和代码来控制。
- 默认的预热和迭代次数都是5,可以用@Warmup和@Measurement来自定义
- 默认输出时间单位是秒,也可以用@OutputTimeUnit 实现显示其它单位
- 默认基准测试是输出吞吐量,可以用@BenchmarkMode 设置平均时间,这两者互为倒数
@Warmup(iterations = 3)
@Measurement(iterations = 6)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@BenchmarkMode(value = Mode.AverageTime)
public class JmhDemo {
@Benchmark
public void testThrought() throws InterruptedException {
Thread.sleep(1000);
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(JmhDemo.class.getSimpleName())
.forks(1)
.build();
new Runner(opt).run();
}
}
Benchmark Mode Cnt Score Error Units
JmhDemo.testThrought avgt 6 1009.491 ± 4.268 ms/op
2.2 ArrayList vs Set
ArrayList和 Set 的查找时间复杂度分别是 O ( n ) O(n) O(n)和 O ( 1 ) O(1) O(1),利用 jmh 测试一下差距
使用 datafaker 准备100,1000,10000个中文姓名字符串, 再用100个随机中文姓名字符串进行查找,用这种方法进行测试
@Warmup(iterations = 3)
@Measurement(iterations = 6)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Benchmark)
@BenchmarkMode(value = Mode.AverageTime)
public class JmhDemo {
List<String> names = new ArrayList<>();
Set<String> namesSet = new HashSet<>();
List<String> finds = new ArrayList<>();
@Param({"100", "1000", "10000"})
int originLen;
@Setup
public void setUp() {
Faker faker = new Faker(Locale.CHINA);
names = faker.collection(() -> faker.name().name()).len(originLen).generate();
namesSet = new HashSet<>(names);
finds = faker.collection(() -> faker.name().name()).len(100).generate();
}
@Benchmark
@OperationsPerInvocation(100)
public boolean listFind() {
for (String name: finds) {
boolean b = names.contains(name);
}
return true;
}
@Benchmark
@OperationsPerInvocation(100)
public boolean setFind() {
for (String name: finds) {
boolean b = namesSet.contains(name);
}
return true;
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(JmhDemo.class.getSimpleName())
.forks(1)
.build();
new Runner(opt).run();
}
}
Benchmark (originLen) Mode Cnt Score Error Units
JmhDemo.listFind 100 avgt 6 ≈ 10⁻⁴ ms/op
JmhDemo.listFind 1000 avgt 6 0.004 ± 0.001 ms/op
JmhDemo.listFind 10000 avgt 6 0.063 ± 0.006 ms/op
JmhDemo.setFind 100 avgt 6 ≈ 10⁻⁵ ms/op
JmhDemo.setFind 1000 avgt 6 ≈ 10⁻⁵ ms/op
JmhDemo.setFind 10000 avgt 6 ≈ 10⁻⁵ ms/op
从结果可以看出,List 随着数据量的增大查找的速度逐渐变慢,数据量从 1 0 2 10^2 102-> 1 0 3 10^3 103-> 1 0 4 10^4 104, 查找耗费时间 1 0 − 4 10^{-4} 10−4-> 1 0 − 3 10^{-3} 10−3-> 1 0 − 2 10^{-2} 10−2
而 set 一直保持不变,保持在 1 0 − 5 10^{-5} 10−5
2.3 StringBuilder vs StringBuffer
StringBuilder 和 StringBuffer 都可以用来拼接字符串。一个是线程不安全的,而另一个是线程安全的。实际用 StringBuilder 用的比较多,想知道这两者的差异,用 jmh 来比较下速度。
@Warmup(iterations = 3, time = 5)
@Measurement(iterations = 6, time = 5)
@Fork(1)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Benchmark)
@BenchmarkMode(value = Mode.AverageTime)
public class JmhDemo {
List<String> names = new ArrayList<>();
@Param({"1000", "100000", "10000000"})
int originLen;
@Setup
public void setUp() {
Faker faker = new Faker(Locale.CHINA);
names = faker.collection(() -> faker.name().name()).len(originLen).generate();
}
@Benchmark
public void stringBufferAppend(Blackhole bh) {
StringBuffer sb = new StringBuffer();
for (String name: names) {
sb.append(name);
}
bh.consume(sb);
}
@Benchmark
public void stringBuilderAppend(Blackhole bh) {
StringBuilder sb = new StringBuilder();
for (String name: names) {
sb.append(name);
}
bh.consume(sb);
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(JmhDemo.class.getSimpleName())
.build();
new Runner(opt).run();
}
}
Benchmark (originLen) Mode Cnt Score Error Units
JmhDemo.stringBufferAppend 1000 avgt 6 0.016 ± 0.002 ms/op
JmhDemo.stringBufferAppend 100000 avgt 6 1.635 ± 0.363 ms/op
JmhDemo.stringBufferAppend 10000000 avgt 6 162.367 ± 3.866 ms/op
JmhDemo.stringBuilderAppend 1000 avgt 6 0.015 ± 0.003 ms/op
JmhDemo.stringBuilderAppend 100000 avgt 6 1.701 ± 1.013 ms/op
JmhDemo.stringBuilderAppend 10000000 avgt 6 150.966 ± 4.673 ms/op
从结果看,StringBuffer和StringBuilder 拼接字符串的效率相差不大
2.4 Stream vs parallelStream vs for
@Warmup(iterations = 3, time = 5)
@Measurement(iterations = 6, time = 5)
@Fork(1)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Benchmark)
@BenchmarkMode(value = Mode.AverageTime)
public class JmhDemo {
@Param({"100000", "1000000", "10000000", "100000000"})
int originLen;
@Benchmark
public void streamGenerate(Blackhole bh) {
int[] array = IntStream.range(0, originLen).toArray();
bh.consume(array);
}
@Benchmark
public void streamParallelGenerate(Blackhole bh) {
int[] array = IntStream.range(0, originLen).parallel().toArray();
bh.consume(array);
}
@Benchmark
public void forGenerate(Blackhole bh) {
int [] array = new int[originLen];
for (int i = 0; i < originLen; i++) {
array[i] = i;
}
bh.consume(array);
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(JmhDemo.class.getSimpleName())
.result("E:\\list.json")
.resultFormat(ResultFormatType.JSON)
.build();
new Runner(opt).run();
}
}
Benchmark (originLen) Mode Cnt Score Error Units
JmhDemo.forGenerate 100000 avgt 6 0.068 ± 0.015 ms/op
JmhDemo.forGenerate 1000000 avgt 6 0.653 ± 0.105 ms/op
JmhDemo.forGenerate 10000000 avgt 6 6.581 ± 1.462 ms/op
JmhDemo.forGenerate 100000000 avgt 6 85.288 ± 104.922 ms/op
JmhDemo.streamGenerate 100000 avgt 6 0.098 ± 0.013 ms/op
JmhDemo.streamGenerate 1000000 avgt 6 0.916 ± 0.456 ms/op
JmhDemo.streamGenerate 10000000 avgt 6 26.783 ± 3.412 ms/op
JmhDemo.streamGenerate 100000000 avgt 6 191.738 ± 173.429 ms/op
JmhDemo.streamParallelGenerate 100000 avgt 6 0.104 ± 0.004 ms/op
JmhDemo.streamParallelGenerate 1000000 avgt 6 0.622 ± 0.022 ms/op
JmhDemo.streamParallelGenerate 10000000 avgt 6 23.461 ± 11.200 ms/op
JmhDemo.streamParallelGenerate 100000000 avgt 6 65.758 ± 44.910 ms/op
从结果看, for的消耗时间随着数据量增大而同比增大,成正比关系。而在千万数据上,流的性能突然下降,数据在亿级别,并行流性能更好
三、结果可视化
jmh 可通过将结果导出json数据
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(JmhDemo.class.getSimpleName())
.result("E:\\list.json")
.resultFormat(ResultFormatType.JSON)
.build();
new Runner(opt).run();
}
可以将Json数据上传两个网站,将结果可视化
四、IDEA插件
IDEA 提供插件 JMH Java Microbenchmark Harness,能够使用快捷键 Alt+Insert
或 MacOS Ctrl + N
快速生成测试方法,还可以执行单个方法,类似 junit