Java微基准测试框架入门

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 文件中添加以下依赖

 

javascript

代码解读

复制代码

<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' }

示例代码

 

java

代码解读

复制代码

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();     } }

结果输出:

 

bash

代码解读

复制代码

# 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启动的影响。

注解说明

 

ruby

代码解读

复制代码

# @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 不会认为这些代码无用,从而强制执行所有的计算。

使用示例

 

java

代码解读

复制代码

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)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值