一、简介
JMH(Java Microbenchmark Harness)是一个用于编写、构建和运行Java微基准测试的框架。它提供了丰富的注解和工具,用于精确控制测试的执行和结果测量,从而帮助我们深入了解代码的性能特性。
二、案例实战
- 在你的pom文件中导入如下依赖:
<dependency><groupId>org.openjdk.jmh</groupId><artifactId>jmh-core</artifactId><version>1.21</version></dependency><dependency><groupId>org.openjdk.jmh</groupId><artifactId>jmh-generator-annprocess</artifactId><version>1.21</version></dependency>
要使用JMH进行微基准测试,你需要在项目的构建系统(如Maven或Gradle)中引入JMH的依赖。
- 便携示例代码
下面是一个简单的JMH测试示例,用于比较直接访问数组元素和通过方法访问数组元素的性能差异。
package cn.pottercoding.jmh;import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;import java.util.concurrent.TimeUnit;/*** @author Mr.Sun* @since 2024年03月20日** 一个简单的JMH测试示例,用于比较直接访问数组元素和通过方法访问数组元素的性能差异。*/
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS)
@State(Scope.Thread)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class ArrayAccessBenchmark {// 数组大小private static final int ARRAY_SIZE = 10000;// 初始化数组private int[] array = new int[ARRAY_SIZE];// 初始化方法,用于填充数组@Setuppublic void setup() {for (int i = 0; i < ARRAY_SIZE; i++) {array[i] = i;}}// 直接访问数组元素@Benchmarkpublic int directAccess() {int sum = 0;for (int i = 0; i < ARRAY_SIZE; i++) {sum += array[i];}return sum;}// 通过方法访问数组元素@Benchmarkpublic int methodAccess() {int sum = 0;for (int i = 0; i < ARRAY_SIZE; i++) {sum += getElement(i);}return sum;}// 获取数组元素的方法private int getElement(int index) {return array[index];}// 主方法,用于运行基准测试public static void main(String[] args) throws RunnerException {Options opt = new OptionsBuilder().include(ArrayAccessBenchmark.class.getSimpleName()).build();new Runner(opt).run();}
}
接下来让我们一起看下上面示例代码中用到的每个JMH注解的含义:
-
@BenchmarkMode
这个注解用于指定基准测试的模式。Mode.AverageTime
表示测量的是每次操作的平均执行时间。JMH提供了多种模式,如Throughput
(吞吐量,即每秒完成的操作数)和SampleTime
(采样时间)等。 -
@Warmup
和@Measurement
@Warmup
:用于指定预热阶段的相关设置。预热阶段用于让JVM的JIT编译器有时间优化测试代码,并使得缓存、垃圾回收等达到稳定状态。iterations指定预热迭代的次数,time和timeUnit分别指定预热阶段的总时间和时间单位。
@Measurement
:用于指定实际测量阶段的相关设置。这些设置类似于预热阶段,但它们是用于收集基准测试结果的。 -
@State
这个注解用于定义测试状态。Scope.Thread
表示每个测试线程都有自己的测试状态实例。这样可以避免多线程之间的状态共享问题。其他可能的范围还包括Scope.Benchmark
(所有线程共享同一个状态实例)和Scope.Group
(每个测试组共享一个状态实例)。 -
@OutputTimeUnit
这个注解用于指定输出结果的时间单位。在这个例子中,TimeUnit.NANOSECONDS
表示输出的时间将以纳秒为单位。 -
@Setup
这个注解用于标记在每次基准测试方法运行之前应该执行的方法。它通常用于初始化测试所需的数据或状态。在这个例子中,setup()方法用于填充数组。 -
@Benchmark
这个注解用于标记一个基准测试方法。JMH会运行这个方法多次,并收集相关的性能数据。在这个例子中,directAccess()
和methodAccess()
都是基准测试方法,它们分别测试直接访问数组元素和通过方法访问数组元素的性能。 -
@Param
虽然这个注解在上面的示例代码中并没有使用,但它是一个常见的JMH注解,用于参数化基准测试。通过在测试类中的字段上使用@Param注解,并指定不同的值,你可以为同一个基准测试方法创建多个不同的测试场景。
执行结果如下:
代码执行结果分析:
-
directAccess()
方法
这个方法直接通过数组索引访问数组元素,并计算它们的和。由于它直接操作数组的内存位置,因此通常是最快的访问方式。在大多数情况下,directAccess()
方法应该会获得较低的平均执行时间。 -
methodAccess()
方法
这个方法通过调用getElement()
方法来访问数组元素。虽然getElement()
方法内部也是直接访问数组,但是方法调用的开销(如参数传递、栈帧创建等)通常会比直接访问要高一些。因此,methodAccess()
方法的平均执行时间很可能会比directAccess()
方法稍长。 -
结果比较
JMH会运行每个基准测试方法多次,并收集每次运行的执行时间。然后,它会计算这些时间的平均值、标准差等统计信息,并将它们输出到控制台或文件中。