一、前言
清明节在家的时候,有个老弟在一个群里看到一段代码。
package com.cache.mycache;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.results.format.ResultFormatType;
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.HashMap;
import java.util.Random;
import java.util.concurrent.TimeUnit;
@BenchmarkMode({Mode.Throughput})
@State(value = Scope.Benchmark)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public class ConcurrentSetJMH {
private static final int SIZE = (2 << 14);
private static final HashMap<Integer, Item> map = new HashMap<>();
public ConcurrentSetJMH() {
for (int i = 0; i < SIZE; i++) {
map.put(i, new Item(i));
}
}
@Benchmark
@Threads(32)
public void sameKey(ThreadState threadState) {
threadState.sameItem.setValue(threadState.value);
}
@Benchmark
@Threads(32)
public void spreadKey(ThreadState threadState) {
threadState.spreadItem.setValue(threadState.value);
}
@State(Scope.Thread)
public static class ThreadState {
static final Random random = new Random();
int value = random.nextInt();
int index = value & (SIZE - 1);
Item spreadItem = map.get(index);
Item sameItem = map.get(SIZE / 2);
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(ConcurrentSetJMH.class.getSimpleName())
.result("ComputeBenchMark.json")
.resultFormat(ResultFormatType.JSON).build();
new Runner(opt).run();
}
private static final class Item {
private int value;
Item(int value) {
this.value = value;
}
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
}
}
老弟大呼,这是什么玩意,为什么我没见过这样的代码,我不会咋办啊。橘子哥,你知道这是啥吗。我安抚了一下他焦虑的内心,告诉他,别慌,JMH而已,不是什么黑科技。他说他想学这个。我突然想起,我还有个博客,好像还有个JDK的专栏,那就写一点这玩意的用法吧。
二、关于测试
JMH实际上能用到的地方很多,我先说一个学JMH常见的开场白,大家在实际开发中遇到自己优化了一段代码的时候一般都想测一下这段代码的运行效率如何,和旧的代码相比优化效率点在哪里。
一般情况下,我们都会很自然的写一段main函数,然后在代码开始输出当前时间,然后在代码结束之后输出当前时间,两个时间一做减法,得到程序运行时间,最后一比较哦,优化了,有时候明明优化了但是看时间反而长了。
这种方法,有点小low。
其实也不是low,有时候简单的测一下完全可以,但是很多时候,这种方式不对。因为java这玩意,jvm有个JIT的编译器,这玩意是运行时优化的,有的程序在被JIT判定为热点代码的时候,是会拿出来单独优化,然后性能得以提升。你这种直接上去测,压根走不到JIT,自然也就不能真实模拟了。
其次,你得这种方式,每次都跑一次,CPU利用不到真实场合。总之就是各种指标他都对不上。导致你这个测试,不准确。
所以就有了JMH这个东西。
三、关于JMH
我们先来看一下他的官网介绍。
JMH官方地址
官方文档开篇明义:
全称为:Java Microbenchmark Harness
JMH is a Java harness for building, running, and analysing nano/micro/milli/macro benchmarks written in Java and other languages targeting the JVM.
翻译一下就是: JMH 是一个 Java 工具,用于构建、运行和分析用 Java 和其他针对 JVM 的语言编写的纳/微米/毫/宏观基准测试。
何谓 Micro Benchmark 呢?简单地说就是在 method 层面上的 benchmark,精度可以精确到微秒级。
# Java 的基准测试需要注意的几个点:
测试前需要预热
防止无用代码进入测试方法中
并发测试
测试结果呈现
# JMH 的使用场景:
定量分析某个热点函数的优化效果
想定量地知道某个函数需要执行多长时间,以及执行时间和输入变量的相关性
对比一个函数的多种实现方式
其实吧,你也不需要看我这个文章,因为官方文档写了很多很多的例子,上面有详细的英文注释,你都可以参考一下。毕竟官方文档的准确度必然是可信的。当然,你觉得英文费劲,喜欢看我这种写的大白话的,那也可以。
JMH官方例子
OK,至此你已经知道了这玩意到底是个啥,以及他到底能做啥了。下面我们就来看看他到底咋用。
四、使用JMH
1、集成JMH
这个东西的集成方式有很多种,都在官网上面有介绍,这里我们以IDEA的使用方式开始介绍,很简单。
生成 jar 文件的形式主要是针对一些比较大的测试,可能对机器性能或者真实环境模拟有一些需求,需要将测试方法写好了放在 linux 环境执行。可以打成jar包,在服务器上执行,也是支持的,比如模拟真实环境,就可以在服务器上运行jar,官网首页都有介绍。
具体操作就是:
$ mvn clean install
$ java -jar target/benchmarks.jar
这里我们模拟本地的IDE执行操作:
step1:创建一个maven项目,引入maven依赖。
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.35</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.35</version>
<scope>provided</scope>
</dependency>
然后我们就可以来使用他的一些操作了。这里说几个常见的问题。
1、你创建测试类的时候,一定要自己建个包,比如这样,不要直接写在java目录下面,有权限错误。
2、你启动测试类的时候最好是不要用debug模式启动,会有异常,本身就是性能测试,你debug是准备打断点还是咋。
2、使用JMH
我们使用的方式也很直白,就是直接怼官方那几个例子,我再用大白话翻译一下例子的内容,总结出来使用的一些方法和含义,毕竟官方的才是坠吼的。
2.1、sample1
示例1的官方地址
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();
}
}
你看这个方法,很简单,它这个是个入门案例,毕竟第一个不要太复杂,先看个大纲,我们先来看一下这个案例,它分为两部分。
第一部分是一个方法名为wellHelloThere,方法什么都没有实现,是一个空方法。只是上面标注了一个注解**@Benchmark**。这个注解的意思就是表示对加这个注解的方法进行测试。
第二部分是一个main函数,里面看上去像是创建了一个对象,然后run了一下,我们先来看一下他的输出再来具体看。记住最好别用debug模式运行,有时候会报错。
最后会在控制台输出这么一段内容。我加点注释,后面用#写的汉字就是我的注释。
# JMH的版本,这个就是你maven文件引入的那个版本
# JMH version: 1.35
# JDK的版本
# VM version: JDK 11.0.22, Java HotSpot(TM) 64-Bit Server VM, 11.0.22+9-LTS-219
# JDK的安装位置
# VM invoker: D:\env-soft\java\jdk11\jdk\bin\java.exe
# JVM的参数,现在我啥参数没加,都是默认的,后面可以JVM调优看一下影响
# VM options: -javaagent:D:\Program Files (x86)\IntelliJ IDEA 2021.2\lib\idea_rt.jar=57542:D:\Program Files (x86)\IntelliJ IDEA 2021.2\bin -Dfile.encoding=UTF-8
# 这个目前还没用到,回头说, Blackhole mode是黑洞模式
# Blackhole mode: full + dont-inline hint (auto-detected, use -Djmh.blackhole.autoDetect=false to disable)
# 以下的都是比较重要的参数
# Warmup 表示预热,就是在正式进行测试你的加上@Benchmark的方法之前,做一次预热,运行起来,避免JIT的影响,这里5 iterations,10 s each表示预热要进行五轮,也就是要跑五个轮次,每个轮次跑十秒。注意这里,不是跑五次这个方法,而是五轮,每轮十秒,你这个空方法,每轮十秒下来能跑的次数那可大了去了。
# Warmup: 5 iterations, 10 s each
# 正式执行的次数,也是跑五轮,每轮跑十秒进行测试
# Measurement: 5 iterations, 10 s each
# 超时时间,每一轮各自的超时时间就是十分钟,过了十分钟还没跑完也结束了
# Timeout: 10 min per iteration
# 线程数,默认是1个线程跑,我们可以调整到时候,一个线程跑五轮不管是预热还是真实测试是,那都是串行执行的了,所以你看到下面的输出指标都是串行的。有时候你验证并发执行,可以调整一下。
# Threads: 1 thread, will synchronize iterations
# 输出模式为吞吐量模式,ops/time表示每秒执行多少次,吞吐量的模式
# Benchmark mode: Throughput, ops/time
# 执行的打@Benchmark注解的方法,com.levi.JMHSample_01_HelloWorld我们这个类只有wellHelloThere这一个方法加了@Benchmark注解,所以就执行他,这里其实可以看出来,你要是多个@Benchmark的方法,他会依次都执行,后面我们会看到多个的。
# Benchmark: com.levi.JMHSample_01_HelloWorld.wellHelloThere
# 这里我们先跳过,后面会介绍这里,先看大纲
# Run progress: 0.00% complete, ETA 00:01:40
# Fork: 1 of 1
# Warmup Iteration 1: 2506700198.155 ops/s
# Warmup Iteration 2: 2611047451.631 ops/s
# Warmup Iteration 3: 2555255646.980 ops/s
# Warmup Iteration 4: 2678825759.068 ops/s
# Warmup Iteration 5: 2803474171.878 ops/s
Iteration 1: 2791113907.236 ops/s
Iteration 2: 2779228951.815 ops/s
Iteration 3: 2724339734.541 ops/s
Iteration 4: 2758381926.323 ops/s
Iteration 5: 2758514249.394 ops/s
测试的结果报告如下:
Result “com.levi.JMHSample_01_HelloWorld.wellHelloThere”:
2762315753.862 ±(99.9%) 误差上下浮动百分之九十九的时候,每秒2762315753次
97936160.313 ops/s [Average] 平均吞吐
(min, avg, max) = (2724339734.541, 2762315753.862, 2791113907.236) 最小,平均,最大执行次数
stdev = 25433709.824 这是标准差
CI (99.9%): [2664379593.548, 2860251914.175]:百分之九十九的吞吐区间,可以看到大部分的时候执行结果
这个最后的统计结果是我们要关注的,我用分开每一行展示,不然会错行。
下面我分开行展示:
Benchmark :JMHSample_01_HelloWorld.wellHelloThere 表示你测试的方法,我们就一个加@Benchmark的方法,所以就一个。
Mode:thrpt 表示是以吞吐量模式测试的,也就是每秒执行几次 。
Cnt:5 表示执行的轮次,我们用的是默认的,就是5次。
Score:2762315753.862 ± 97936160.313 表示你本次测试的评分,越高越高,你可以把多种实现测完对分数做一下对比,就能看出性能了,其实这里我理解还是你的吞吐量,越高肯定性能越好。
Error:这是报错次数,有时候并发的时候,或者网络波动可能会有错,我们这里空方法,肯定没错。
Units:ops/s 输出结果的单位,其实我理解就是你分数的单位,分数本身就是一个吞吐量。
五、缩短时间
我们在测试的时候,发现一个问题,就是很慢才能运行出来结果,而且看控制台时间基本消耗在预热的五轮执行和正式执行的时候的五轮执行。所以我们这里可以使用一些注解来修改一下默认的这五轮开销,让我们看起来更迅速的得到结果,注意我们这里只是为了测试更快些,实际开发你要是做测试,需要自己判断要改大还是改小,比如你想充分预热,那就可以调大。这里只是说一下用法。
// 预热注解,修改为只预热一轮,每轮只跑一秒,默认是5,5这里改为1,1
@Warmup(iterations = 1,time = 1,timeUnit = TimeUnit.SECONDS)
// 测试执行注解,修改为只执行一轮,每轮只跑一秒,默认是5,5这里改为1,1
@Measurement(iterations = 1,time = 1,timeUnit = TimeUnit.SECONDS)
所以最后就变为这样。
// 预热注解,修改为只预热一轮,每轮只跑一秒,默认是5,5这里改为1,1
@Warmup(iterations = 1,time = 1,timeUnit = TimeUnit.SECONDS)
// 测试执行注解,修改为只执行一轮,每轮只跑一秒,默认是5,5这里改为1,1
@Measurement(iterations = 1,time = 1,timeUnit = TimeUnit.SECONDS)
public class JMHSample_01_02_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_02_HelloWorld.class.getSimpleName())
.forks(1)
.build();
new Runner(opt).run();
}
}
这样修改只是为了观察方便,预热轮数变小必然预热不充分,执行轮数变小必然统计不是那么精确。实际生产记得不断调整观察。
六、总结
我们每一篇都会有一个总结,就是关于这些注解的使用和结果的观察。
不然内容分散在文中,有时候不太适合观察。
1、注解
@Warmup:用于方法和类上,预热注解,加在类上对所有@Benchmark生效,加在方法上对指定方法生效。
默认为预热五轮,每轮执行10秒。可以通过参数修改。
iterations:预热执行轮数
time:预热每轮执行时间
timeUnit:执行时间的单位,默认为秒
@Measurement:测试执行注解,用于方法和类上,测试执行注解,加在类上对所有@Benchmark生效,加在方法上对指定方法生效。
iterations:预热执行轮数
time:预热每轮执行时间
timeUnit:执行时间的单位,默认为秒
@Benchmark:用在方法上,被这个注解标识的方法表示要被测试跑的方法。
七、相关链接
八、参考链接
1、OpenJdk官方文档
2、https://www.cnblogs.com/kiwifly/p/11477153.html
3、https://blog.csdn.net/u012060033/article/details/130244468
4、https://www.bilibili.com/video/BV1aT41177QZ?p=6&vd_source=ed2188454468b8a76f32cc2e7188243c