JMH:JVM基准测试工具

概述

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 最典型的用法和部分常用选项。

基本概念

@Benchmark

表示该方法是需要进行 benchmark 的对象,用法和 JUnit 的 @Test 类似。

@BenchmarkModes(Mode)

Mode 表示 JMH 进行 Benchmark 时所使用的模式。通常是测量的维度不同,或是测量的方式不同。目前 JMH 共有四种模式:

  • Throughput: 测试方法每秒的次数。
  • AverageTime: 测试执行的平均时间(单个执行),例如“每次调用平均耗时xxx毫秒”。
  • SampleTime: 随机取样,最后输出取样结果的分布,例如“99%的调用在xxx毫秒以内,99.99%的调用在xxx毫秒以内”
  • SingleShotTime: 测试方法执行需要多长时间,以上模式都是默认一次 iteration 是 1s,唯有 SingleShotTime 是只运行一次。往往同时把 warmup 次数设为0, 用于测试冷启动时的性能。
  • ALL : 对上述所有措施。

日志输出 Iteration 是 JMH 进行测试的最小单位。在大部分模式下,一次 iteration 代表的是一秒,JMH 会在这一秒内不断调用需要 benchmark 的方法,然后根据模式对其采样,计算吞吐量,计算平均执行时间等。

日志输出 Warmup 是指在实际进行 benchmark 前先进行预热的行为。为什么需要预热?因为 JVM 的 JIT 机制的存在,如果某个函数被 调用多次之后,JVM 会尝试将其编译成为机器码从而提高执行速度。所以为了让 benchmark 的结果更加接近真实情况就需要进行预热。

@State

State 用于声明某个类是一个“状态”,然后接受一个 Scope 参数用来表示该状态的共享范围。因为很多 benchmark 会需要一些表示 状态的类,JMH 允许你把这些类以依赖注入的方式注入到 benchmark 函数里。Scope 主要分为两种。

  • Thread: 该状态为每个线程独享。
  • Group: 该状态为每个线程组独享。
  • Benchmark: 该状态在所有线程间共享。

@OutputTimeUnit

benchmark 结果所使用的时间单位。

  • NANOSECONDS
  • MICROSECONDS
  • MILLISECONDS
  • SECONDS
  • MINUTES
  • HOURS
  • DAYS

启动选项

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(JMHSample_03_States.class.getSimpleName())
                .warmupIterations(5)
                .measurementIterations(5)
                .threads(4)
                .forks(1)
                .build();

        new Runner(opt).run();
    }

  • include :benchmark 所在的类的名字,注意这里是使用正则表达式对所有类进行匹配的。
  • fork : 进行 fork 的次数。如果 fork 数是2的话,则 JMH 会 fork 出两个进程来进行测试。
  • warmupIterations: 预热的迭代次数。
  • measurementIterations: 实际测量的迭代次数。

@Param

可以用来指定某项参数的多种情况。特别适合用来测试一个函数在不同的参数输入的情况下的性能。

@Setup @TearDown

@Setup 会在执行 benchmark 之前被执行,正如其名,主要用于初始化。

@TearDown 和 @Setup 相对的,会在所有 benchmark 执行结束以后执行,主要用于资源的回收等。

注意注释需要一个参数。这个参数可以有三个不同的值。您设置的值指示JMH应该调用方法的时间。如下: Level.Trial 该方法在基准的每次全面运行中每次调用一次。全面运行意味着一个完整的“fork”,包括所有的热身和基准迭代。 Level.Iteration 该方法被调用一次,用于每次迭代基准测试。. Level.Invocation 每次调用基准方法时都调用此方法