一、简介
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];
// 初始化方法,用于填充数组
@Setup
public void setup() {
for (int i = 0; i < ARRAY_SIZE; i++) {
array[i] = i;
}
}
// 直接访问数组元素
@Benchmark
public int directAccess() {
int sum = 0;
for (int i = 0; i < ARRAY_SIZE; i++) {
sum += array[i];
}
return sum;
}
// 通过方法访问数组元素
@Benchmark
public 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会运行每个基准测试方法多次,并收集每次运行的执行时间。然后,它会计算这些时间的平均值、标准差等统计信息,并将它们输出到控制台或文件中。