103.【Java Microbenchmark Harness】

(一)、JMH概述

1.什么是JMH?

JMH(Java Microbenchmark Harness)是一个 Java 工具,用于构建、运行和分析用 Java 和其他针对 JVM 的语言编写的 纳米/微米/毫/宏观 基准测试,而且是由Java虚拟机团队开发的。简单说,就是用来测量代码运行性能,简称 :“Java性能检测工具”。

JMH官网: https://openjdk.org/projects/code-tools/jmh/

JMH源码下载: https://github.com/openjdk/jmh

2.JMH入门操作

(1).没有JMH的时候我们怎么进行测试的?

我们利用结束的时间戳 - 结束后的时间戳

    @Test
    void contextLoads() {
    
    
        long l1 = System.currentTimeMillis();
        int n=0;
        for (int i = 1; i <100000 ; i++) {
    
    
            n+=1;
        }
        System.out.println(n);
        long l2 = System.currentTimeMillis();
        System.out.println("一共消耗时间: "+(l2-l1));
    }

(2).利用JMH进行测试

添加依赖

<dependencies>
    <dependency>
        <groupId>org.openjdk.jmh</groupId>
        <artifactId>jmh-core</artifactId>
        <version>1.23</version>
    </dependency>
 
    <dependency>
        <groupId>org.openjdk.jmh</groupId>
        <artifactId>jmh-generator-annprocess</artifactId>
        <version>1.23</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/cn.hutool/hutool-all -->
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.5</version>
</dependency>
</dependencies>

(二)、JMH运用展示

1.Hello JMH

源码


package org.openjdk.jmh.samples;

import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

public class JMHSample_01_HelloWorld {
    
    



    @Benchmark   //代表我们现在执行测试的就是这个空方法
    public void wellHelloThere() {
    
        //
        // this method was intentionally left blank.
    }

    public static void main(String[] args) throws RunnerException {
    
    
        Options opt = new OptionsBuilder()
                .include(JMHSample_01_HelloWorld.class.getSimpleName())  //执行测试方法的载体类是什么。
                .forks(1)  //总共测试几轮,默认我们呢选择1
                .build();

        new Runner(opt).run();  //执行
    }

}

解释

v
在这里插入图片描述

(三)、JMH注解

1.@Warmip和@Measurement (预热和真实执行)【类】

@Warmip 预热

预热: 执行一次,一次1秒

@Warmup(iterations = 1,time = 1,timeUnit = TimeUnit.SECONDS)  //预热次数和时间

真实测试执行一次,一次一秒

@Measurement(iterations = 1,time = 1,timeUnit = TimeUnit.SECONDS) //测试次数和时间

源码

package org.openjdk.jmh.samples;

import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Warmup;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.concurrent.TimeUnit;

@Warmup(iterations = 1,time = 1,timeUnit = TimeUnit.SECONDS)  //预热次数和时间
@Measurement(iterations = 1,time = 1,timeUnit = TimeUnit.SECONDS) //测试次数和时间
public class JMHSample_01_HelloWorld {
    
    



    @Benchmark   //代表我们现在执行测试的就是这个空方法
    public void wellHelloThere() {
    
        //
        // this method was intentionally left blank.
    }


