JMH概述
JMH 是一个由 OpenJDK/Oracle 里面那群开发了 Java 编译器的大牛们所开发的 Micro Benchmark Framework 。何谓 Micro Benchmark 呢?简单地说就是在 method 层面上的 benchmark,精度可以精确到微秒级。可以看出 JMH 主要使用在当你已经找出了热点函数,而需要对热点函数进行进一步的优化时,就可以使用 JMH 对优化的效果进行定量的分析。
比较典型的使用场景还有:
- 想定量地知道某个函数需要执行多长时间,以及执行时间和输入 n 的相关性
- 一个函数有两种不同实现(例如实现 A 使用了 FixedThreadPool,实现 B 使用了 ForkJoinPool),不知道哪种实现性能更好
尽管 JMH 是一个相当不错的 Micro Benchmark Framework,但很无奈的是网上能够找到的文档比较少,而官方也没有提供比较详细的文档,对使用造成了一定的障碍。但是有个好消息是官方的 Code Sample 写得非常浅显易懂,推荐在需要详细了解 JMH 的用法时可以通读一遍——本文则会介绍 JMH 最典型的用法和部分常用选项。
我Fork了一份到github,可以提供大家参考一下,使用gradle构建。jmh-gradle-samples
JMH基本概念
Mode
Mode 表示 JMH 进行 Benchmark 时所使用的模式。通常是测量的维度不同,或是测量的方式不同。目前 JMH 共有四种模式:
Throughput
: 整体吞吐量,例如“1秒内可以执行多少次调用”。AverageTime
: 调用的平均时间,例如“每次调用平均耗时xxx毫秒”。SampleTime
: 随机取样,最后输出取样结果的分布,例如“99%的调用在xxx毫秒以内,99.99%的调用在xxx毫秒以内”SingleShotTime
: 以上模式都是默认一次 iteration 是 1s,唯有SingleShotTime
是只运行一次。往往同时把 warmup 次数设为0,用于测试冷启动时的性能。All
: 顾名思义,所有模式,这个在内部测试中常用
Iteration
Iteration 是 JMH 进行测试的最小单位,包含一组invocations。在大部分模式下,一次 iteration 代表的是一秒,JMH 会在这一秒内不断调用需要 benchmark 的方法,然后根据模式对其采样,计算吞吐量,计算平均执行时间等。
Invocation
一次benchmark方法调用
Operation
benchmark方法中,被测量操作的执行。如果被测试的操作在benchmark方法中循环执行,可以使用@OperationsPerInvocation
表明循环次数,使测试结果为单次operation的性能。
Warmup
Warmup 是指在实际进行 benchmark 前先进行预热的行为。为什么需要预热?因为 JVM 的 JIT 机制的存在,如果某个函数被调用多次之后,JVM 会尝试将其编译成为机器码从而提高执行速度。所以为了让 benchmark 的结果更加接近真实情况就需要进行预热。
JMH相关注解
现在来解释一下上面例子中使用到的注解,其实很多注解的意义完全可以望文生义 :)
@Benchmark
表示该方法是需要进行 benchmark 的对象,用法和 JUnit 的 @Test
类似。
@Warmup
@Warmup用来配置预热的内容,可用于类或者方法上,越靠近执行方法的地方越准确。一般配置warmup的参数有这些:
- iterations:预热的次数。
- time:每次预热的时间。
- timeUnit:时间单位,默认是s。
- batchSize:批处理大小,每次操作调用几次方法。
@Measurement
用来控制实际执行的内容,配置的选项和warmup一样。
@BenchmarkMode
Mode
如之前所说,表示 JMH 进行 Benchmark 时所使用的模式。
@BenchmarkMode主要是表示测量的纬度,有以下这些纬度可供选择:
- Mode.Throughput 吞吐量纬度
- Mode.AverageTime 平均时间
- Mode.SampleTime 抽样检测
- Mode.SingleShotTime 检测一次调用
@State
State
用于声明某个类是一个“状态”,然后接受一个 Scope
参数用来表示该状态的共享范围。因为很多 benchmark 会需要一些表示状态的类,JMH 允许你把这些类以依赖注入的方式注入到 benchmark 函数里。Scope
主要分为两种。
- Scope.Benchmark 该状态的意思是会在所有的Benchmark的工作线程中共享变量内容。
- Scope.Group 同一个Group的线程可以享有同样的变量
- Scope.Thread 每隔线程都享有一份变量的副本,线程之间对于变量的修改不会相互影响。
关于State
的用法,官方的 code sample 里有比较好的例子。
@OutputTimeUnit
@OutputTimeUnit代表测量的单位,比如秒级别,毫秒级别,微妙级别等等。一般都使用微妙和毫秒级别的稍微多一点。该注解可以用在方法级别和类级别,当用在类级别的时候会被更加精确的方法级别的注解覆盖,原则就是离目标更近的注解更容易生效。
@Fork
用于配置JMH运行时fork的Java进程。使用单独的进程可以避免测试结果之间互相影响。
- value: fork的进程数量
- warmups: 每个进程执行Warmup的轮数
- jvm:进程使用的JVM
- jvm参数通过以下三个属性,按照从上到下的顺序拼接:
- jvmArgsPrepend
- jvmArgs
- jvmArgsAppend
默认fork的进程数配置在org.openjdk.jmh.runner.Defaults
类中:
/**
* Number of forks in which we measure the workload.
*/
public static final int MEASUREMENT_FORKS = 5;
@Param
在很多情况下,我们需要测试不同的参数的不同结果,但是测试的了逻辑又都是一样的,因此如果我们编写镀铬benchmark的话会造成逻辑的冗余,幸好JMH提供了@Param参数来帮助我们处理这个事情,被@Param注解标示的参数组会一次被benchmark消费到。
@Threads
测试线程的数量,可以配置在方法或者类上,代表执行测试的线程数量。
@Setup
@Setup 方法注解,会在执行 benchmark 之前被执行,正如其名,主要用于初始化
TearDown
@TearDown 方法注解,与@Setup 相对的,会在所有 benchmark 执行结束以后执行,主要用于资源的回收等。
启动选项
解释完了注解,再来看看 JMH 在启动前设置的参数。
Options opt = new OptionsBuilder()
.include(FirstBenchmark.class.getSimpleName())
.forks(1)
.warmupIterations(5)
.measurementIterations(5)
.build();
new Runner(opt).run();
include
benchmark 所在的类的名字,注意这里是使用正则表达式对所有类进行匹配的。
fork
进行 fork 的次数。如果 fork 数是2的话,则 JMH 会 fork 出两个进程来进行测试。
warmupIterations
预热的迭代次数。
measurementIterations
实际测量的迭代次数。
运行结果
> Task :JMHSample_03_States.main()
# JMH version: 1.23
# VM version: JDK 1.8.0_181, Java HotSpot(TM) 64-Bit Server VM, 25.181-b13
# VM invoker: /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/bin/java
# VM options: -Dfile.encoding=UTF-8 -Duser.country=CN -Duser.language=en -Duser.variant
# Warmup: 5 iterations, 10 s each
# Measurement: 5 iterations, 10 s each
# Timeout: 10 min per iteration
# Threads: 4 threads, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: org.openjdk.jmh.samples.JMHSample_03_States.measureShared
# Run progress: 0.00% complete, ETA 00:03:20
# Fork: 1 of 1
# Warmup Iteration 1: 40171051.842 ops/s
# Warmup Iteration 2: 40252218.743 ops/s
# Warmup Iteration 3: 40364177.716 ops/s
# Warmup Iteration 4: 40404016.867 ops/s
# Warmup Iteration 5: 40404799.770 ops/s
Iteration 1: 40647891.459 ops/s
Iteration 2: 40493191.378 ops/s
Iteration 3: 40439925.585 ops/s
Iteration 4: 40304980.399 ops/s
Iteration 5: 39981776.259 ops/s
Result "org.openjdk.jmh.samples.JMHSample_03_States.measureShared":
40373553.016 ±(99.9%) 966832.718 ops/s [Average]
(min, avg, max) = (39981776.259, 40373553.016, 40647891.459), stdev = 251083.387
CI (99.9%): [39406720.298, 41340385.734] (assumes normal distribution)
# JMH version: 1.23
# VM version: JDK 1.8.0_181, Java HotSpot(TM) 64-Bit Server VM, 25.181-b13
# VM invoker: /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/bin/java
# VM options: -Dfile.encoding=UTF-8 -Duser.country=CN -Duser.language=en -Duser.variant
# Warmup: 5 iterations, 10 s each
# Measurement: 5 iterations, 10 s each
# Timeout: 10 min per iteration
# Threads: 4 threads, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: org.openjdk.jmh.samples.JMHSample_03_States.measureUnshared
# Run progress: 50.00% complete, ETA 00:01:40
# Fork: 1 of 1
# Warmup Iteration 1: 610056952.435 ops/s
# Warmup Iteration 2: 604463030.904 ops/s
# Warmup Iteration 3: 598467532.966 ops/s
# Warmup Iteration 4: 604482926.268 ops/s
# Warmup Iteration 5: 609670717.845 ops/s
Iteration 1: 595976801.767 ops/s
Iteration 2: 606049671.220 ops/s
Iteration 3: 612153531.508 ops/s
Iteration 4: 540723287.097 ops/s
Iteration 5: 569040967.811 ops/s
Result "org.openjdk.jmh.samples.JMHSample_03_States.measureUnshared":
584788851.881 ±(99.9%) 114160199.154 ops/s [Average]
(min, avg, max) = (540723287.097, 584788851.881, 612153531.508), stdev = 29647041.189
CI (99.9%): [470628652.726, 698949051.035] (assumes normal distribution)
# Run complete. Total time: 00:03:20
REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
experiments, perform baseline and negative tests that provide experimental control, make sure
the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
Do not assume the numbers tell you what you want them to tell.
Benchmark Mode Cnt Score Error Units
JMHSample_03_States.measureShared thrpt 5 40373553.016 ± 966832.718 ops/s
JMHSample_03_States.measureUnshared thrpt 5 584788851.881 ± 114160199.154 ops/s
启动参数可以配置,但是也有默认值,可以参考类:org.openjdk.jmh.runner.Defaults
配置的优先级
OptionsBuilder > 方法注解 > 类注解
Java对象Mapping框架性能对比
Mapping框架使用的版本,一下都是基于2020-03-23最新的版本:
框架 | 版本 | 发布时间 |
---|---|---|
dozer | 5.5.1 | 2014-04 |
orika | 1.5.4 | 2019-02 |
mapstruct | 1.3.1.Final | 2019-09 |
modelmapper | 2.3.6 | 2019-12 |
机器:
MacBook Pro (15-inch, 2018)
下面进行两组测试,一组简单代码测试,一组真实项目测试。
代码:https://github.com/amuguelove/java-mapping-frameworks-benchmark
简单测试
Throughput:
框架 | Throughput(operations per milliseconds) |
---|---|
dozer | 796 |
mapstruct | 184902 |
modelmapper | 1171 |
orika | 2428 |
AverageTime:
框架 | AverageTime(milliseconds per operation) |
---|---|
dozer | 0.001 |
mapstruct | 10⁻⁵ |
modelmapper | 0.001 |
orika | 10⁻³ |
SingleShotTime
框架 | SingleShotTime(milliseconds per operation) |
---|---|
dozer | 10.871 |
mapstruct | 0.418 |
modelmapper | 5.393 |
orika | 4.362 |
SampleTime(ms per operation)
框架 | P0.90 | P0.999 | P1.00 |
---|---|---|---|
dozer | 0.001 | 0.005 | 1.288 |
mapstruct | 10⁻⁴ | 10⁻⁴ | 1.270 |
modelmapper | 0.001 | 0.003 | 1.239 |
orika | 0.001 | 0.001 | 1.307 |
真实项目测试:
Throughput:
框架 | Throughput(operations per milliseconds) |
---|---|
dozer | 13 |
mapstruct | 6587 |
modelmapper | 13 |
orika | 355 |
AverageTime:
框架 | AverageTime(milliseconds per operation) |
---|---|
dozer | 0.068 |
mapstruct | 10⁻⁴ |
modelmapper | 0.060 |
orika | 0.003 |
SingleShotTime
框架 | SingleShotTime(milliseconds per operation) |
---|---|
dozer | 20.794 |
mapstruct | 0.769 |
modelmapper | 20.267 |
orika | 31.194 |
SampleTime(ms per operation)
框架 | P0.90 | P0.999 | P1.00 |
---|---|---|---|
dozer | 0.061 | 0.182 | 2.302 |
mapstruct | 10⁻⁴ | 0.002 | 2.101 |
modelmapper | 0.053 | 0.169 | 2.245 |
orika | 0.002 | 0.023 | 2.159 |
综上:mapstruct的性能要好于其他的mapping框架。
「真诚赞赏,手留余香」
请我喝杯咖啡?
使用微信扫描二维码完成支付
