一、JMH 概述

JMH (Java Micro Benchmark Harness) ,java 微基准测试工具。
JMH 是由实现 Java 虚拟机的团队开发的,用于基准测试。

二、JMH 工具基础使用

2.1、简单案例

以简单的代码作为讲解:对比 LinkedList#add 和 ArrayList#add 性能

基础代码如下:

  1. /**
  2. * JMH 测试:微基准测试 HashMap 和 LinkedHashMap add 性能
  3. * @author zhixing
  4. * @since V2.2
  5. */
  6. @Warmup(iterations = 3) // 预热批次 3 ,不计入度量统计
  7. @Measurement(iterations = 3) // 度量批次 3 ,计入度量统计
  8. @BenchmarkMode(Mode.AverageTime)
  9. @OutputTimeUnit(TimeUnit.MICROSECONDS)
  10. @State(Scope.Thread)
  11. public class JmhTest {
  12. private final static String DATA = "TEST DATA";
  13. private Map<String,String> hashMap;
  14. private Map<String,String> linkedHashMap;
  15. @Setup(Level.Iteration)
  16. public void setUp(){
  17. hashMap = new HashMap<>();
  18. linkedHashMap = new LinkedHashMap<>();
  19. }
  20. @Benchmark
  21. public Map<String,String> hashMapPut(){
  22. this.hashMap.put(DATA,DATA);
  23. return this.hashMap;
  24. }
  25. @Benchmark
  26. @Warmup(iterations = 5) // 预热批次 5 ,不计入度量统计
  27. @Measurement(iterations = 5) // 度量批次 5 ,计入度量统计
  28. public Map<String,String> linkedHashMapPut(){
  29. this.linkedHashMap.put(DATA,DATA);
  30. return this.linkedHashMap;
  31. }
  32. public static void main(String[] args) throws RunnerException {
  33. final Options opts = new OptionsBuilder().include(JmhTest.class.getSimpleName()).forks(1)
  34. .measurementIterations(2) // 度量执行批次为2,度量数据会纳入统计
  35. .warmupIterations(2) // 度量前预热次数为2,不纳入统计
  36. .build();
  37. new Runner(opts).run();
  38. }
  39. }

代码运行结果(只看最后两行)
image.png

  • HashMap#put 方法调用平均响应时间为:0.006微秒
  • LinkedHashMap#put 方法调用平均响应时间为:0.007 微秒

    三、JMH 使用简要说明

    3.1、@Benchmark

    类似 @Test单元测试,@Benchmark 标注方法为需要基准测试的方法

3.2、Warmup 和 Measurement

Warmup 概述

直译为 “预热”,Warmup 工作是在基准测试代码正式度量前,对其进行预热,使得代码的执行经过了类的早期优化、JVM运行期编译、JIT优化后的最终状态,使得代码性能测试数据更准确。

Measurement概述

直译为“度量”,是真正的度量操作,每一轮的度量,所有的度量数据会被纳入统计之中(预热数据不会纳入统计之中)。

3.2.1、全局设置 Warmup 和 Measurement

两种方式设置:

  • 构造 Options 时设置image.png
  • class 上加注解image.png

    3.2.2、针对基准方法局部设置 Warmup 和 Measurement

    1.26 版本优先级:全局 Options 设置 > 局部设置 > 全局注解设置

image.png

3.2.3、Warmup 和 Measurement 输出结果解析

## 使用的 JMH 版本:1.26
# JMH version: 1.26

## JDK 版本信息
# VM version: JDK 1.8.0_131, Java HotSpot(TM) 64-Bit Server VM, 25.131-b11

## JAVA 命令目录
# VM invoker: D:\2-software\15-jdk8\jdk\jre\bin\java.exe

## JAVA 运行时指定参数
# VM options: -javaagent:D:\2-software\9-idea-2019-1\lib\idea_rt.jar=51465:D:\2-software\9-idea-2019-1\bin -Dfile.encoding=UTF-8

## 热身批次 2,每次执行 10s
# Warmup: 2 iterations, 10 s each

## 度量测试批次 2 ,每次执行 10s
# Measurement: 2 iterations, 10 s each

