1、为什么需要JMH
- 时间精度问题,本身获取到的时间戳就是存在误差的,它和操作系统有关。
- JVM 在运行时会进行代码预热,说白了就是越跑越快。因为类需要装载、需要准备操作。
- JVM 会在各个阶段都有可能对你的代码进行优化处理。
- 资源回收的不确定性,可能运行很快,回收很慢。
2、JMH是什么
JMH 的全名是Java Microbenchmark Harness
,它是由 Java 虚拟机团队开发的一款用于 Java 微基准测试工具。
使用 JMH 可以让你方便快速的进行一次严格的代码基准测试,并且有多种测试模式,多种测试维度可供选择;而且使用简单、增加注解便可启动测试。
3、JMH 使用
3.1 引入依赖
<!--jmh 基准测试 -->
<properties>
<jmh.version>1.0</jmh.version>
</properties>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>${jmh.version}</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>${jmh.version}</version>
<scope>provided</scope>
</dependency>
3.2 测试案例
3.2.1 字符串拼接
@BenchmarkMode(Mode.AverageTime)
@State(Scope.Thread)
@Fork(1)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Warmup(iterations = 3)
@Measurement(iterations = 5)
public class MyBenchmark{
@Benchmark
public String measureStringApend() {
String targetString = "";
for (int i = 0; i < 10000; i++) {
targetString += "hello";
}
return targetString;
}
@Benchmark
public String measureStringBufferApend() {
StringBuffer buffer = new StringBuffer();
for (int i = 0; i < 10000; i++) {
buffer.append("hello");
}
return buffer.toString();
}
@Benchmark
public String measureStringBuilderApend() {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < 10000; i++) {
builder.append("hello");
}
return builder.toString();
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(MyBenchmark.class.getSimpleName())
.build();
// 在参数方式配置测试
/* Options opt = new OptionsBuilder()
.include(MyBenchmark.class.getSimpleName())
.forks(1)
.warmupIterations(3)
.measurementIterations(5)
.build();*/
new Runner(opt).run();
}
}
- 测试结果如下:
# Warmup: 3 iterations, 1 s each // 预热运行三次
# Measurement: 5 iterations, 1 s each // 性能测试5次
# Threads: 1 thread, will synchronize iterations // 线程数量为1
# Benchmark mode: Average time, time/op // 统计方法调用一次的平均时间
# Benchmark: com.example.demo1.quality.MyBenchmark.measureStringApend // 本次执行的方法
// 1、measureStringApend
# Run progress: 0.00% complete, ETA 00:00:24
# Fork: 1 of 1
# Warmup Iteration 1: 504372.433 us/op // 第一次预热,耗时95us
# Warmup Iteration 2: 233214.700 us/op
# Warmup Iteration 3: 140691.413 us/op
Iteration 1: 75023.720 us/op
Iteration 2: 64375.283 us/op
Iteration 3: 67601.253 us/op
Iteration 4: 72401.631 us/op
Iteration 5: 63729.739 us/op
Result: 68626.325 ±(99.9%) 19086.757 us/op [Average]
// 执行的最小、平均、最大、误差值
Statistics: (min, avg, max) = (63729.739, 68626.325, 75023.720), stdev = 4956.770
Confidence interval (99.9%): [49539.568, 87713.082]
// 2、measureStringBufferApend
# Run progress: 33.33% complete, ETA 00:00:22
# Fork: 1 of 1
# Warmup Iteration 1: 145.465 us/op
# Warmup Iteration 2: 121.062 us/op
# Warmup Iteration 3: 106.924 us/op
Iteration 1: 99.278 us/op
Iteration 2: 101.873 us/op
Iteration 3: 98.889 us/op
Iteration 4: 100.907 us/op
Iteration 5: 108.453 us/op
Result: 101.880 ±(99.9%) 14.897 us/op [Average]
Statistics: (min, avg, max) = (98.889, 101.880, 108.453), stdev = 3.869
Confidence interval (99.9%): [86.983, 116.777]
// 3、measureStringBuilderApend
# Run progress: 66.67% complete, ETA 00:00:10
# Fork: 1 of 1
# Warmup Iteration 1: 150.403 us/op
# Warmup Iteration 2: 138.444 us/op
# Warmup Iteration 3: 106.494 us/op
Iteration 1: 111.841 us/op
Iteration 2: 104.562 us/op
Iteration 3: 126.979 us/op
Iteration 4: 124.022 us/op
Iteration 5: 109.198 us/op
Result: 115.320 ±(99.9%) 37.382 us/op [Average]
Statistics: (min, avg, max) = (104.562, 115.320, 126.979), stdev = 9.708
Confidence interval (99.9%): [77.938, 152.702]
# Run complete. Total time: 00:00:32
Benchmark Mode Samples Score Score error Units
c.e.d.q.MyBenchmark.measureStringApend avgt 5 68626.325 19086.757 us/op
c.e.d.q.MyBenchmark.measureStringBufferApend avgt 5 101.880 14.897 us/op
c.e.d.q.MyBenchmark.measureStringBuilderApend avgt 5 115.320 37.382 us/op
3.2.2 HashKey冲突
@Benchmark
public void measureHashMap() throws IOException {
Map<HashedKey, String> map = new HashMap<>();
for (int i = 0; i < 10000; i++) {
map.put(new HashedKey(i), "value");
}
}
private static class HashedKey {
final int key;
HashedKey(int key) {
this.key = key;
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (obj instanceof HashedKey) {
return key == ((HashedKey)obj).key;
}
return false;
}
@Override
public int hashCode() {
return key;
}
}
@Benchmark
public void measureCollidedHashMap() throws IOException {
Map<CollidedKey, String> map = new HashMap<>();
for (int i = 0; i < 10000; i++) {
map.put(new CollidedKey(i), "value");
}
}
private static class CollidedKey {
final int key;
CollidedKey(int key) {
this.key = key;
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (obj instanceof CollidedKey) {
return key == ((CollidedKey)obj).key;
}
return false;
}
@Override
public int hashCode() {
return key % 10;
}
}
3.3 注解说明
@BenchmarkMode(Mode)
表示 JMH 进行 Benchmark 时所使用的模式。通常是测量的维度不同,或是测量的方式不同。目前JMH 共有四种模式:
- Throughput: 整体吞吐量,例如“1秒内可以执行多少次调用”。
- AverageTime: 调用的平均时间,例如“每次调用平均耗时xxx毫秒”。
- SampleTime: 随机取样,最后输出取样结果的分布,例如“99%的调用在xxx毫秒以内,99.99%的调用在xxx毫秒以内”
- SingleShotTime: 以上模式都是默认一次 iteration 是 1s,唯有 SingleShotTime 是只运行一次。往往同时把 warmup 次数设为0,用于测试冷启动时的性能。
@State(Scope.Thread)
State 用于声明某个类是一个“状态”,然后接受一个 Scope 参数用来表示该状态的共享范围。因为很多 benchmark 会需要一些表示状态的类,JMH 允许你把这些类以依赖注入的方式注入到 benchmark 函数里。Scope 主要分为三种。
- Thread: 该状态为每个线程独享。
- Benchmark: 该状态在所有线程间共享。
- Group:线程组共享一个示例,在测试方法上使用 @Group 设置线程组。
@fork
进行 fork 的次数。如果 fork 数是2的话,则 JMH 会 fork 出两个线程来进行测试。
@Warmup:
Warmup 是指在实际进行 benchmark 前先进行预热的行为。为什么需要预热?因为 JVM 的 JIT 机制的存在,如果某个函数被调用多次之后,JVM 会尝试将其编译成为机器码从而提高执行速度。所以为了让 benchmark 的结果更加接近真实情况就需要进行预热。
@Measurement
进行 5 次微基准测试,也可用在测试方法上
@Benchmark
表示该方法是需要进行 benchmark 的对象,用法和 JUnit 的 @Test 类似。
@OutputTimeUnit
benchmark 结果所使用的时间单位。
@Param
@Param 可以用来指定某项参数的多种情况。特别适合用来测试一个函数在不同的参数输入的情况下的性能。
@Setup
@Setup 会在执行 benchmark 之前被执行,正如其名,主要用于初始化。
@TearDown
@TearDown 和 @Setup 相对的,会在所有 benchmark 执行结束以后执行,主要用于资源的回收等。
Iteration:
Iteration 是 JMH 进行测试的最小单位。在大部分模式下,一次 iteration 代表的是一秒,JMH 会在这一秒内不断调用需要 benchmark 的方法,然后根据模式对其采样,计算吞吐量,计算平均执行时间等。
include
benchmark 所在的类的名字,注意这里是使用正则表达式对所有类进行匹配的。