1.什么是jmh?
JMH是Java Microbenchmark Harness的简称,一个针对Java做基准测试的工具,是由开发JVM的那群人开发的。想准确的对一段代码做基准性能测试并不容易,因为JVM层面在编译期、运行时对代码做很多优化,但是当代码块处于整个系统中运行时这些优化并不一定会生效,从而产生错误的基准测试结果,而这个问题就是JMH要解决的。
Benchmark基本概念
Benchmark State
有时候我们在做基准测试的时候会需要使用一些变量、字段,@State注解是用来配置这些变量的生命周期,@State注解可以放在类上,然后在基准测试方法中可以通过参数的方式把该类对象作为参数使用。@State支持的生命周期类型:
- Benchmark: 整个基准测试的生命周期,多个线程共用同一份实例对象。该类内部的@Setup @TearDown注解的方法可能会被任一个线程执行,但是只会执行一次。
- Group: 每一个Group内部共享同一个实例,需要配合@Group @GroupThread使用。该类内部的@Setup @TearDown注解的方法可能会该Group内的任一个线程执行,但是只会执行一次。
- **Thread:**每个线程的实例都是不同的、唯一的。该类内部的@Setup @TearDown注解的方法只会被当前线程执行,而且只会执行一次。
被@State标示的类必须满足如下两个要求:
- 类必须是public的
- 必须有无参构造函数
State Object @Setup @TearDown
在@Scope注解标示的类的方法上可以添加@Setup和@TearDwon注解。@Setup:用来标示在Benchmark方法使用State对象之前需要执行的操作。@TearDown:用来标示在Benchmark方法之后需要对State对象执行的操作。 @Setup、@TearDown支持设置Level级别,Level有三个值:
- Trial: 每次benchmark前/后执行一次,每次benchmark会包含多轮(Iteration)
- Iteration: 每轮执行前/后执行一次
- Invocation: 每次调用测试的方法前/后都执行一次,这个执行频率会很高,一般用不上。
Fork
@Fork注解用来设置启动的JVM进程数量,多个进程是串行的方式启动的,多个进程可以减少偶发因素对测试结果的影响。
Thread
@Thread用来配置执行测试启动的线程数量
Warmup
@Warmup 用来配置预热的时间,如下所示配置预热五轮,每轮1second,也就是说总共会预热5s左右,在这5s内会不停的循环调用测试方法,但是预热时的数据不作为测试结果参考。
2.代码工程
实验目的
- 测试排序的吞吐量
- 测试字符串连接的的平均耗时
pom.xml
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>springboot-demo</artifactId>
<groupId>com.et</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>JMH</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.33</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.33</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
测试排序的吞吐量
java
package com.et.jmh;
import org.junit.Test;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
@State(Scope.Thread)
public class MyBenchmark {
private List<String> list;
@Setup
public void setup() {
list = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
list.add(UUID.randomUUID().toString());
}
}
@TearDown
public void tearDown() {
list = null;
}
@Benchmark
public void testSort() {
Collections.sort(list);
}
@Test
public void testMyBenchmark() throws Exception {
Options options = new OptionsBuilder()
.include(MyBenchmark.class.getSimpleName())
.forks(1)
.threads(1)
.warmupIterations(5)
.measurementIterations(5)
.mode(Mode.Throughput)
.build();
new Runner(options).run();
}
}
启动运行
yaml
# Run progress: 0.00% complete, ETA 00:01:40
# Fork: 1 of 1
# Warmup Iteration 1: 16881.851 ops/s
# Warmup Iteration 2: 17844.093 ops/s
# Warmup Iteration 3: 17076.747 ops/s
# Warmup Iteration 4: 16877.725 ops/s
# Warmup Iteration 5: 16480.613 ops/s
Iteration 1: 16697.580 ops/s
Iteration 2: 16620.370 ops/s
Iteration 3: 15644.794 ops/s
Iteration 4: 16373.904 ops/s
Iteration 5: 16518.710 ops/s
Result "com.et.jmh.MyBenchmark.testSort":
16371.072 ±(99.9%) 1631.467 ops/s [Average]
(min, avg, max) = (15644.794, 16371.072, 16697.580), stdev = 423.687
CI (99.9%): [14739.605, 18002.538] (assumes normal distribution)
# Run complete. Total time: 00:01:45
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
MyBenchmark.testSort thrpt 5 16371.072 ± 1631.467 ops/s
从上面的日志我们大致可以了解到 JMH的基准测试主要经历了下面几个过程:
- 打印本次测试的配置,warmup:5轮;measurement:5轮;每轮:10s;启动1个线程做测试;基准测试指标:吞吐量(throughput,单位是s);测试方法MyBenchmark.testSort
- 启动一个JVM进程做基准测试(也可以设置启动多个进程,减少随机因素的误差影响)
- 在JVM进程中先执行了5轮的预热(warmup),每轮10s,总共50s的预热时间。预热的数据不作为基准测试的参考。
- 测试了5轮,每轮10s,总共50s的测试时间
- 汇总测试数据、生成结果报表。最终结论是吞吐量(16371.072 ± 1631.467ops/s),其中16371.072 是结果,1631.467是误差范围。
字符串连接的的平均耗时
JMH benchmark支持如下几种测试模式:
- Throughput: 吞吐量,测试每秒可以执行操作的次数
- Average Time: 平均耗时,测试单次操作的平均耗时
- **Sample Time:**采样耗时,测试单次操作的耗时,包括最大、最小耗时,已经百分位耗时等
- Single Shot Time: 只计算一次的耗时,一般用来测试冷启动的性能(不设置JVM预热)
- All: 测试上面的所有指标
默认的benchmark mode是Throughput,可以通过注解的方式设置BenchmarkMode,注解支持放在类或方法上。如下所示设置了Throughput和SampleTime两个Benchmark mode。
typescript
package com.et.jmh;
import org.junit.Test;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
@State(Scope.Thread)
public class StringConcatBenchmark {
private String str1;
private String str2;
@Setup
public void setup() {
str1 = "Hello";
str2 = "World";
}
@TearDown
public void tearDown() {
str1 = null;
str2 = null;
}
@Benchmark
public String testStringConcat() {
return str1 + " " + str2;
}
@Test
public void testStringConcatBenchmark() throws Exception {
Options options = new OptionsBuilder()
.include(StringConcatBenchmark.class.getSimpleName())
.forks(1)
.threads(1)
.warmupIterations(5)
.measurementIterations(5)
.mode(Mode.AverageTime)
.build();
new Runner(options).run();
}
}
启动测试
bash
# Run progress: 0.00% complete, ETA 00:01:40
# Fork: 1 of 1
# Warmup Iteration 1: ≈ 10⁻⁸ s/op
# Warmup Iteration 2: ≈ 10⁻⁸ s/op
# Warmup Iteration 3: ≈ 10⁻⁸ s/op
# Warmup Iteration 4: ≈ 10⁻⁸ s/op
# Warmup Iteration 5: ≈ 10⁻⁸ s/op
Iteration 1: ≈ 10⁻⁸ s/op
Iteration 2: ≈ 10⁻⁸ s/op
Iteration 3: ≈ 10⁻⁸ s/op
Iteration 4: ≈ 10⁻⁸ s/op
Iteration 5: ≈ 10⁻⁸ s/op
Result "com.et.jmh.StringConcatBenchmark.testStringConcat":
≈ 10⁻⁸ s/op
# Run complete. Total time: 00:01:45
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
StringConcatBenchmark.testStringConcat avgt 5 ≈ 10⁻⁸ s/op
以上只是一些关键代码,所有代码请参见下面代码仓库