JMH
- (一)、JMH概述
- 1.什么是JMH?
- 2.JMH入门操作
- (1).没有JMH的时候我们怎么进行测试的?
- (2).利用JMH进行测试
- (二)、JMH运用展示
- 1.Hello JMH
- (三)、JMH注解
- 1.@Warmip和@Measurement (预热和真实执行)【类】
- 2.@BenchmarkMode和@OutputTimeUnit (输出方式和输出单位)【方法】
- 3.@State (状态)【类】
- 4.DefaultState (默认状态)【类】
- 5.@Setup和@TearDown (初始化和销毁)【方法】
- 6.FixtureLevel (初始化和销毁等级)【方法】
- 7.FixtureLevelInvocation (固定级别调用)
- 8.DeadCode (JVM调优)
- 9.Blackholes (黑洞拒绝JVM调优)
- 10.ConstantFold (常量折叠)
- 11.@OperationsPerInvocation (调用次数)
- 12.Loops (循环)
- 13.@fork (线程)
- 14.什么时候使用fork
- 15. @Group和@GroupThreads (组数和线程)
- (四)、JMH实战
- 1.导入三个依赖
- 2.比较冒泡排序和快速排序
- 3.报表可视化
(一)、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的时候我们怎么进行测试的?
我们利用结束的时间戳 - 结束后的时间戳
@Testvoid 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(); //执行}}
解释
(三)、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个线程去一起执行@Benchmarkpublic void measureUnshared(ThreadState state) { //每一个线程的入参都是不同的对象state.x++;}// 所有测试共享一个实列,用于测试有状态实列在多线程共享下的性能// 一般用来测试多线程竞争下的性能@State(Scope.Benchmark) //整个测试总共用这一个对象public static class BenchmarkState {volatile double x = Math.PI;}@Benchmarkpublic 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;// 我们使用默认静态类的主要目的是: 我们不用特地的去写一个静态内部类去声明一个入参。如果没有上面的注解那么我们就需要声明一个静态内部类@Benchmarkpublic 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?";}@Benchmarkpublic void measureRight() { //正确代码,正常执行x++;}@Benchmarkpublic 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?";}@Benchmarkpublic void measureRight() {x++;}@Benchmarkpublic 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;}@Overridepublic 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;}@Benchmarkpublic void baseline() {// do nothing, this is a baseline}@Benchmarkpublic void measureWrong() {// This is wrong: result is not used and the entire computation is optimized away.compute(x); //因为我们没有使用计算结果,JVM会自动把这段代码优化掉,相当于测试了一个空方法。}@Benchmarkpublic 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调优)
黑洞: 可以有效的避免过于激进的优化
源码
@Benchmarkpublic 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;}@Benchmarkpublic double baseline() { //整一个,且真实有效的只有一个return compute(x1);}@Benchmarkpublic double measureWrong() { //整两个,但真正有效值为1个compute(x1); //编译器自动识别,直接被JVM优化掉return compute(x2);}@Benchmarkpublic double measureRight_1() { //整两个且真实计算的有2个return compute(x1) + compute(x2);}@Benchmarkpublic 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;}@Benchmarkpublic double baseline() { // 获取常量// simply return the value, this is a baselinereturn Math.PI;}@Benchmarkpublic double measureWrong_1() { //计算常量// This is wrong: the source is predictable, and computation is foldable.return compute(Math.PI);}@Benchmarkpublic double measureWrong_2() { // 计算常量// This is wrong: the source is predictable, and computation is foldable.return compute(wrongX);}@Benchmarkpublic 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;@Benchmarkpublic 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 forkpublic int measure_1_c1() {return 1;}@Setup(Level.Trial) // 在基准执行之前先打印pid。public void setup() {printProcessID("setup");}public static void printProcessID(String name) { //打印pidSystem.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方法的时候打印pidOptions 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 forkpublic int measure_1_c1() {return 1;}@Setup(Level.Trial) // 在基准执行之前先打印pid。public void setup() {printProcessID("setup");}public static void printProcessID(String name) { //打印pidSystem.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方法的时候打印pidOptions 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 forkpublic int measure_1_c1() {return 1;}@Setup(Level.Trial) // 在基准执行之前先打印pid。public void setup() {printProcessID("setup");}public static void printProcessID(String name) { //打印pidSystem.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方法的时候打印pidOptions 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 实际传5public int measure_1_c1() {return 1;}@Setup(Level.Trial) // 在基准执行之前先打印pid。public void setup() {printProcessID("setup");}public static void printProcessID(String name) { //打印pidSystem.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方法的时候打印pidOptions 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 { //实现接口1private int x;@Overridepublic int inc() {return x++;}}public static class Counter2 implements Counter { //实现接口2private int x;@Overridepublic 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;@Setuppublic 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};@Benchmarkpublic void wellHelloThere1() throws Exception {sort(array);}@Benchmarkpublic 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;}@Testvoid 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文件拖拽到这里就可以可视化了。