    public static void main(String[] args) throws RunnerException {
    
    
        Options opt = new OptionsBuilder()
                .include(JMHSample_01_HelloWorld.class.getSimpleName())
                .forks(1)
                .build();

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

在这里插入图片描述

2.@BenchmarkMode和@OutputTimeUnit (输出方式和输出单位)【方法】

源码

@BenchmarkMode(Mode.Throughput)  //吞吐量测试, 输出报告: 每单位时间会执行多少次
@OutputTimeUnit(TimeUnit.SECONDS) //输出报告的时间单位

package org.openjdk.jmh.samples;

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.concurrent.TimeUnit;

@Warmup(iterations = 1,time = 1,timeUnit = TimeUnit.SECONDS)  //预热次数和时间
@Measurement(iterations = 1,time = 1,timeUnit = TimeUnit.SECONDS) //测试次数和时间
public class JMHSample_02_BenchmarkModes {
    
    



    @Benchmark   //执行测试的方法
    @BenchmarkMode(Mode.Throughput)  //吞吐量测试, 输出报告: 每单位时间会执行多少次
    @OutputTimeUnit(TimeUnit.SECONDS) //输出报告的时间单位
    public void measureThroughput() throws InterruptedException {
    
    
        TimeUnit.MILLISECONDS.sleep(100);
    }



    @Benchmark
    @BenchmarkMode(Mode.AverageTime)  //平均耗时测试, 输出报告每次操作耗时
    @OutputTimeUnit(TimeUnit.SECONDS)  //耗时的单位
    public void measureAvgTime() throws InterruptedException {
    
    
        TimeUnit.MILLISECONDS.sleep(100);
    }


    @Benchmark
    @BenchmarkMode(Mode.SampleTime)  //抽样测试,输出报告: 会在执行过程中采样(每次操作耗时)
    @OutputTimeUnit(TimeUnit.SECONDS)
    public void measureSamples() throws InterruptedException {
    
    
        TimeUnit.MILLISECONDS.sleep(100);
    }


    @Benchmark
    @BenchmarkMode(Mode.SingleShotTime)  //冷启动测试,设置这个,此方法在一轮中只运行一次,这个模式主要是为了测试冷启动的性能
    @OutputTimeUnit(TimeUnit.SECONDS)  //输出单位
    public void measureSingleShot() throws InterruptedException {
    
    
        TimeUnit.MILLISECONDS.sleep(100);
    }


    @Benchmark
    @BenchmarkMode({
    
    Mode.Throughput, Mode.AverageTime, Mode.SampleTime, Mode.SingleShotTime})  // 前四种都进行测试。四种模式都会测试一次,输出四种报告
    @OutputTimeUnit(TimeUnit.SECONDS)
    public void measureMultiple() throws InterruptedException {
    
    
        TimeUnit.MILLISECONDS.sleep(100);
    }



    @Benchmark
    @BenchmarkMode(Mode.All)  // 不可以像第五种一样指定多种测试方法。只能测试全部四种
    @OutputTimeUnit(TimeUnit.SECONDS)
    public void measureAll() throws InterruptedException {
    
    
        TimeUnit.MILLISECONDS.sleep(100);
    }


    public static void main(String[] args) throws RunnerException {
    
    
        Options opt = new OptionsBuilder()
                .include(JMHSample_02_BenchmarkModes.class.getSimpleName())
                .forks(1)
                .build();

        new Runner(opt).run();
    }

}

第一个测试 在这里插入图片描述
第二个测试 在这里插入图片描述
第三个测试 在这里插入图片描述
第四次测试 在这里插入图片描述
第五次测试 在这里插入图片描述
第六次测试 在这里插入图片描述

3.@State (状态)【类】

源码

@State(Scope.Thread) //各个线程独自占有一个对象
 @State(Scope.Benchmark)  //整个测试总共用这一个对象
package org.openjdk.jmh.samples;

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.concurrent.TimeUnit;

/**
 *   @State(Scope.XXXX) 描述了这个类对象的作用域
 *   测试共享与独享的区别
 */
@Warmup(iterations = 1,time = 1,timeUnit = TimeUnit.SECONDS)  //预热次数和时间
@Measurement(iterations = 1,time = 1,timeUnit = TimeUnit.SECONDS) //测试次数和时间
public class JMHSample_03_States {
    
    


    // 所有测试线程各用各的 (独享)
    @State(Scope.Thread) //各个线程独自占有一个对象
    public static class ThreadState {
    
      //在我们执行Benchmark的时候,他会生成一个ThreadState对象。可以被Benchmark直接使用的,也就是入参。
        volatile double x = Math.PI;
    }

    // 根据main方法,会启动4个线程去一起执行
    @Benchmark
    public void measureUnshared(ThreadState state) {
    
      //每一个线程的入参都是不同的对象
        state.x++;
    }


    // 所有测试共享一个实列,用于测试有状态实列在多线程共享下的性能
    // 一般用来测试多线程竞争下的性能
    @State(Scope.Benchmark)  //整个测试总共用这一个对象
    public static class BenchmarkState {
    
    
        volatile double x = Math.PI;
    }

    @Benchmark
    public void measureShared(BenchmarkState state) {
    
      //这个依然启动4个进程,但是入参都是同一个实列,竞争非常激烈

        state.x++;
    }



    public static void main(String[] args) throws RunnerException {
    
    
        Options opt = new OptionsBuilder()
                .include(JMHSample_03_States.class.getSimpleName())
                .threads(4)   //在执行Benchmark的时候会启动4个线程
                .forks(1)
                .build();

        new Runner(opt).run();
    }

}

在这里插入图片描述

4.DefaultState (默认状态)【类】

源码

@State(Scope.Thread)  //放在整体类上
package org.openjdk.jmh.samples;

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.concurrent.TimeUnit;


@State(Scope.Thread)  //我们标记这个类会成为一个对象由JMH进行管理。多个线程会创建多个下面类对象
@Warmup(iterations = 1,time = 1,timeUnit = TimeUnit.SECONDS)  //预热次数和时间
@Measurement(iterations = 1,time = 1,timeUnit = TimeUnit.SECONDS) //测试次数和时间
public class JMHSample_04_DefaultState {
    
    

    double x = Math.PI;


    // 我们使用默认静态类的主要目的是:  我们不用特地的去写一个静态内部类去声明一个入参。如果没有上面的注解那么我们就需要声明一个静态内部类
    @Benchmark
    public void measure() {
    
    
        x++;
    }
    

    public static void main(String[] args) throws RunnerException {
    
    
        Options opt = new OptionsBuilder()
                .include(JMHSample_04_DefaultState.class.getSimpleName())
                .forks(1)
                .build();

        new Runner(opt).run();
    }

}

在这里插入图片描述

5.@Setup和@TearDown (初始化和销毁)【方法】

启动之前准备工作

    // 启动Benchmark之前的准备工作
    @Setup // 必须在@State下的类中才能使用,实际上也算是@state管理对象的生命周期一部分

结束之后检查工作

    // Benchmark结束之后的检查工作
    @TearDown  //必须在@State下的类中才能使用

源码


package org.openjdk.jmh.samples;

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.concurrent.TimeUnit;

/**
 *   控制state 对象中的方法在什么时候执行
 *
 *   @Setup state中的方法如何被执行
 *   @TearDown state中的方法如何执行
 *   level.Trial 默认的执行策略,整个基准测试执行一次
 */
@State(Scope.Thread)  //生命默认静态类,我们可以不用进行手动传入参数
@Warmup(iterations = 1,time = 1,timeUnit = TimeUnit.SECONDS)  //预热次数和时间
@Measurement(iterations = 1,time = 1,timeUnit = TimeUnit.SECONDS) //测试次数和时间
public class JMHSample_05_StateFixtures {
    
    

    double x;

    // 启动Benchmark之前的准备工作
    @Setup // 必须在@State下的类中才能使用,实际上也算是@state管理对象的生命周期一部分
    public void prepare() {
    
    
        x = Math.PI;
        System.out.println("--------------1");
    }

    // Benchmark结束之后的检查工作
    @TearDown  //必须在@State下的类中才能使用
    public void check() {
    
    
        System.out.println("--------------2");
        // 这里使用了断言
        assert x > Math.PI : "Nothing changed?";
    }


    @Benchmark
    public void measureRight() {
    
      //正确代码,正常执行
        x++;
    }

    @Benchmark
    public void measureWrong() {
    
      // 这里是错误代码实列,会在Benchmark执行完毕后报错
        double x = 0;
        x++;
    }

    public static void main(String[] args) throws RunnerException {
    
    
        Options opt = new OptionsBuilder()
                .include(JMHSample_05_StateFixtures.class.getSimpleName())
                .forks(1)
                .jvmArgs("-ea")  //开启断言检测: assertion在一般情况下是关闭的,通过 java -ea 可以打开改功能,关闭为 -da
                .build();

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

在这里插入图片描述
在这里插入图片描述

6.FixtureLevel (初始化和销毁等级)【方法】

源码

   @Setup(Level.Trial)  // 与TearDown同理.启动之前会执行一次
   @TearDown(Level.Trial)  // 整个完整的基准测试之后才会执行一次
   @TearDown(Level.Iteration)  // 每轮循环完成之后才会执行一次
   @TearDown(Level.Invocation)  // 每次方法被调用都会执行一次

package org.openjdk.jmh.samples;

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.concurrent.TimeUnit;

@State(Scope.Thread)  //可以免除静态内部类
@Warmup(iterations = 1,time = 1,timeUnit = TimeUnit.SECONDS)  //预热次数和时间
@Measurement(iterations = 3,time = 1,timeUnit = TimeUnit.SECONDS) //测试次数和时间
public class JMHSample_06_FixtureLevel {
    
    

    double x;

//    @Setup(Level.Trial)  // 与TearDown同理.启动之前会执行一次
//    @TearDown(Level.Trial)  // 整个完整的基准测试之后才会执行一次
//    @TearDown(Level.Iteration)  // 每轮循环完成之后才会执行一次
//     @TearDown(Level.Invocation)  // 每次方法被调用都会执行一次

    @TearDown(Level.Iteration)  // 每次方法被调用都会执行一次
    public void check() {
    
    
        System.out.println("----------1");
        assert x > Math.PI : "Nothing changed?";
    }

    @Benchmark
    public void measureRight() {
    
    
        x++;
    }

    @Benchmark
    public void measureWrong() {
    
    
        double x = 0;
        x++;
    }

    public static void main(String[] args) throws RunnerException {
    
    
        Options opt = new OptionsBuilder()
                .include(JMHSample_06_FixtureLevel.class.getSimpleName())
                .forks(1)
                .jvmArgs("-ea")
                .shouldFailOnError(false) //  默认是false,即使assert错误也不会让整个测试失败
                .build();

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

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

7.FixtureLevelInvocation (固定级别调用)

package org.openjdk.jmh.samples;

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.concurrent.*;


@OutputTimeUnit(TimeUnit.MICROSECONDS)  // 报告时间单位
@BenchmarkMode(Mode.AverageTime)   // 输出报告
@Warmup(iterations = 1,time = 1,timeUnit = TimeUnit.SECONDS)  //预热次数和时间
@Measurement(iterations = 1,time = 1,timeUnit = TimeUnit.SECONDS) //测试次数和时间
public class JMHSample_07_FixtureLevelInvocation {
    
    

    @State(Scope.Benchmark)   // 共享一个对象
    public static class NormalState {
    
    
        ExecutorService service;

        @Setup(Level.Trial)  // 整个基准调用之前进行准备工作 (全局一次)
        public void up() {
    
    
            System.out.println("--------准备工作");
            service = Executors.newCachedThreadPool();
        }

        @TearDown(Level.Trial)  // 整个基准结束之后进行检查工作 (全局一次)
        public void down() {
    
    
            System.out.println("------------销毁");
            service.shutdown();
        }

    }

    public static class LaggingState extends NormalState {
    
    
        public static final int SLEEP_TIME = Integer.getInteger("sleepTime", 10);

        @Setup(Level.Invocation)  //每次方法被调用都会执行
        public void lag() throws InterruptedException {
    
    
            TimeUnit.MILLISECONDS.sleep(SLEEP_TIME);  //调用的时候会睡眠10毫秒
        }
    }


    @Benchmark
    @BenchmarkMode(Mode.AverageTime)  // 输出的方式是每次多少秒
    public double measureHot(NormalState e, final Scratch s) throws ExecutionException, InterruptedException {
    
    
        return e.service.submit(new Task(s)).get();
    }

    @Benchmark
    @BenchmarkMode(Mode.AverageTime)  //输出的方式是每次多少秒
    public double measureCold(LaggingState e, final Scratch s) throws ExecutionException, InterruptedException {
    
    
        return e.service.submit(new Task(s)).get();
    }


    @State(Scope.Thread)  // 独享下面的静态方法
    public static class Scratch {
    
    
        private double p;
        public double doWork() {
    
    
            p = Math.log(p);
            return p;
        }
    }

    public static class Task implements Callable<Double> {
    
    
        private Scratch s;

        public Task(Scratch s) {
    
    
            this.s = s;
        }

        @Override
        public Double call() {
    
    
            return s.doWork();
        }
    }


    public static void main(String[] args) throws RunnerException {
    
    
        Options opt = new OptionsBuilder()
                .include(JMHSample_07_FixtureLevelInvocation.class.getSimpleName())
                .forks(1)
                .build();

        new Runner(opt).run();
    }

}

在这里插入图片描述

8.DeadCode (JVM调优)

源码

package org.openjdk.jmh.samples;

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.concurrent.TimeUnit;

/*** 这个列子展示了一种场景: 有些代码会变成JVM优化掉,使得基准测试结果不可用。
 *
 *  baseline(): 空方法
 *
 *  measureWrong(): 由于计算结果并没有返回,JVM会自动优化,使其耗时测得与baseline()结果一样
 *
 *  measureRight(): 将计算结果返回,JVM自动优化,这样才能真实测得真实的对象
 *
 */

@State(Scope.Thread)  //静态内部可省略
@BenchmarkMode(Mode.AverageTime)  //每次多少秒
@OutputTimeUnit(TimeUnit.NANOSECONDS)  //单位
@Warmup(iterations = 1,time = 1,timeUnit = TimeUnit.SECONDS)  //预热次数和时间
@Measurement(iterations = 1,time = 1,timeUnit = TimeUnit.SECONDS) //测试次数和时间
public class JMHSample_08_DeadCode {
    
    

    private double x = Math.PI;

    private double compute(double d) {
    
    
        for (int c = 0; c < 10; c++) {
    
    
            d = d * d / Math.PI;
        }
        return d;
    }

    @Benchmark
    public void baseline() {
    
    
        // do nothing, this is a baseline
    }

    @Benchmark
    public void measureWrong() {
    
    
        // This is wrong: result is not used and the entire computation is optimized away.
        compute(x);  //因为我们没有使用计算结果,JVM会自动把这段代码优化掉,相当于测试了一个空方法。
    }

    @Benchmark
    public double measureRight() {
    
    
        // This is correct: the result is being used.
        return compute(x);  //让JVM不能优化掉,我们返回了结果
    }


    public static void main(String[] args) throws RunnerException {
    
    
        Options opt = new OptionsBuilder()
                .include(JMHSample_08_DeadCode.class.getSimpleName())
                .forks(1)
                .jvmArgs("-server")  //注意这里一定要设置server模式,为了充分使用JVM调优。
                .build();

        new Runner(opt).run();
    }

}

在这里插入图片描述

9.Blackholes (黑洞拒绝JVM调优)

黑洞: 可以有效的避免过于激进的优化

源码

 @Benchmark
    public void measureRight_2(Blackhole bh) {
    
    
        // 如果执行结果不使用编译器优化
        // 为了防止编译器自作主张,这里使用JMH提供的黑洞对象执行结果进行消费
        bh.consume(compute(x1));
        bh.consume(compute(x2));
    }
package org.openjdk.jmh.samples;

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.concurrent.TimeUnit;

/**
 *  空方法会被JVM给优化掉,但黑洞可以拒绝优化
 */

@BenchmarkMode(Mode.AverageTime)  //输出方式
@OutputTimeUnit(TimeUnit.NANOSECONDS) //输出的时间
@State(Scope.Thread) //面内部静态
@Warmup(iterations = 1,time = 1,timeUnit = TimeUnit.SECONDS)  //预热次数和时间
@Measurement(iterations = 1,time = 1,timeUnit = TimeUnit.SECONDS) //测试次数和时间
public class JMHSample_09_Blackholes {
    
    

    double x1 = Math.PI;
    double x2 = Math.PI * 2;

    private double compute(double d) {
    
    
        for (int c = 0; c < 10; c++) {
    
    
            d = d * d / Math.PI;
        }
        return d;
    }


    @Benchmark
    public double baseline() {
    
      //整一个,且真实有效的只有一个
        return compute(x1);
    }


    @Benchmark
    public double measureWrong() {
    
     //整两个,但真正有效值为1个
        compute(x1);  //编译器自动识别,直接被JVM优化掉
        return compute(x2);
    }


    @Benchmark
    public double measureRight_1() {
    
      //整两个且真实计算的有2个
        return compute(x1) + compute(x2);
    }



    @Benchmark
    public void measureRight_2(Blackhole bh) {
    
    
        // 如果执行结果不使用编译器优化
        // 为了防止编译器自作主张,这里使用JMH提供的黑洞对象执行结果进行消费
        bh.consume(compute(x1));
        bh.consume(compute(x2));
    }
    
    public static void main(String[] args) throws RunnerException {
    
    
        Options opt = new OptionsBuilder()
                .include(JMHSample_09_Blackholes.class.getSimpleName())
                .forks(1)
                .build();
        new Runner(opt).run();
    }
}

在这里插入图片描述

10.ConstantFold (常量折叠)

常数折叠是编译器最佳化技术,被使用在现代的编译器中。进阶的常数传播形式,或称之为稀疏有条件的常量传播,可以更精确地传播常数及无缝的移除无用的程式码

eg: i=20+40+30; 出现了常量折叠。那么编译器会直接省略步骤,直接给我们结果,而不会去在CPU中进行计算的操作,这就是省去无用代码。

源码


package org.openjdk.jmh.samples;

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
/**
*  计算常量和变量的耗时情况
*/

import java.util.concurrent.TimeUnit;

@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 1,time = 1,timeUnit = TimeUnit.SECONDS)  //预热次数和时间
@Measurement(iterations = 1,time = 1,timeUnit = TimeUnit.SECONDS) //测试次数和时间
public class JMHSample_10_ConstantFold {
    
    


    private double x = Math.PI;  //变量


    private final double wrongX = Math.PI;  //常量

    private double compute(double d) {
    
      //计算方法
        for (int c = 0; c < 10; c++) {
    
    
            d = d * d / Math.PI;
        }
        return d;
    }

    @Benchmark
    public double baseline() {
    
      // 获取常量
        // simply return the value, this is a baseline
        return Math.PI;
    }

    @Benchmark
    public double measureWrong_1() {
    
      //计算常量
        // This is wrong: the source is predictable, and computation is foldable.
        return compute(Math.PI);
    }

    @Benchmark
    public double measureWrong_2() {
    
      // 计算常量
        // This is wrong: the source is predictable, and computation is foldable.
        return compute(wrongX);
    }

    @Benchmark
    public double measureRight() {
    
      //计算x x因为没有被final修饰,所以是变量
        // This is correct: the source is not predictable.
        return compute(x);
    }


    public static void main(String[] args) throws RunnerException {
    
    
        Options opt = new OptionsBuilder()
                .include(JMHSample_10_ConstantFold.class.getSimpleName())
                .forks(1)
                .build();

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

在这里插入图片描述

11.@OperationsPerInvocation (调用次数)

但报表只有一次

@OperationsPerInvocation(n)  
告诉JMH的Benchmark一次执行相当于执行多少次,就是说JMH的Benchmark被JVM只
执行了一次,但在报表的时候会把这一次当作n次来计算。也就是输出这n次总耗时.

12.Loops (循环)

源码

package org.openjdk.jmh.samples;

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.concurrent.TimeUnit;

/**
 *   计算两个数相加耗时: 理论上应该一致,但我们发现for循环更快
 */
@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 1,time = 1,timeUnit = TimeUnit.SECONDS)  //预热次数和时间
@Measurement(iterations = 1,time = 1,timeUnit = TimeUnit.SECONDS) //测试次数和时间
public class JMHSample_11_Loops {
    
    

    int x = 1;
    int y = 2;


    @Benchmark
    public int measureRight() {
    
      //测试变量相加
        return (x + y);
    }

    private int reps(int reps) {
    
      //进行循环相加
        int s = 0;
        for (int i = 0; i < reps; i++) {
    
    
            s += (x + y);
        }
        return s;
    }


    @Benchmark
    @OperationsPerInvocation(1)  //告诉JMH的Benchmark一次执行相当于执行多少次,就是说JMH的Benchmark被JVM只执行了一次,但在报表的时候会把这一次当作n次来计算。也就是输出这n次总耗时
    public int measureWrong_1() {
    
      //循环相加 理论上应该与 第一个方式相加一样
        return reps(1);
    }

    @Benchmark
    @OperationsPerInvocation(10) //告诉JMH一次执行相当于执行多少次,就是说JMH执行n次,只不过在报表的时候只会报出n/n个
    public int measureWrong_10() {
    
      //循环相加
        return reps(10);
    }

    @Benchmark
    @OperationsPerInvocation(100)
    public int measureWrong_100() {
    
     //循环相加
        return reps(100);
    }

    @Benchmark
    @OperationsPerInvocation(1_000)
    public int measureWrong_1000() {
    
     //循环相加
        return reps(1_000);
    }

    @Benchmark
    @OperationsPerInvocation(10_000)
    public int measureWrong_10000() {
    
     //循环相加
        return reps(10_000);
    }

    @Benchmark
    @OperationsPerInvocation(100_000)
    public int measureWrong_100000() {
    
     //循环相加
        return reps(100_000);
    }


    public static void main(String[] args) throws RunnerException {
    
    
        Options opt = new OptionsBuilder()
                .include(JMHSample_11_Loops.class.getSimpleName())
                .forks(1)
                .build();

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

在这里插入图片描述

13.@fork (线程)

线程 fork(0)


package org.openjdk.jmh.samples;

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.lang.management.ManagementFactory;
import java.util.concurrent.TimeUnit;

/**
 *  我们发现进程pid是一样的,初步推测fork(0),代表没有进程新建
 */

@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 1, time = 1, timeUnit = TimeUnit.SECONDS)  //预热次数和时间
@Measurement(iterations = 1, time = 1, timeUnit = TimeUnit.SECONDS) //测试次数和时间
public class JMHSample_12_Forking {
    
    


    @Benchmark
    @Fork(0)  //通过源码我们可以直到,0代表 no fork
    public int measure_1_c1() {
    
    
        return 1;
    }

    @Setup(Level.Trial)  // 在基准执行之前先打印pid。
    public void setup() {
    
    
        printProcessID("setup");
    }

    public static void printProcessID(String name) {
    
      //打印pid
        System.out.println();
        System.out.println("--------------");
        System.out.println(name + " pid is : " + ManagementFactory.getRuntimeMXBean().getName());
        System.out.println("--------------");
        System.out.println();
    }


    public static void main(String[] args) throws RunnerException {
    
    
        printProcessID("main");  // 执行main方法的时候打印pid
        Options opt = new OptionsBuilder()
                .include(JMHSample_12_Forking.class.getSimpleName())
                .build();

        new Runner(opt).run();  //当他启动的时候,才会开始运行基准
    }
}

在这里插入图片描述

线程fork(1)


package org.openjdk.jmh.samples;

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.lang.management.ManagementFactory;
import java.util.concurrent.TimeUnit;

/**
 *  我们发现进程pid是一样的,初步推测fork(1),代表新开一个进程
 */

@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 1, time = 1, timeUnit = TimeUnit.SECONDS)  //预热次数和时间
@Measurement(iterations = 1, time = 1, timeUnit = TimeUnit.SECONDS) //测试次数和时间
public class JMHSample_12_Forking {
    
    


    @Benchmark
    @Fork(1)  //通过源码我们可以直到,1代表 1 fork
    public int measure_1_c1() {
    
    
        return 1;
    }

    @Setup(Level.Trial)  // 在基准执行之前先打印pid。
    public void setup() {
    
    
        printProcessID("setup");
    }

    public static void printProcessID(String name) {
    
      //打印pid
        System.out.println();
        System.out.println("--------------");
        System.out.println(name + " pid is : " + ManagementFactory.getRuntimeMXBean().getName());
        System.out.println("--------------");
        System.out.println();
    }


    public static void main(String[] args) throws RunnerException {
    
    
        printProcessID("main");  // 执行main方法的时候打印pid
        Options opt = new OptionsBuilder()
                .include(JMHSample_12_Forking.class.getSimpleName())
                .build();

        new Runner(opt).run();  //当他启动的时候,才会开始运行基准
    }

}

在这里插入图片描述

线程 fork(10)


package org.openjdk.jmh.samples;

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.lang.management.ManagementFactory;
import java.util.concurrent.TimeUnit;

/**
 *  我们发现进程pid是一样的,初步推测fork(10),代表新开10个进程
 */

@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 1, time = 1, timeUnit = TimeUnit.SECONDS)  //预热次数和时间
@Measurement(iterations = 1, time = 1, timeUnit = TimeUnit.SECONDS) //测试次数和时间
public class JMHSample_12_Forking {
    
    


    @Benchmark
    @Fork(10)  //通过源码我们可以直到,10代表 10 fork
    public int measure_1_c1() {
    
    
        return 1;
    }

    @Setup(Level.Trial)  // 在基准执行之前先打印pid。
    public void setup() {
    
    
        printProcessID("setup");
    }

    public static void printProcessID(String name) {
    
      //打印pid
        System.out.println();
        System.out.println("--------------");
        System.out.println(name + " pid is : " + ManagementFactory.getRuntimeMXBean().getName());
        System.out.println("--------------");
        System.out.println();
    }


    public static void main(String[] args) throws RunnerException {
    
    
        printProcessID("main");  // 执行main方法的时候打印pid
        Options opt = new OptionsBuilder()
                .include(JMHSample_12_Forking.class.getSimpleName())
                .build();

        new Runner(opt).run();  //当他启动的时候,才会开始运行基准
    }

}

在这里插入图片描述

fork() 默认值是-1,但效果等于传5

package org.openjdk.jmh.samples;

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.lang.management.ManagementFactory;
import java.util.concurrent.TimeUnit;

/**
 *  我们发现进程pid是一样的,初步推测fork),代表新开5个进程
 */

@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 1, time = 1, timeUnit = TimeUnit.SECONDS)  //预热次数和时间
@Measurement(iterations = 1, time = 1, timeUnit = TimeUnit.SECONDS) //测试次数和时间
public class JMHSample_12_Forking {
    
    


    @Benchmark
    @Fork()  //默认是-1 实际传5
    public int measure_1_c1() {
    
    
        return 1;
    }

    @Setup(Level.Trial)  // 在基准执行之前先打印pid。
    public void setup() {
    
    
        printProcessID("setup");
    }

    public static void printProcessID(String name) {
    
      //打印pid
        System.out.println();
        System.out.println("--------------");
        System.out.println(name + " pid is : " + ManagementFactory.getRuntimeMXBean().getName());
        System.out.println("--------------");
        System.out.println();
    }


    public static void main(String[] args) throws RunnerException {
    
    
        printProcessID("main");  // 执行main方法的时候打印pid
        Options opt = new OptionsBuilder()
                .include(JMHSample_12_Forking.class.getSimpleName())
                .build();

        new Runner(opt).run();  //当他启动的时候,才会开始运行基准
    }
}

在这里插入图片描述

forking 源码

在同一个JVM中,不同的基准会相互影响的。


package org.openjdk.jmh.samples;

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.concurrent.TimeUnit;

/**
 * 在同一个JVM线程中,我们发现各个基准是相互影响的
 */

@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 1,time = 1,timeUnit = TimeUnit.SECONDS)  //预热次数和时间
@Measurement(iterations = 1,time = 1,timeUnit = TimeUnit.SECONDS) //测试次数和时间
public class JMHSample_12_Forking {
    
    

    public interface Counter {
    
      //匿名内部类
        int inc();
    }

    public static class Counter1 implements Counter {
    
     //实现接口1
        private int x;

        @Override
        public int inc() {
    
    
            return x++;
        }
    }

    public static class Counter2 implements Counter {
    
      //实现接口2
        private int x;

        @Override
        public int inc() {
    
    
            return x++;
        }
    }



    public int measure(Counter c) {
    
      //方法
        int s = 0;
        for (int i = 0; i < 10; i++) {
    
    
            s += c.inc();
        }
        return s;
    }


    Counter c1 = new Counter1();
    Counter c2 = new Counter2();



    @Benchmark
    @Fork(0)
    public int measure_1_c1() {
    
      //与main方法同进程
        return measure(c1);
    }


    @Benchmark
    @Fork(0)
    public int measure_2_c2() {
    
     //与main方法同进程
        return measure(c2);
    }



    @Benchmark
    @Fork(0)
    public int measure_3_c1_again() {
    
     //与main方法同进程
        return measure(c1);
    }



    @Benchmark
    @Fork(1)
    public int measure_4_forked_c1() {
    
    //与main方法不同进程
        return measure(c1);
    }


    @Benchmark
    @Fork(1)
    public int measure_5_forked_c2() {
    
     //与main方法不同进程
        return measure(c2);
    }



    public static void main(String[] args) throws RunnerException {
    
    
        Options opt = new OptionsBuilder()
                .include(JMHSample_12_Forking.class.getSimpleName())
                .build();

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

在这里插入图片描述

14.什么时候使用fork

大数样本: 大数法则当我们的样本足够多的时候,我们抽取的越多,那么就越接近平均值。 所以我们多开点线程就可以多取样本。



package org.openjdk.jmh.samples;

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.concurrent.TimeUnit;

/**
 *   大数样本: 大数法则当我们的样本足够多的时候,我们抽取的越多,那么就越接近平均值。
 */
@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Warmup(iterations = 1,time = 1,timeUnit = TimeUnit.SECONDS)  //预热次数和时间
@Measurement(iterations = 1,time = 1,timeUnit = TimeUnit.SECONDS) //测试次数和时间
public class JMHSample_13_RunToRun {
    
    


    @State(Scope.Thread)
    public static class SleepyState {
    
      //定义一个静态内部类
        public long sleepTime;

        @Setup
        public void setup() {
    
    
            sleepTime = (long) (Math.random() * 1000);
            System.out.println("---------------");
            System.out.println("sleepTime-> "+sleepTime);
            System.out.println("---------------");
        }  //生成一个随机休眠数
    }

    /*
     * Now, we will run this different number of times.
     */

    @Benchmark
    @Fork(1)
    public void baseline(SleepyState s) throws InterruptedException {
    
    
        TimeUnit.MILLISECONDS.sleep(s.sleepTime);
    }

    @Benchmark
    @Fork(5)
    public void fork_1(SleepyState s) throws InterruptedException {
    
    
        TimeUnit.MILLISECONDS.sleep(s.sleepTime);
    }

    @Benchmark
    @Fork(20)
    public void fork_2(SleepyState s) throws InterruptedException {
    
    
        TimeUnit.MILLISECONDS.sleep(s.sleepTime);
    }


    public static void main(String[] args) throws RunnerException {
    
    
        Options opt = new OptionsBuilder()
                .include(JMHSample_13_RunToRun.class.getSimpleName())
                .warmupIterations(0)
                .measurementIterations(3)
                .build();

        new Runner(opt).run();
    }

}

在这里插入图片描述

15. @Group和@GroupThreads (组数和线程)

可以更加的接近生产环境

源码

@Group("g")  // 假如这个注解里面的字符串相同,那就说明它们是一组
 @GroupThreads(3)  // 这个地方会创建三个线程进行执行

package org.openjdk.jmh.samples;

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

@State(Scope.Group)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 1,time = 1,timeUnit = TimeUnit.SECONDS)  //预热次数和时间
@Measurement(iterations = 1,time = 1,timeUnit = TimeUnit.SECONDS) //测试次数和时间
public class JMHSample_15_Asymmetric {
    
    

    private AtomicInteger counter;

    @Setup  //基准初始化前
    public void up() {
    
    
        counter = new AtomicInteger(0);
    }

    @Benchmark
    @Group("g")  // 假如这个注解里面的字符串相同,那就说明它们是一组
    @GroupThreads(1)  // 这个地方会创建三个线程进行执行
    public int inc() {
    
    
        return counter.incrementAndGet();  //自增的操作
    }

    @Benchmark
    @Group("g") // 假如这个注解里面的字符串相同,那就说明它们是一组
    @GroupThreads(1) // 这个地方会创建三个线程进行执行
    public int get() {
    
    
        return counter.get();  //获取值的操作
    }


    public static void main(String[] args) throws RunnerException {
    
    
        Options opt = new OptionsBuilder()
                .include(JMHSample_15_Asymmetric.class.getSimpleName())
                .forks(1)
                .build();

        new Runner(opt).run();
    }

}

总的
Benchmark Mode Cnt Score Error Units
JMHSample_15_Asymmetric.g avgt 56.784 ns/op
JMHSample_15_Asymmetric.g:get avgt 26.845 ns/op
JMHSample_15_Asymmetric.g:inc avgt 66.764 ns/op

单独执行 get
Benchmark Mode Cnt Score Error Units
JMHSample_15_Asymmetric.g avgt 1.986 ns/op

单独执行inc 3个线程

Benchmark                  Mode  Cnt   Score   Error  Units
JMHSample_15_Asymmetric.g  avgt       40.330          ns/op

单独执行inc 1个线程
Benchmark Mode Cnt Score Error Units
JMHSample_15_Asymmetric.g avgt 6.659 ns/op

在这里插入图片描述

(四)、JMH实战

1.导入三个依赖

    <dependency>
        <groupId>org.openjdk.jmh</groupId>
        <artifactId>jmh-core</artifactId>
        <version>1.23</version>
    </dependency>
 
    <dependency>
        <groupId>org.openjdk.jmh</groupId>
        <artifactId>jmh-generator-annprocess</artifactId>
        <version>1.23</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/cn.hutool/hutool-all -->
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.5</version>
</dependency>

2.比较冒泡排序和快速排序

package com.example.springboot01hello;

import org.junit.jupiter.api.Test;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.Arrays;
import java.util.concurrent.TimeUnit;


@SpringBootTest
@State(Scope.Thread)
@Warmup(iterations = 1,time = 1,timeUnit = TimeUnit.SECONDS)  //预热次数和时间
@Measurement(iterations = 1,time = 1,timeUnit = TimeUnit.SECONDS) //测试次数和时间
public class SpringBoot01HelloApplicationTests {
    
    

    int array[]={
    
    1,2,5,3,6,9};
    @Benchmark
    public void wellHelloThere1() throws Exception {
    
    

        sort(array);
    }
    @Benchmark
    public void wellHelloThere2() throws Exception {
    
    
        sort(array);
    }
    public int[] sort(int[] sourceArray) throws Exception {
    
    
        // 对 arr 进行拷贝,不改变参数内容
        int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);

        for (int i = 1; i < arr.length; i++) {
    
    
            // 设定一个标记,若为true,则表示此次循环没有进行交换,也就是待排序列已经有序,排序已经完成。
            boolean flag = true;

            for (int j = 0; j < arr.length - i; j++) {
    
    
                if (arr[j] > arr[j + 1]) {
    
    
                    int tmp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = tmp;

                    flag = false;
                }
            }

            if (flag) {
    
    
                break;
            }
        }
        return arr;
    }

    public int[] sort1(int[] sourceArray) throws Exception {
    
    
        // 对 arr 进行拷贝,不改变参数内容
        int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);

        // 从下标为1的元素开始选择合适的位置插入,因为下标为0的只有一个元素,默认是有序的
        for (int i = 1; i < arr.length; i++) {
    
    

            // 记录要插入的数据
            int tmp = arr[i];

            // 从已经排序的序列最右边的开始比较,找到比其小的数
            int j = i;
            while (j > 0 && tmp < arr[j - 1]) {
    
    
                arr[j] = arr[j - 1];
                j--;
            }

            // 存在比其小的数,插入
            if (j != i) {
    
    
                arr[j] = tmp;
            }

        }
        return arr;
    }

    @Test
    void contextLoads() throws RunnerException {
    
    
            Options opt = new OptionsBuilder()
                    .include(SpringBoot01HelloApplicationTests.class.getSimpleName())
                    .forks(1)
                    .build();
            new Runner(opt).run();
        }

    }

在这里插入图片描述

3.报表可视化

配置

            Options opt = new OptionsBuilder()
                    .include(SpringBoot01HelloApplicationTests.class.getSimpleName())
                    .forks(1)
                    .resultFormat(ResultFormatType.JSON)  //报表需要一个JSON文件
                    .result("/E:/加速器/result.json")  //报表存放的位置
                    .build();
            new Runner(opt).run();

可视化工具链接: https://jmh.morethan.io/

把生成的json文件拖拽到这里就可以可视化了。
在这里插入图片描述

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_69683957/article/details/129631754