java性能调优攻城利器-JMH

简介

JMH即Java Microbenchmark Harness.是由开发JVM的那些大佬开发出的Micro Benchmark Framework.理论上支持各种在JVM上运行的语言。
何为Micro Benchmark Framework?简单的说就是在方法级层面的benchmarck。通常来说,随着系统整体复杂性的不断提升,精准的衡量系统的一个单元的性能愈加困难(如单独进行测试系统的某个部分时,因为JVM或硬件在该场景下可能会产生了一些执行优化,而这些优化在生产环境中是无法应用的,这将导致这类的独立测试不够准确)。
此外借助于JMH框架也能较快速简洁的完成独立的模块测试,有助于在开发高性能应用程序时对性能进行优化。
比较经典的使用场景有:

  • 想定量地知道某个函数需要执行多长时间,以及执行时间和输入 n 的相关性
  • 一个函数有两种不同实现(例如实现 A 使用了 FixedThreadPool,实现 B 使用了 ForkJoinPool),不知道哪种实现性能更好

当然使用场景也不仅仅局限于此。

例子

首先引入依赖


    1.21



    
        org.openjdk.jmh
        jmh-core
        ${jmh.version}
    
    
        org.openjdk.jmh
        jmh-generator-annprocess
        ${jmh.version}
        provided
    

如现在想测试一个方法的耗时分布

@BenchmarkMode(Mode.SampleTime) // 该模式可以测试耗时分布情况
@OutputTimeUnit(TimeUnit.MILLISECONDS) // 输出的时间单位
@State(Scope.Benchmark)
public class JmhTest2 {

    @Benchmark
    public void test() throws InterruptedException {
        TimeUnit.MILLISECONDS.sleep(10);
    }

    public static void main(String[] args) throws RunnerException {
        // 可以通过注解来进行配置
        Options opt = new OptionsBuilder()
                .include(JmhTest2.class.getSimpleName())
                .warmupIterations(3) // 预热3次
                .measurementIterations(2).measurementTime(TimeValue.valueOf("5s")) // 迭代2次,每次5秒
                .threads(10) // 10线程并发
                .forks(1)
                .build();

        new Runner(opt).run();
    }
}

输出

# 以下是配置信息

# JMH version: 1.21
# VM version: JDK 1.8.0_171, Java HotSpot(TM) 64-Bit Server VM, 25.171-b11
# VM invoker: D:\Program\java1.8\jdk1.8\jre\bin\java.exe
# VM options: -Dvisualvm.id=24535188607243 -javaagent:D:\Program\idea\lib\idea_rt.jar=49279:D:\Program\idea\bin -Dfile.encoding=UTF-8
# Warmup: 3 iterations, 10 s each
# Measurement: 2 iterations, 5 s each
# Timeout: 10 min per iteration
# Threads: 10 threads, will synchronize iterations
# Benchmark mode: Sampling time
# Benchmark: top.amazingwu.jmh.JmhTest2.test

# 迭代信息

# Run progress: 0.00% complete, ETA 00:00:40
# Fork: 1 of 1
# Warmup Iteration   1: SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
10.007 ±(99.9%) 0.013 ms/op
# Warmup Iteration   2: 9.990 ±(99.9%) 0.001 ms/op
# Warmup Iteration   3: 9.989 ±(99.9%) 0.002 ms/op
Iteration   1: 9.989 ±(99.9%) 0.003 ms/op
                 test·p0.00:   9.535 ms/op
                 test·p0.50:   9.994 ms/op
                 test·p0.90:   10.060 ms/op
                 test·p0.95:   10.109 ms/op
                 test·p0.99:   10.158 ms/op
                 test·p0.999:  10.256 ms/op
                 test·p0.9999: 10.306 ms/op
                 test·p1.00:   10.306 ms/op

Iteration   2: 9.994 ±(99.9%) 0.006 ms/op
                 test·p0.00:   9.093 ms/op
                 test·p0.50:   9.994 ms/op
                 test·p0.90:   10.109 ms/op
                 test·p0.95:   10.142 ms/op
                 test·p0.99:   10.207 ms/op
                 test·p0.999:  12.239 ms/op
                 test·p0.9999: 12.960 ms/op
                 test·p1.00:   12.960 ms/op

# 结果