## 每一个批次的超时时间 
# Timeout: 10 min per iteration

## 执行基准测试的线程数量
# Threads: 1 thread, will synchronize iterations

## 统计标准:方法调用一次所耗费的单位时间
# Benchmark mode: Average time, time/op

## Benchmark 方法的绝对历经
# Benchmark: com.zhixing.jmh.JmhTest.hashMapPut

## 执行进度
# Run progress: 0.00% complete, ETA 00:01:20
# Fork: 1 of 1

## 第一次热身平均耗时:0.007 微秒
# Warmup Iteration   1: 0.007 us/op
## 第二次热身评论耗时:0.007 微秒
# Warmup Iteration   2: 0.007 us/op

## 度量批次调用:2个批次
Iteration   1: 0.006 us/op
Iteration   2: 0.006 us/op


## 最终统计结果:调用一次方法平均耗费 0.006 微秒
Result "com.zhixing.jmh.JmhTest.hashMapPut":
  0.006 us/op

## 同上
# JMH version: 1.26
# VM version: JDK 1.8.0_131, Java HotSpot(TM) 64-Bit Server VM, 25.131-b11
# VM invoker: D:\2-software\15-jdk8\jdk\jre\bin\java.exe
# VM options: -javaagent:D:\2-software\9-idea-2019-1\lib\idea_rt.jar=51465:D:\2-software\9-idea-2019-1\bin -Dfile.encoding=UTF-8
# Warmup: 2 iterations, 10 s each
# Measurement: 2 iterations, 10 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: com.zhixing.jmh.JmhTest.linkedHashMapPut

# Run progress: 50.00% complete, ETA 00:00:40
# Fork: 1 of 1
# Warmup Iteration   1: 0.007 us/op
# Warmup Iteration   2: 0.007 us/op
Iteration   1: 0.007 us/op
Iteration   2: 0.007 us/op


Result "com.zhixing.jmh.JmhTest.linkedHashMapPut":
  0.007 us/op


# Run complete. Total time: 00:01:21

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
JmhTest.hashMapPut        avgt    2  0.006          us/op
JmhTest.linkedHashMapPut  avgt    2  0.007          us/op

3.3、@BenchmarkMode

用来声明使用那种模式运行,JMH 提供了四种模式

  • AverageTime(平均响应时间)
    基准测试方法每一次调用的平均耗时
  • Throughput(方法吞吐量)
    单位时间内能够对基准方法调用多少次
  • SampleTime(时间采样)
    抽样统计基准测试方法性能
  • SingleShotTime
    冷测试

3.3.1、AverageTime(平均响应时间)

Class 注解设置
image.png
执行结果
image.png

3.3.2、Throughput(方法吞吐量)

class 设置注解
image.png
执行结果
image.png

  • hashMapPut 每毫秒调用 156551.656 次
  • linkedHashMapPut 每毫秒调用 150773.914

    3.3.3、SampleTime(时间采样)

    class 设置注解
    image.png
    执行结果
    image.png

    3.3.4、SingleShotTime

    class 设置注解

    预热批次设为 0

image.png
执行结果
image.png

3.4、@OutputTimeUnit

设置统计输出时的单位:通过 TimeUnit 设置,单位由:秒、毫秒等。 可以通过

  • 注解
  • Options 参数配置

image.png
image.png

3.5、@State

State 中的 scope 对应三个值

  • Thread
  • Group
  • Benchmark

    3.5.1、Thread

    表示线程独享,每一个运行基准测试方法的线程都会持有一个独立的对象实例,该实例既可能作为基准测试方法参数传入的,也可能是运行基准方法所在的宿主 class 。 针对非线程安全的类使用。

验证关键代码如下:
JmhTest.java
其他方式JmhDemo.java
image.png

  • @State(Scope.Thread) 设置线程独享
  • @Threads(5) 设置 5 个线程运行基准测试方法
  • 无参构造打印信息验证

执行结果
image.png
每一个基准测试方法由 5 个线程测试,每个线程持有各自的实例对象。

3.5.2、Benchmark

对象实例被所有的线程共享

