Java Microbenchmark Harness (微基准测试框架)
在Java开发中,性能优化是一个持续的过程,它要求我们不仅理解代码的逻辑,还需要对代码的执行效率有深入的认识。为了有效地进行性能优化,我们需要一款能够准确度量代码执行时间的工具,而JMH正是这样一个强大的工具。
Java JMH (Java Microbenchmark Harness) 是一个用于编写、执行和分析 Java 微基准测试的工具。它是由 Java 开发者基于 JVM 的实际情况设计的,能够帮助开发者准确地测量和分析 Java 代码的性能。
为什么使用?
在 Java 中,进行性能测试(特别是微基准测试)是非常复杂的,因为 JVM 会执行一系列的优化操作(如即时编译、垃圾回收等),这些操作会影响测试结果的准确性。JMH 通过精心设计和一系列配置选项,能够屏蔽这些影响,提供更准确的基准测试结果。
JMH核心特性
注解驱动:JMH使用注解来标记基准测试方法和配置测试参数。这些注解提供了丰富的配置选项,如测试模式(吞吐量、平均时间等)、预热迭代次数、测量迭代次数等。这使得编写、配置和运行基准测试变得简单而直观。
隔离测试:为了确保测试结果的可重复性,JMH会在单独的JVM进程中运行每个基准测试。这样可以避免测试之间的干扰,并确保每个测试都在相同的初始条件下运行。此外,通过多次迭代测试,JMH可以计算统计上显著的结果,减少偶然误差。
广泛支持:JMH不仅支持Java语言的基准测试,还能对运行在JVM上的其他语言进行基准测试。这使得它成为一个非常强大的跨语言性能分析工具。
如何使用JMH?
如果使用 Maven,可以在 pom.xml 文件中添加以下依赖
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.36</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.36</version>
</dependency>
dependencies {
implementation 'org.openjdk.jmh:jmh-core:1.36'
annotationProcessor 'org.openjdk.jmh:jmh-generator-annprocess:1.36'
}
示例代码
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import java.util.concurrent.TimeUnit;
public class JMHExample {
// 基准测试方法,测量字符串拼接的性能
@Benchmark
@BenchmarkMode(Mode.AverageTime) // 设置测量模式为平均时间
@OutputTimeUnit(TimeUnit.MILLISECONDS) // 输出时间单位为毫秒
public String testStringConcat() {
String result = "";
for (int i = 0; i < 100; i++) {
result += "test";
}
return result;
}
// 基准测试方法,测量 StringBuilder 拼接的性能
@Benchmark
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public String testStringBuilderConcat() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100; i++) {
sb.append("test");
}
return sb.toString();
}
// 运行基准测试
public static void main(String[] args) throws Exception {
Options opt = new OptionsBuilder()
// 要导入的测试类
.include(JMHExample.class.getSimpleName())
.output("/Users/wushiwei/code/test/Benchmark.log")
.build();
new Runner(opt).run();
}
}
结果输出:
# Warmup: 5 iterations, 10 s each
# Measurement: 5 iterations, 10 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Average time, time/op
# Run progress: 0.00% complete, ETA 00:16:40
# Fork: 1 of 5
# Warmup Iteration 1: ≈ 10⁻⁴ ms/op
# Warmup Iteration 2: ≈ 10⁻⁴ ms/op
# Warmup Iteration 3: ≈ 10⁻⁴ ms/op
# Warmup Iteration 4: ≈ 10⁻⁴ ms/op
# Warmup Iteration 5: ≈ 10⁻⁴ ms/op
Iteration 1: ≈ 10⁻⁴ ms/op
Iteration 2: ≈ 10⁻⁴ ms/op
Iteration 3: ≈ 10⁻⁴ ms/op
Iteration 4: ≈ 10⁻⁴ ms/op
Iteration 5: ≈ 10⁻⁴ ms/op
// 1. Warmup和Iteration: 是JMH在正式测量之前的预热过程,目的是让JVM进入稳定状态。这有助于消除由于JIT编译等因素导致的初始不稳定性。
// 2. 单位 ms/op: 表示每个操作的平均执行时间,以毫秒为单位。这里显示的值是≈ 10⁻⁴ ms/op,即每次操作的平均时间非常短,大约在0.0001毫秒范围内。
// 3. Fork: JMH通过多次“fork”来运行测试,每次fork都会启动一个新的JVM实例,从而更好地消除JVM启动的影响。
注解说明
# @Benchmark
@Benchmark: 用于告诉JMH哪些方法需要进行测试,只能注解在方法上,JMH会针对注解了@Benchmark的方法生成Benchmark方法代码。通常情况下,每个Benchmark方法都运行在独立的进程中,互不干涉。
# @BenchmarkMode
@BenchmarkMode: 指定测试模式,@BenchmarkMode用于指定当前Benchmark方法使用哪种模式测试。JMH提供了4种不同的模式,用于输出不同的结果指标,如下:
• Throughput: 整体吞吐量(时间内程序的执行次数),ops/time。单位时间内执行操作的平均次数
• AverageTime: 平均时间,执行程序的平均耗时,time/op。执行每次操作所需的平均时间
• SampleTime: 执行时间随机取样,输出执行时间的结果分布,time/op,最后输出取样结果的分布。例如“99%的调用在xxx毫秒以内,99.99%的调用在xxx毫秒以内”
• SingleShotTime: 运行一次,测试冷启动时间,time/op。这种模式的结果存在较大随机性。
• All: 上边所有的都执行一遍。
# @OutputTimeUnit
@OutputTimeUnit: 输出的时间单位,为统计结果的时间单位,可用于类或者方法注解。
# @Iteration
@Iteration: JMH 进行测试的最小单位。在大部分模式下,一次 iteration 代表的是一秒,JMH 会在这一秒内不断调用需要 Benchmark 的方法,然后根据模式对其采样,计算吞吐量,计算平均执行时间等。
# @WarmUp
@WarmUp: 是指在实际进行 Benchmark 前先进行预热的行为。
JVM 的JIT机制的存在,如果某个函数被调用多次之后,JVM 会尝试将其编译成为机器码从而提高执行速度。为了让 Benchmark 的结果更加接近真实情况就需要进行预热。
# @Measurement
@Measurement: @Measurement 注解可作用于类或者方法上,用于指定测试的次数、时间和批处理数量,提供真正的测试阶段参数,指定迭代的次数,每次迭代的运行时间和每次迭代测试调用的数量。
• iterations:测量次数,默认是 5 次。
• time:单次测量持续时间,默认是 10。
• timeUnit:时间单位,指定 time 的单位,默认是秒。
• batchSize:每次操作的批处理次数,默认是 1,即调用一次测试方法算作一次操作。
@Warmup和@Measurement分别用于配置预热迭代和测试迭代。其中,iterations用于指定迭代次数,time和timeUnit用于每个迭代的时间,batchSize表示执行多少次Benchmark方法为一个invocation
# @State
@State: 该注解修饰类,JMH测试类必须使用@State注解,它定义了一个类实例的生命周期,可以类比 Spring Bean 的 Scope。由于 JMH 允许多线程同时执行测试,不同的选项含义如下:
• Scope.Thread:默认的 State,每个测试线程分配一个实例;
• Scope.Benchmark:所有测试线程共享一个实例,用于测试有状态实例在多线程共享下的性能;
• Scope.Group:每个线程组共享一个实例;
# @Setup
@Setup: 方法注解,会在执行 benchmark 之前被执行,正如其名,主要用于初始化。
# @TearDown
@TearDown: 方法注解,与@Setup 相对的,会在所有 benchmark 执行结束以后执行,主要用于资源的回收等。@Setup/@TearDown注解使用Level参数来指定何时调用fixture。
# @Fork
@Fork: 进行 fork 的次数。如果 fork 数是2的话,则 JMH 会 fork 出两个进程来进行测试。
# @Threads
@Threads: 每个进程中的测试线程,可用于类或者方法上
# @Param
@Param: 成员注解,可以用来指定某项参数的多种情况。特别适合用来测试一个函数在不同的参数输入的情况下的性能。@Param 注解接收一个String数组,在 @Setup 方法执行前转化为为对应的数据类型。多个 @Param 注解的成员之间是乘积关系,譬如有两个用 @Param 注解的字段,第一个有5个值,第二个字段有2个值,那么每个测试方法会跑5* 2=10次。
Blackhole
org.openjdk.jmh.infra.Blackhole 是 Java Microbenchmark Harness (JMH) 提供的一个辅助类,专门用于消除微基准测试(microbenchmark)中的“死代码消除”问题。
在微基准测试中,JVM 的优化机制(例如死代码消除)可能会干扰基准测试的准确性。具体来说,如果 JVM 检测到某些代码的结果没有被使用,它可能会在优化时将这些代码完全消除,从而导致基准测试的时间测量不准确。Blackhole 类的作用就是防止 JVM 在运行基准测试时进行过度优化,尤其是消除不必要的代码。通过将代码的输出“喂入” Blackhole,可以确保 JVM 不会认为这些代码无用,从而强制执行所有的计算。
使用示例
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.infra.Blackhole;
public class MyBenchmark {
@Benchmark
public void testMethod(Blackhole blackhole) {
int result = computeSomething();
blackhole.consume(result); // 将结果“喂入” Blackhole,防止 JVM 优化掉 computeSomething 的调用
}
private int computeSomething() {
return 42;
}
}
// 在这个例子中:
//• computeSomething() 方法返回一个结果。如果这个结果没有被使用,JVM 可能会将该方法的调用优化掉。
//• 通过 blackhole.consume(result);,JMH 确保 JVM 不会优化掉 computeSomething() 的调用,因为 Blackhole 使得结果似乎被使用了。
Java Microbenchmark Harness (JMH) 是一个专门设计用于对 Java 代码性能进行微基准测试 的框架。其主要作用是帮助开发者评估 Java 程序中 小型代码片段(如算法、方法、或类)的性能表现,并生成高精度的测试结果。JMH 由 Oracle 的 Java 团队 开发,并且是 JDK 官方工具的一部分。
JMH 的作用:
- 精确测量性能:通过 JMH,可以避免由于 JVM JIT 编译器的优化导致的测试数据不准确,保证测试结果的精度。
- 自动管理 JVM 特性:如垃圾回收、JIT 编译等,JMH 能够在测试过程中自动处理这些特性,确保测试结果的可复现性和稳定性。
- 支持并发测试:可以在多线程环境下进行性能基准测试,测试代码在并发执行时的表现。
- 多种测试模式:支持吞吐量、延迟、单次调用时间等多种性能指标的测试。
- 支持多平台和多语言:虽然 JMH 主要用于 Java,也支持其他 JVM 语言如 Scala、Kotlin 的性能基准测试。
应用场景:
-
性能优化:开发者在进行代码优化时,通过 JMH 测试不同实现的性能差异。
-
验证算法性能:测试算法在不同输入数据和环境下的执行效率。
-
测量 JVM 特性影响:测试代码在不同 JVM 设置(如垃圾回收策略、内存分配等)下的性能表现。
-
JMH 是 Java 开发中评估代码性能的专业工具,适用于需要 高精度性能测量和优化的场景。
致谢
更多内容欢迎关注 [ 小巫编程室 ] 公众号,喜欢文章的话,也希望能给小编点个赞或者转发,你们的喜欢与支持是小编最大的鼓励,小巫编程室感谢您的关注与支持。好好学习,天天向上(good good study day day up)。
参考资料
源码地址: github.com/openjdk/jmh