Result "top.amazingwu.jmh.JmhTest2.test":
  N = 10002
  mean =      9.991 ±(99.9%) 0.003 ms/op

  Histogram, ms/op:
    [ 9.000,  9.250) = 2 
    [ 9.250,  9.500) = 2 
    [ 9.500,  9.750) = 29 
    [ 9.750, 10.000) = 6290 
    [10.000, 10.250) = 3656 
    [10.250, 10.500) = 11 
    [10.500, 10.750) = 2 
    [10.750, 11.000) = 3 
    [11.000, 11.250) = 0 
    [11.250, 11.500) = 0 
    [11.500, 11.750) = 0 
    [11.750, 12.000) = 0 
    [12.000, 12.250) = 3 
    [12.250, 12.500) = 2 
    [12.500, 12.750) = 1 

  Percentiles, ms/op:
      p(0.0000) =      9.093 ms/op
     p(50.0000) =      9.994 ms/op
     p(90.0000) =     10.093 ms/op
     p(95.0000) =     10.125 ms/op
     p(99.0000) =     10.174 ms/op
     p(99.9000) =     10.796 ms/op
     p(99.9900) =     12.960 ms/op
     p(99.9990) =     12.960 ms/op
     p(99.9999) =     12.960 ms/op
    p(100.0000) =     12.960 ms/op


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

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
JmhTest2.test               sample  10002   9.991 ± 0.003  ms/op
JmhTest2.test:test·p0.00    sample          9.093          ms/op
JmhTest2.test:test·p0.50    sample          9.994          ms/op
JmhTest2.test:test·p0.90    sample         10.093          ms/op    // 90% 每次执行耗时10.093ms以内,以下类推
JmhTest2.test:test·p0.95    sample         10.125          ms/op
JmhTest2.test:test·p0.99    sample         10.174          ms/op
JmhTest2.test:test·p0.999   sample         10.796          ms/op
JmhTest2.test:test·p0.9999  sample         12.960          ms/op
JmhTest2.test:test·p1.00    sample         12.960          ms/op

Process finished with exit code 0

基本概念

Mode

共有四种模式:

  • Throughput: 整体吞吐量,例如“1秒内可以执行多少次调用”。
  • AverageTime: 调用的平均时间,例如“每次调用平均耗时xxx毫秒”。
  • SampleTime: 随机取样,最后输出取样结果的分布,例如“99%的调用在xxx毫秒以内,99.99%的调用在xxx毫秒以内”
  • SingleShotTime: 以上模式都是默认一次 iteration 是 1s,唯有 SingleShotTime 是只运行一次。往往同时把 warmup 次数设为0,用于测试冷启动时的性能。

Warmup

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

注解

@Benchmark

与JUnit的@Test类似,即一个测试单元

@Mode

@State

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

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

例子:

    // 所有线程共享
    @State(Scope.Benchmark)
    public static class BenchmarkState {
        volatile double x = Math.PI;
    }

    // 线程独享
    @State(Scope.Thread)
    public static class ThreadState {
        volatile double x = Math.PI;
    }

    // ThreadState state将会通过依赖注入的形式注入
    @Benchmark
    public void measureUnshared(ThreadState state) {
        state.x++;
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.info("measureUnshared:"+ state.x);
    }

    @Benchmark
    public void measureShared(BenchmarkState state) {
        state.x++;
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.info("measureShared:"+ state.x);
    }

@OutputTimeUnit

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

@Param

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

@Setup

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

@TearDown

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

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Benchmark)
public class SecondBenchmark {
    @Param({"10000", "100000", "1000000"})
    private int length;

    private int[] numbers;

    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(SecondBenchmark.class.getSimpleName())
                .forks(2)
                .warmupIterations(5)
                .measurementIterations(5)
                .build();

        new Runner(opt).run();
    }

    @Benchmark
    public void singleThreadBench() {
        
    }

    @Benchmark
    public void multiThreadBench() {
        return multiThreadCalc.sum(numbers);
    }

    @Setup
    public void prepare() {
        // 初始化,可以使用参数length
        numbers = IntStream.rangeClosed(1, length).toArray();
    }

    @TearDown
    public void shutdown() {
        // 资源回收
    }
}

启动项

如上文使用的启动项配置(以下配置只是常用的部分,同时这些配置可以通过注解来实现):

// 可以通过注解来进行配置
    Options opt = new OptionsBuilder()
            .include(JmhTest2.class.getSimpleName())
            .warmupIterations(3) // 预热3次
            .measurementIterations(2).measurementTime(TimeValue.valueOf("5s")) // 迭代2次,每次5秒
            .threads(10) // 10线程并发
            .forks(1)
            .build();

    new Runner(opt).run();
    

include

benchmark 所在的类的名字,注意这里是使用正则表达式对所有类进行匹配的。

warmupIterations

预热的迭代次数。

measurementIterations

实际测量的迭代次数。

threads

并发线程数

fork

进行 fork 的次数。如果fork数是2的话,则JMH会fork出两个进程来进行测试。主要用于减少运行结果的差异化

其他

扩展资料

  • Jenkov 的 JMH 教程
  • 官方demo

IDE插件

在Intellij Idea中可以搜索插件JMH。

你可能感兴趣的:(Java)