验证关键代码JmhDemo.java
image.png

  • 设置 5 个线程
  • 设定对象线程共享
  • 无参构造打印,方便测试

执行结果
image.png
仅仅实例化了一次

3.5.3、Group

在多线程的情况下,允许多一个实例的多个基准测试方法同时进行操作

验证关键代码JmhDemo.java
image.png
**
执行结果

# JMH version: 1.26
# VM version: JDK 1.8.0_131, Java HotSpot(TM) 64-Bit Server VM, 25.131-b11
# VM invoker: D:\2-software\15-jdk8\jdk\jre\bin\java.exe
# VM options: -javaagent:D:\2-software\9-idea-2019-1\lib\idea_rt.jar=54745:D:\2-software\9-idea-2019-1\bin -Dfile.encoding=UTF-8
# Warmup: <none>
# Measurement: 3 iterations, 10 s each
# Timeout: 10 min per iteration

## 总共 6 个线程,在同一个 group 中,3个调用 read 方法,3个调用 write 方法
# Threads: 6 threads (1 group; 3x "read", 3x "write" in each group), will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: com.zhixing.jmh.JmhDemo.test

# Run progress: 0.00% complete, ETA 00:00:30
# Fork: 1 of 1
Iteration   1: create instance
write
read
write
write
............. 省略read write 交替打印
read
read
0.078 ±(99.9%) 0.047 ms/op
                 read:  0.074 ±(99.9%) 0.165 ms/op
                 write: 0.082 ±(99.9%) 0.432 ms/op



Result "com.zhixing.jmh.JmhDemo.test":
  0.083 ±(99.9%) 0.209 ms/op [Average]
  (min, avg, max) = (0.076, 0.083, 0.097), stdev = 0.011
  CI (99.9%): [≈ 0, 0.292] (assumes normal distribution)

Secondary result "com.zhixing.jmh.JmhDemo.test:read":
  0.086 ±(99.9%) 0.446 ms/op [Average]
  (min, avg, max) = (0.071, 0.086, 0.115), stdev = 0.024
  CI (99.9%): [≈ 0, 0.532] (assumes normal distribution)

Secondary result "com.zhixing.jmh.JmhDemo.test:write":
  0.081 ±(99.9%) 0.033 ms/op [Average]
  (min, avg, max) = (0.079, 0.081, 0.082), stdev = 0.002
  CI (99.9%): [0.047, 0.114] (assumes normal distribution)


# Run complete. Total time: 00:00:30

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
JmhDemo.test        avgt    3  0.083 ± 0.209  ms/op
JmhDemo.test:read   avgt    3  0.086 ± 0.446  ms/op
JmhDemo.test:write  avgt    3  0.081 ± 0.033  ms/op

3.6、@Param

提供配置化参数 案例:提供配置参数,一个基准测试方法,同时测试同种类型的Map 关键代码如下JmhDemo.java image.png

执行结果
image.png

Tips:type 为我们设定参数配置,对应不同的容器实例

3.7、JMH测试套件-Fixture

Fixture 的作用类似于单元测试提供的 @Before @After 之类的注解使用。

3.7.1、 @Setup@TearDown

@Setup 在每个基准测试方法调用前执行。通常用于资源初始化。
@TearDown 在每个基准测试方法调用后执行。通常用于资源回收。

Level 用来控制 @Setup@TearDown 的执行。
Level 三个级别

  • Trial
    @Setup@TearDown 默认配置,表示两个方法会在基准测试方法的所有批次执行前后执行。
  • Iteration
    在设置了 WarmupMeasurement 后,预热和度量批次会执行多次。
    该配置能够使得在每个批次执行前后执行 @Setup@TearDown 标注的方法。
  • Invocation
    在每一次度量操作中,针对基准测试方法的每一次调用前后都会执行 @Setup@TearDown 标注的方法

3.8、@CompilerControl(不推荐)

禁止JVM 运行期间针对方法的优化

image.png

小贴士:其他两种方式,禁止 JVM 运行期的优化操作

  • 代码编写
    java.lang.Compiler.disable();
  • JVM 参数设置
    -Djava.compiler=NONE