accurate-builder-equipment-1573821.jpg 前言
"If you cannot measure it, you cannot improve it".
"If you cannot measure it, you cannot improve it".
在日常开发中,我们对一些代码的调用或者工具的使用会存在多种选择方式,在不确定他们性能的时候,我们首先想要做的就是去测量它。大多数时候,我们会简单的采用多次计数的方式来测量,来看这个方法的总耗时。
image.png
也就是说,JVM会不断的进行编译优化,这就使得很难确定重复多少次才能得到一个稳定的测试结果?所以,很多有经验的同学会在测试代码前写一段预热的逻辑。
JMH,全称 Java Microbenchmark Harness (微基准测试框架),是专门用于Java代码微基准测试的一套测试工具API,是由 OpenJDK/Oracle 官方发布的工具。何谓 Micro Benchmark 呢?简单地说就是在 method 层面上的 benchmark,精度可以精确到微秒级。
Java的基准测试需要注意的几个点:
测试前需要预热。
防止无用代码进入测试方法中。
并发测试。
测试结果呈现。
测试前需要预热。
防止无用代码进入测试方法中。
并发测试。
测试结果呈现。
JMH的使用场景:
定量分析某个热点函数的优化效果
想定量地知道某个函数需要执行多长时间,以及执行时间和输入变量的相关性
对比一个函数的多种实现方式
定量分析某个热点函数的优化效果
想定量地知道某个函数需要执行多长时间,以及执行时间和输入变量的相关性
对比一个函数的多种实现方式
本篇主要是介绍JMH的DEMO演示,和常用的注解参数。希望能对你起到帮助。
DEMO 演示
这里先演示一个DEMO,让不了解JMH的同学能够快速掌握这个工具的大概用法。
1. 测试项目构建
JMH是内置Java9及之后的版本。这里是以Java8进行说明。
为了方便,这里直接介绍使用maven构建JMH测试项目的方式。
第一种是使用命令行构建,在指定目录下执行以下命令:
$ mvn archetype:generate \
-DinteractiveMode=false \
-DarchetypeGroupId=org.openjdk.jmh \
-DarchetypeArtifactId=jmh-java-benchmark-archetype \
-DgroupId=org.sample \
-DartifactId=test \
-Dversion=1.0
复制代码
对应目录下会出现一个test项目,打开项目后我们会看到这样的项目结构。
image.png
第二种方式就是直接在现有的maven项目中添加 jmh-core 和 jmh-generator-annprocess 的依赖来集成JMH。
< dependency>
< groupId> org.openjdk.jmh </ groupId>
< artifactId> jmh-core </ artifactId>
< version> ${jmh.version} </ version>
</ dependency>
< dependency>
< groupId> org.openjdk.jmh </ groupId>
< artifactId> jmh-generator-annprocess </ artifactId>
< version> ${jmh.version} </ version>
< scope> provided </ scope>
</ dependency>
复制代码
2. 编写性能测试
这里我以测试LinkedList 通过index 方式迭代和foreach 方式迭代的性能差距为例子,编写测试类,涉及到的注解在之后会讲解,
/**
* @authorRichard_yyf
* @version1.0 2019/8/27
*/
@State(Scope.Benchmark)
@OutputTimeUnit(TimeUnit.SECONDS)
@Threads(Threads.MAX)
publicclassLinkedListIterationBenchMark{
privatestaticfinalintSIZE = 10000;
privateList<String> list = newLinkedList<>;
@Setup
publicvoidsetUp{
for( inti = 0; i < SIZE; i++) {
list.add(String.valueOf(i));
}
}
@Benchmark
@BenchmarkMode(Mode.Throughput)
publicvoidforIndexIterate{
for( inti = 0; i < list.size; i++) {
list.get(i);
System.out.print( "");
}
}
@Benchmark
@BenchmarkMode(Mode.Throughput)
publicvoidforEachIterate{
for(String s : list) {
System.out.print( "");
}
}
}
复制代码
3. 执行测试
运行 JMH基准测试有两种方式,一个是生产jar文件运行,另一个是直接写main函数或者放在单元测试中执行。
生成jar文件的形式主要是针对一些比较大的测试,可能对机器性能或者真实环境模拟有一些需求,需要将测试方法写好了放在linux环境执行。具体命令如下
$mvn clean install
$java -jar target/benchmarks.jar
复制代码
我们日常中遇到的一般是一些小测试,比如我上面写的例子,直接在IDE中跑就好了。启动方式如下:
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder
.include(LinkedListIterationBenchMark.class.getSimpleName)
.forks(1)
.warmupIterations(2)
.measurementIterations(2)
.output("E:/Benchmark.log")
.build;
new Runner(opt).run;
}
复制代码
4. 报告结果
输出结果如下,
最后的结果:
Benchmark Mode Cnt Score Error Units
LinkedListIterationBenchMark.forEachIterate thrpt 2 1192.380 ops/s
LinkedListIterationBenchMark.forIndexIterate thrpt 2 206.866 ops/s
复制代码
整个过程:
# Detecting actual CPU count: 12 detected
# JMH version: 1.21
# VM version: JDK 1.8.0_131, Java HotSpot(TM) 64-Bit Server VM, 25.131-b11
# VM invoker: C:\Program Files\Java\jdk1.8.0_131\jre\bin\java.exe
# VM options: -javaagent:D:\Program Files\JetBrains\IntelliJ IDEA 2018.2.2\lib\idea_rt.jar=65175:D:\Program Files\JetBrains\IntelliJ IDEA 2018.2.2\bin -Dfile.encoding=UTF-8
# Warmup: 2 iterations, 10 s each
# Measurement: 2 iterations, 10 s each
# Timeout: 10 min per iteration
# Threads: 12 threads, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: org.sample.jmh.LinkedListIterationBenchMark.forEachIterate
# Run progress: 0.00% complete, ETA 00:01:20
# Fork: 1 of 1
# Warmup Iteration 1: 1189.267 ops/s
# Warmup Iteration 2: 1197.321 ops/s
Iteration 1:1193.062ops/s
Iteration 2:1191.698ops/s
Result"org.sample.jmh.LinkedListIterationBenchMark.forEachIterate":
1192.380ops/s
# JMH version: 1.21
# VM version: JDK 1.8.0_131, Java HotSpot(TM) 64-Bit Server VM, 25.131-b11
# VM invoker: C:\Program Files\Java\jdk1.8.0_131\jre\bin\java.exe
# VM options: -javaagent:D:\Program Files\JetBrains\IntelliJ IDEA 2018.2.2\lib\idea_rt.jar=65175:D:\Program Files\JetBrains\IntelliJ IDEA 2018.2.2\bin -Dfile.encoding=UTF-8
# Warmup: 2 iterations, 10 s each
# Measurement: 2 iterations, 10 s each
# Timeout: 10 min per iteration
# Threads: 12 threads, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: org.sample.jmh.LinkedListIterationBenchMark.forIndexIterate
# Run progress: 50.00% complete, ETA 00:00:40
# Fork: 1 of 1
# Warmup Iteration 1: 205.676 ops/s
# Warmup Iteration 2: 206.512 ops/s
Iteration 1:206.542ops/s
Iteration 2:207.189ops/s
Result"org.sample.jmh.LinkedListIterationBenchMark.forIndexIterate":
206.866ops/s
# Run complete. Total time: 00:01:21
REMEMBER:Thenumbersbelowarejustdata.Togainreusableinsights,youneedtofollowupon
whythenumbersarethewaytheyare.Useprofilers(see-prof,-lprof),designfactorial
experiments,performbaselineandnegativeteststhatprovideexperimentalcontrol,makesure
thebenchmarkingenvironmentissafeonJVM/OS/HWlevel,askforreviewsfromthedomainexperts.
Donotassumethenumberstellyouwhatyouwantthemtotell.
BenchmarkModeCntScoreErrorUnits
LinkedListIterationBenchMark.forEachIteratethrpt21192.380ops/s
LinkedListIterationBenchMark.forIndexIteratethrpt2206.866ops/s
复制代码
注解介绍
下面我们来详细介绍一下相关的注解,
@BenchmarkMode
微基准测试类型。 JMH提供了以下几种类型进行支持:
类型 | 描述 |
---|---|
Throughput | 每段时间执行的次数,一般是秒 |
AverageTime | 平均时间,每次操作的平均耗时 |
SampleTime | 在测试中,随机进行采样执行的时间 |
SingleShotTime | 在每次执行中计算耗时 |
All | 所有模式 |
可以注释在方法级别,也可以注释在类级别,
@BenchmarkMode(Mode.All)
public class LinkedListIterationBenchMark {
...
}
@Benchmark
@BenchmarkMode({Mode.Throughput, Mode.SingleShotTime})
public void m {
...
}
复制代码
@Warmup
这个单词的意思就是预热, iterations = 3 就是指预热轮数。
@Benchmark
@BenchmarkMode({Mode.Throughput, Mode.SingleShotTime})
@Warmup(iterations = 3)
public void m {
...
}
复制代码
@Measurement
正式度量计算的轮数。
iterations 进行测试的轮次
time 每轮进行的时长
timeUnit 时长单位
iterations 进行测试的轮次
time 每轮进行的时长
timeUnit 时长单位
@BenchmarkMode({Mode.Throughput, Mode.SingleShotTime})
@Measurement(iterations = 3)
public void m {
...
}
复制代码
@Threads
每个进程中的测试线程。
@Threads(Threads.MAX)
publicclassLinkedListIterationBenchMark{
...
}
复制代码
@Fork
进行 fork 的次数。如果 fork 数是3的话,则 JMH 会 fork 出3个进程来进行测试。
@Benchmark
@BenchmarkMode({Mode.Throughput, Mode.SingleShotTime})
@Fork(value = 3)
public void m {
...
}
复制代码
@OutputTimeUnit
基准测试结果的时间类型。一般选择秒、毫秒、微秒。
@OutputTimeUnit(TimeUnit.SECONDS)
publicclassLinkedListIterationBenchMark{
...
}
复制代码
@Benchmark
方法级注解,表示该方法是需要进行 benchmark 的对象,用法和 JUnit 的 @Test 类似。
@Param
属性级注解, @Param 可以用来指定某项参数的多种情况。特别适合用来测试一个函数在不同的参数输入的情况下的性能。
@Setup
方法级注解,这个注解的作用就是我们需要在测试之前进行一些 准备工作,比如对一些数据的初始化之类的。
@TearDown
方法级注解,这个注解的作用就是我们需要在测试之后进行一些 结束工作,比如关闭线程池,数据库连接等的,主要用于资源的回收等。
@State
当使用 @Setup 参数的时候,必须在类上加这个参数,不然会提示无法运行。
就比如我上面的例子中,就必须设置 state 。
State 用于声明某个类是一个“状态”,然后接受一个 Scope 参数用来表示该状态的共享范围。因为很多 benchmark 会需要一些表示状态的类,JMH 允许你把这些类以依赖注入的方式注入到 benchmark 函数里。Scope 主要分为三种。
Thread: 该状态为每个线程独享。
Group: 该状态为同一个组里面所有线程共享。
Benchmark: 该状态在所有线程间共享。
Thread: 该状态为每个线程独享。
Group: 该状态为同一个组里面所有线程共享。
Benchmark: 该状态在所有线程间共享。
在启动方法中,可以直接指定上述说到的一些参数,并且能将测试结果输出到指定文件中,
/**
* 仅限于IDE中运行
* 命令行模式 则是 build 然后 java -jar 启动
*
* 1. 这是benchmark 启动的入口
* 2. 这里同时还完成了JMH测试的一些配置工作
* 3. 默认场景下,JMH会去找寻标注了@Benchmark的方法,可以通过include和exclude两个方法来完成包含以及排除的语义
*/
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder
// 包含语义
// 可以用方法名,也可以用XXX.class.getSimpleName
.include("Helloworld")
// 排除语义
.exclude("Pref")
// 预热10轮
.warmupIterations(10)
// 代表正式计量测试做10轮,
// 而每次都是先执行完预热再执行正式计量,
// 内容都是调用标注了@Benchmark的代码。
.measurementIterations(10)
// forks(3)指的是做3轮测试,
// 因为一次测试无法有效的代表结果,
// 所以通过3轮测试较为全面的测试,
// 而每一轮都是先预热,再正式计量。
.forks(3)
.output("E:/Benchmark.log")
.build;
new Runner(opt).run;
}
复制代码
结语
基于JMH可以对很多工具和框架进行测试,比如日志框架性能对比、BeanCopy性能对比 等,更多的example可以参考官方给出的 JMH samples[1]
上面其实只是讲解了关于 JMH的使用,推荐延伸阅读这篇文章
JAVA拾遗 — JMH与8个测试陷阱[2]
作者从 Java Developer 角度来谈谈一些常见的代码测试陷阱,分析他们和操作系统底层以及 Java 底层的关联性,并借助 JMH来帮助大家摆脱这些陷阱。
参考
hg.openjdk.java.net/code-tools/…[3]
www.hollischuang.com/archives/10…[4]
yq.aliyun.com/articles/34…[5]
openjdk.java.net/projects/co…[6]
hg.openjdk.java.net/code-tools/…[3]
www.hollischuang.com/archives/10…[4]
yq.aliyun.com/articles/34…[5]
openjdk.java.net/projects/co…[6]
[1]
https://hg.openjdk.java.net/code-tools/jmh/file/tip/jmh-samples/src/main/java/org/openjdk/jmh/samples/: https://link.juejin.cn?target=https%3A%2F%2Fhg.openjdk.java.net%2Fcode-tools%2Fjmh%2Ffile%2Ftip%2Fjmh-samples%2Fsrc%2Fmain%2Fjava%2Forg%2Fopenjdk%2Fjmh%2Fsamples%2F
[2]
https://www.cnkirito.moe/java-jmh/: https://link.juejin.cn?target=https%3A%2F%2Fwww.cnkirito.moe%2Fjava-jmh%2F
[3]
http://hg.openjdk.java.net/code-tools/jmh/file/tip/jmh-samples/src/main/java/org/openjdk/jmh/samples/: https://link.juejin.cn?target=http%3A%2F%2Fhg.openjdk.java.net%2Fcode-tools%2Fjmh%2Ffile%2Ftip%2Fjmh-samples%2Fsrc%2Fmain%2Fjava%2Forg%2Fopenjdk%2Fjmh%2Fsamples%2F
[4]
http://www.hollischuang.com/archives/1072: https://link.juejin.cn?target=http%3A%2F%2Fwww.hollischuang.com%2Farchives%2F1072
[5][6]
https://openjdk.java.net/projects/code-tools/jmh/: https://link.juejin.cn?target=https%3A%2F%2Fopenjdk.java.net%2Fprojects%2Fcode-tools%2Fjmh%2F
• 面试官:聊一聊Java 泛型通配符 T,E,K,V,?
• 阿里一面:如何保证API接口数据安全?
• 0.2秒居然复制了100G文件?
• Java17,有史以来最快 JDK!
最近写了一套 6000 页的 Java 学习手册,以及珍藏四本 Java 人必读4大神器,分享到知乎已经 3 万赞了!
每篇文章图文并茂,附有源码。还有电子书合集
如果你想获得完整PDF可以通过以下方式获得
资料获取方法
- 即可领取
- 在后台回复关键词 002
明天见(。・ω・。)