概述
JMH 的全名是 Java Microbenchmark Harness,它是由 Java 虚拟机团队开发的一款用于 Java 微基准测试工具。
依赖库
https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-core
<!-- https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-core -->
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.35</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-generator-annprocess -->
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.35</version>
<scope>provided</scope>
</dependency>
出现Exception in thread “main” java.lang.RuntimeException: ERROR: Unable to find the resource: /META-INF/BenchmarkList问题,
问题就在于:jmh-generator-annprocess依赖的scope配置为test,此时表示该依赖仅仅在测试阶段会参与进来,包括测试代码的编译,执行。
由于这个依赖并不用于生产,我们可以将scope设置为provided
JMH注解
@BenchmarkMode
JMH使用@BenchmarkMode这个注解来声明运行模式,BenchmarkMode既可以在class上进行注解设置,也可以在基准方法上进行注解设置,方法中设置的模式将会覆盖class注解上的设置,同样,在Options中也可以进行设置,它将会覆盖所有基准方法上的设置。JMH为我们提供了四种运行模式:
AverageTime(平均响应时间) | 要用于输出基准测试方法每调用一次所耗费的时间,也就是elapsed time/operation |
Throughput(方法吞吐量) | 则刚好与AverageTime相反,它的输出信息表明了在单位时间内可以对该方法调用多少次 |
SampleTime(时间采样) | 采用一种抽样的方式来统计基准测试方法的性能结果,与我们常见的直方图几乎是一样的,它会收集所有的性能数据,并且将其分布在不同的区间中。 |
SingleShotTime | 主要可用来进行冷测试,不论是Warmup还是Measurement,在每一个批次中基准测试方法只会被执行一次,一般情况下,我们会将Warmup的批次设置为0 |
@Benchmark
JMH对基准测试的方法需要使用@Benchmark注解进行标记,否则方法将被视为普通方法,并且不会对其执行基准测试
@Warmup
Warmup所做的就是在基准测试代码正式度量之前,先对其进行预热,使得代码的执行是经历过了类的早期优化、JVM运行期编译、JIT优化之后的最终状态,从而能够获得代码真实的性能数据。
参数说明
- timeUnit:时间的单位,默认的单位是秒;
- iterations:预热阶段的迭代数;
- time:每次预热的时间;
- batchSize:批处理大小,指定了每次操作调用几次方法。 ```java @Warmup(
iterations = 5,
time = 1,
timeUnit = TimeUnit.SECONDS)
代码预热总计 5 秒(迭代 5 次,每次一秒)。预热过程的测试数据,是不记录测量结果的。
<a name="sUeKq"></a>
### @Measurement
Measurement则是真正的度量操作,在每一轮的度量中,所有的度量数据会被纳入统计之中(预热数据不会纳入统计之中)
```java
@Measurement(
iterations = 5,
time = 1,
timeUnit = TimeUnit.SECONDS)
fork
fork 的值一般设置成 1,表示只使用一个进程进行测试;如果这个数字大于 1,表示会启用新的进程进行测试;但如果设置成 0,程序依然会运行,此时在用户的 JVM 进程上运行
Threads
fork 是面向进程的,而 Threads 是面向线程的。指定了这个注解以后,将会开启并行测试。如果配置了 Threads.MAX,则使用和处理机器核数相同的线程数。
@Setup
@Setup会在每一个基准测试方法执行前被调用,通常用于资源的初始化
@TearDown
@TearDown则会在基准测试方法被执行之后被调用,通常可用于资源的回收清理工作
@BenchmarkMode(Mode.AverageTime)
@State(Scope.Thread)
@Fork(1)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Warmup(iterations = 3)
@Measurement(iterations = 5)
public class StringBuildTest {
String string = "";
StringBuilder stringBuilder = new StringBuilder();
@Benchmark
public String stringAdd() {
for (int i = 0; i < 1000; i++) {
string = string + i;
}
return string;
}
@Benchmark
public String stringBuilderAppend() {
for (int i = 0; i < 1000; i++) {
stringBuilder.append(i);
}
return stringBuilder.toString();
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(StringBuildTest.class.getSimpleName())
.build();
new Runner(opt).run();
}
}
连接池测试
使用连接池和不使用连接池,它们之间的性能差距到底有多大呢?下面是一个简单的 JMH 测试例子(见仓库),进行一个简单的 set 操作,为 redis 的 key 设置一个随机值。
@Fork(2)
@State(Scope.Benchmark)
@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 5, time = 1)
@BenchmarkMode(Mode.Throughput)
public class JedisPoolVSJedisBenchmark {
JedisPool pool = new JedisPool("localhost", 6379);
@Benchmark
public void testPool() {
Jedis jedis = pool.getResource();
jedis.set("a", UUID.randomUUID().toString());
jedis.close();
}
@Benchmark
public void testJedis() {
Jedis jedis = new Jedis("localhost", 6379);
jedis.set("a", UUID.randomUUID().toString());
jedis.close();
}
...
图形化
Options opt = new OptionsBuilder()
.resultFormat(ResultFormatType.JSON)
.build();
JMH 支持 5 种格式结果
- TEXT 导出文本文件。
- CSV 导出 csv 格式文件。
- SCSV 导出 scsv 等格式的文件。
- JSON 导出成 json 文件。
- LATEX 导出到 latex,一种基于 ΤΕΧ 的排版系统。
图形工具
JMH Visualizer
https://jmh.morethan.io/
通过导出 json 文件,上传至 JMH Visualizer
JMH Visual Chart
http://deepoove.com/jmh-visual-chart/
meta-chart
https://www.meta-chart.com/