在软件开发中,性能优化是一个永恒的话题。为了确保代码在生产环境中运行得尽可能快,开发者需要一种准确的方法来度量和比较不同代码片段的性能。Java Microbenchmark Harness(JMH)是一个专门为Java和其他基于JVM的语言设计的工具,它允许开发者以高精度执行微基准测试。
1.JMH简介
JMH是一个用于编写可靠Java微基准测试的工具。它可以帮助开发者量化代码片段的执行时间,这对于理解代码性能至关重要。通过JMH,开发者可以比较不同算法或代码实现的性能,从而做出基于数据的优化决策。
JMH的设计考虑了基准测试中的各种陷阱,如JVM的热点优化、死码消除和垃圾收集暂停。它提供了一组注解和工具类,使得编写、配置和运行基准测试变得简单而直观。
2.JMH核心特性
- 注解驱动:JMH使用注解来标记基准测试方法和配置测试参数。这些注解提供了丰富的配置选项,如测试模式(吞吐量、平均时间等)、预热迭代次数、测量迭代次数等。
- 隔离测试:为了确保测试结果的可重复性,JMH会在单独的JVM进程中运行每个基准测试。这样可以避免测试之间的干扰,并确保每个测试都在相同的初始条件下运行。
- 预热和迭代:JMH允许开发者指定预热迭代次数,以使得JVM的热点优化在测量阶段之前生效。此外,通过多次迭代测试,JMH可以计算统计上显著的结果,减少偶然误差。
- 结果统计:JMH会自动收集和分析测试结果,提供有关吞吐量、平均执行时间等的详细信息。这些信息对于理解代码性能瓶颈和优化方向非常有价值。
三、使用JMH进行基准测试
使用JMH进行基准测试涉及几个步骤:添加依赖、编写基准测试类、配置测试选项和运行测试。
- 添加JMH依赖
<dependencies> <dependency> <groupId>org.openjdk.jmh</groupId> <artifactId>jmh-core</artifactId> <version>1.33</version> </dependency> <dependency> <groupId>org.openjdk.jmh</groupId> <artifactId>jmh-generator-annprocess</artifactId> <version>1.33</version> <scope>provided</scope> </dependency>
</dependencies>
- 编写基准测试类
创建一个Java类,并使用JMH提供的注解来标记基准测试方法。例如,使用@Benchmark
注解来标记要进行性能测量的方法,使用@BenchmarkMode
来指定测试模式(如Throughput
表示吞吐量,AverageTime
表示平均时间),以及使用@OutputTimeUnit
来指定输出结果的时间单位。
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; @BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class MyBenchmark { @Benchmark public void measure() { // 这里放置你想要基准测试的代码 }
}
- 运行基准测试
<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>2.22.2</version> <configuration> <skipTests>true</skipTests> <!-- 禁用常规的Maven测试 --> </configuration> </plugin> <plugin> <groupId>org.openjdk.jmh</groupId> <artifactId>jmh-maven-plugin</artifactId> <version>1.33</version> <!-- 使用你需要的版本 --> <executions> <execution> <id>run-benchmarks</id> <phase>integrate-test</phase> <goals> <goal>run</goal> </goals> </execution> </executions> </plugin> </plugins>
</build>
然后,你可以通过Maven命令来运行基准测试:
mvn clean integrate-test
通过JMH命令行工具运行:
mvn clean package
java -jar target/benchmarks.jar
3.JMH注解
- Benchmark:
- 这是一个方法注解,用于声明该方法是一个基准测试方法。
- 被此注解标记的方法将被JMH用于重复执行,以便进行性能测量。
- State:
- 这是一个类注解,用于声明该类是一个“状态”类。
- 状态类定义了基准测试的状态,可以包含测试所需的实例变量。
- 它有一个
Scope
参数,用于指定状态实例的生命周期和共享范围。
- Scope枚举值:
Scope.Thread
:每个测试线程分配一个状态实例。Scope.Benchmark
:所有测试线程共享一个状态实例。Scope.Group
:每个线程组共享一个状态实例。
- Setup :
- 这是一个方法注解,用于指定在基准测试方法执行之前运行的初始化方法。
- 通常用于准备测试数据或初始化状态。
- TearDown :
- 这是一个方法注解,用于指定在基准测试方法执行之后运行的清理方法。
- 通常用于释放资源或进行后处理。
- Param :
- 这是一个字段注解,用于指定基准测试的参数。
- 可以为基准测试方法提供不同的输入值,以便测试在不同条件下的性能。
- OutputTimeUnit :
- 这是一个类或方法注解,用于指定基准测试结果的时间单位。
- 它使用
java.util.concurrent.TimeUnit
中的标准时间单位。
- BenchmarkMode :
- 这是一个类或方法注解,用于指定基准测试的模式。
Mode
枚举值包括:Throughput
(吞吐量),AverageTime
(平均时间),SampleTime
(随机采样时间),SingleShotTime
(单次执行时间),All
(所有模式)。
- Warmup :
- 这是一个类或方法注解,用于配置预热迭代的次数。
- 预热迭代用于使JVM的热点代码优化达到稳定状态,以获得更准确的基准测试结果。
- Measurement :
- 这是一个类或方法注解,用于配置实际测量迭代的次数。
- 这些迭代将用于收集性能数据。
- Fork :
- 这是一个类注解,用于指定基准测试的进程分叉次数。
- 每个分叉将在单独的进程中运行基准测试,以减少噪声和干扰。
- Threads :
- 这是一个方法注解,用于指定执行基准测试的线程数。
- 它允许模拟多线程环境下的性能。
- Group :
- 这是一个类和方法注解,用于将多个基准测试方法组合成一个测试组。
- 测试组内的方法将按照指定的顺序执行,并且共享相同的状态实例(当使用
Scope.Group
时)。
4.简单的基准测试
比较两种字符串拼接方法的性能:使用+操作符和使用StringBuilder。
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 org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder; import java.util.concurrent.TimeUnit; @State(Scope.Thread)
public class StringConcatBenchmark { private static final String A = "Hello, "; private static final String B = "World!"; @Benchmark @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) public String stringConcatPlus() { return A + B; } @Benchmark @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) public String stringConcatStringBuilder() { StringBuilder sb = new StringBuilder(); sb.append(A); sb.append(B); return sb.toString(); } public static void main(String[] args) throws Exception { Options opt = new OptionsBuilder() .include(StringConcatBenchmark.class.getSimpleName()) .warmupIterations(5) .measurementIterations(10) .forks(1) .build(); new Runner(opt).run(); }
}
这个例子中,我们定义了一个StringConcatBenchmark
类,其中包含两个基准测试方法:stringConcatPlus
和stringConcatStringBuilder
。我们使用@State(Scope.Thread)
注解来指定每个测试线程有其独立的状态实例。
@BenchmarkMode(Mode.AverageTime)
和@OutputTimeUnit(TimeUnit.NANOSECONDS)
注解分别指定我们想要测量的是平均时间,并且输出结果的时间单位为纳秒。main
方法中,我们配置了基准测试的运行选项,并通过Runner
类来执行基准测试。
5.参数化基准测试
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 org.openjdk.jmh.annotations.Param;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder; import java.util.Arrays;
import java.util.Random;
import java.util.concurrent.TimeUnit; @State(Scope.Thread)
public class ArraySortBenchmark { @Param({"100", "1000", "10000"}) private int arraySize; private Integer[] array; @Setup public void setup() { array = new Integer[arraySize]; Random rand = new Random(); for (int i = 0; i < arraySize; i++) { array[i] = rand.nextInt(); } } @Benchmark @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.MICROSECONDS) public void sortArrayTimSort() { Arrays.sort(array); } @Benchmark @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.MICROSECONDS) public void sortArrayJava8ParallelSort() { Arrays.parallelSort(array); } public static void main(String[] args) throws Exception { Options opt = new OptionsBuilder() .include(ArraySortBenchmark.class.getSimpleName()) .warmupIterations(5) .measurementIterations(5) .forks(1) .build(); new Runner(opt).run(); }
}
在这个例子中,我们使用@Param
注解来定义了一个参数arraySize
,它将在基准测试中取不同的值(100、1000、10000)。@Setup
注解用于在执行基准测试之前进行一些初始化工作,在本例中是生成一个随机数组。
我们定义了两个基准测试方法:sortArrayTimSort
使用Arrays.sort
进行排序,而sortArrayJava8ParallelSort
使用Arrays.parallelSort
进行排序。我们将测量这两种方法对不同大小数组的平均排序时间。
main
方法中,我们配置了基准测试的运行选项,并通过Runner
类来执行基准测试。执行结果将包括每个数组大小和每种排序方法的平均执行时间。
6.结论
JMH是一个强大而灵活的工具,用于在Java和其他基于JVM的语言中进行微基准测试。通过掌握JMH的核心特性和最佳实践,开发者可以准确地度量和比较代码的性能,从而做出明智的优化决策。在性能关键的场景中,使用JMH进行基准测试是确保代码高效运行的关键步骤。