JMH,全称Java Microbenchmark Harness (微基准测试框架),是专门用于Java代码微基准测试的一套测试工具API,是由Java虚拟机团队开发的的,一般用于代码的性能调优。
BenchMark又叫做基准测试,主要用来测试一些方法的性能,可以根据不同的参数以不同的单位进行计算(例如可以使用吞吐量为单位,也可以使用平均时间作为单位,在 BenchmarkMode 里面进行调整)。
MicroBenchmark就是在method层面上的benchmark,精度可以精确到微秒级、甚至可以达到纳秒级别,适用于 java 以及其他基于 JVM 的语言。与Apache JMeter 不同,JMH 测试的对象可以是任一方法,颗粒度更小,而不仅限于接口以及API层面。
JMH的使用可以参考官方示例。
在maven的配置文件中增加如下依赖,最新的依赖版本可以参考:
<dependency>
<groupId>org.openjdk.jmhgroupId>
<artifactId>jmh-coreartifactId>
<version>1.35version>
dependency>
<dependency>
<groupId>org.openjdk.jmhgroupId>
<artifactId>jmh-generator-annprocessartifactId>
<version>1.35version>
dependency>
JMH主要是通过注解的形式编写测试单元,告诉JMH如何测试,JMH自动生成测试代码,所以在使用JMH进行微基准测试时一定要先了对JMH注解有一定了解,下面就介绍下JMH的注解。
@Benchmark用于告诉JMH哪些方法需要进行测试,只能注解在方法上,JMH会针对注解了@Benchmark的方法生成Benchmark方法代码。通常情况下,每个Benchmark方法都运行在独立的进程中,互不干涉。
@Benchmark
public Object testString(BenchmarkState state) {
return state.str.replace(TGT, REPLACEMENT);
}
@Benchmark
public Object testStringUtils(BenchmarkState state) {
return StringUtils.replace(state.str, TGT, REPLACEMENT);
}
@Benchmark
public Object testLang3StringUtils(BenchmarkState state) {
return org.apache.commons.lang3.StringUtils.replace(state.str, TGT, REPLACEMENT);
}
方法注解,表示该方法是需要进行 benchmark 的对象。使用@BenchmarkMode 指定测试模式,@BenchmarkMode用于指定当前Benchmark方法使用哪种模式测试。JMH提供了4种不同的模式,用于输出不同的结果指标,如下:
@Benchmark
@BenchmarkMode(Mode.Throughput) // 吞吐量
public void measureThroughput() throws InterruptedException {
/* 仅测试吞吐量 */
TimeUnit.MILLISECONDS.sleep(100);
}
@Benchmark
@BenchmarkMode({Mode.Throughput, Mode.AverageTime, Mode.SampleTime})
public void measureMultiple() throws InterruptedException {
/* 测试吞吐量、平均时间和抽样时间 */
TimeUnit.MILLISECONDS.sleep(100);
}
@Benchmark
@BenchmarkMode(Mode.All)
public void measureAll() throws InterruptedException {
/* 测试所有,即吞吐量、平均时间、抽样时间和启动时间 */
TimeUnit.MILLISECONDS.sleep(100);
}
单位中的 op 代表的是一次操作,默认一次操作指的是执行一次测试方法。但是我们可以指定调用多少次测试方法算作一次操作。在 JMH 中称作操作中的批处理次数,例如我们可以设置执行五次测试方法算作一次操作。
输出的时间单位,为统计结果的时间单位,可用于类或者方法注解。
@OutputTimeUnit(TimeUnit.MILLISECONDS) // 结果所使用的时间单位
public class JmhExample{}
Iteration 是 JMH 进行测试的最小单位。在大部分模式下,一次 iteration 代表的是一秒,JMH 会在这一秒内不断调用需要 Benchmark 的方法,然后根据模式对其采样,计算吞吐量,计算平均执行时间等。
Warmup是指在实际进行 Benchmark 前先进行预热的行为。
JVM 的JIT机制的存在,如果某个函数被调用多次之后,JVM 会尝试将其编译成为机器码从而提高执行速度。为了让 Benchmark 的结果更加接近真实情况就需要进行预热。
由于JVM会使用JIT即时编译器对热点代码进行编译,因此同一份代码可能由于执行次数的增加而导致执行时间差异太大,因此我们可以让代码先预热几轮,预热时间不算入测量计时。
@Warmup(iterations = 5) // 先预热5轮
public class JmhSeample {}
@Measurement 注解可作用于类或者方法上,用于指定测试的次数、时间和批处理数量,提供真正的测试阶段参数,指定迭代的次数,每次迭代的运行时间和每次迭代测试调用的数量。
@Measurement(iterations = 2) // 进行2轮测试
public class JmhSeample {}
@Warmup和@Measurement分别用于配置预热迭代和测试迭代。其中,iterations用于指定迭代次数,time和timeUnit用于每个迭代的时间,batchSize表示执行多少次Benchmark方法为一个invocation。
该注解修饰类,JMH测试类必须使用@State注解,它定义了一个类实例的生命周期,可以类比 Spring Bean 的 Scope。由于 JMH 允许多线程同时执行测试,不同的选项含义如下:
@State(Scope.Thread) // 每个测试线程分配一个实例
public class JMHSample {
public void prepare() {
System.err.println("init............");
}
}
方法注解,会在执行 benchmark 之前被执行,正如其名,主要用于初始化。
方法注解,与@Setup 相对的,会在所有 benchmark 执行结束以后执行,主要用于资源的回收等。@Setup/@TearDown注解使用Level参数来指定何时调用fixture。
| 名称 | 描述 |
| - | :-: | -: |
| Level.Trial | 默认level。Benchmark 开始前或结束后执行,如下。Level 为 Benchmark 的 Setup 和 TearDown 方法的开销不会计入到最终结果。 |
| Level.Iteration | Benchmark 里每个 Iteration 开始前或结束后执行,Level 为 Iteration 的 Setup 和 TearDown 方法的开销不会计入到最终结果。 |
| Level.Invocation | Iteration 里每次方法调用开始前或结束后执行,如Level 为 Invocation 的 Setup 和 TearDown 方法的开销将计入到最终结果。 |
@State(Scope.Thread)
public class JmhSample22 {
@Setup(Level.Iteration)
public void prepare() {
System.err.println("init............");
}
@TearDown(Level.Iteration)
public void check() {
System.err.println("destroy............");
}
@Benchmark
public void measureRight() {
x++;
}
}
进行 fork 的次数。如果 fork 数是2的话,则 JMH 会 fork 出两个进程来进行测试。
@Fork(2) // Fork进行的数目
public class BenchMark {}
每个进程中的测试线程,可用于类或者方法上。
@Threads(4)
public class JmhTest {}
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 3, time = 1)
@Measurement(iterations = 5, time = 5)
@Threads(4)
@Fork(1)
@State(value = Scope.Benchmark)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class JmhTest {}
成员注解,可以用来指定某项参数的多种情况。特别适合用来测试一个函数在不同的参数输入的情况下的性能。@Param 注解接收一个String数组,在 @Setup 方法执行前转化为为对应的数据类型。多个 @Param 注解的成员之间是乘积关系,譬如有两个用 @Param 注解的字段,第一个有5个值,第二个字段有2个值,那么每个测试方法会跑5* 2=10次。
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 3, time = 1)
@Measurement(iterations = 5, time = 5)
@Threads(4)
@Fork(1)
@State(value = Scope.Benchmark)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class StringConnectTest {
@Param(value = {"10", "50", "100"})
private int length;
@Benchmark
public void testStringAdd(Blackhole blackhole) {
String a = "";
for (int i = 0; i < length; i++) {
a += i;
}
blackhole.consume(a);
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(StringConnectTest.class.getSimpleName());
new Runner(opt).run();
}
}
JMH 官方提供了生成 jar 包的方式来执行,我们需要在 maven 里增加一个 plugin,具体配置如下:
<plugins>
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-shade-pluginartifactId>
<version>2.4.1version>
<executions>
<execution>
<phase>packagephase>
<goals>
<goal>shadegoal>
goals>
<configuration>
<finalName>jmh-demofinalName>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>org.openjdk.jmh.MainmainClass>
transformer>
transformers>
configuration>
execution>
executions>
plugin>
plugins>