简介
JMH(Java Microbenchmark Harness)是一个帮助开发者更好的实现微基准测试(microbenchmark)的工具,属于OpenJDK项目的一部分。虽然名字叫微基准测试工具,但JMH也可以适用于其他类型的基准测试,对较大型项目的基准测试同样也有帮助。
为什么需要用JMH来做基准测试
由于JVM会对代码做各种优化,而自己写的基准测试很难考虑到各方面的优化,这样会导致测试的结果可能跟线上的相差较大。JMH本身虽然不能完全消除这些优化的影响,但是它能帮助减轻这方面的影响。
本文使用环境:Java 8
使用Maven构建运行JMH
使用Maven构建运行JMH是被OpenJDK团队推荐的使用方式,因为该方式能产生更加可靠的结果。
使用Maven创建新项目
使用该命令可以创建一个基于Maven的JMH模板项目:
mvn archetype:generate
-DinteractiveMode=false
-DarchetypeGroupId=org.openjdk.jmh
-DarchetypeArtifactId=jmh-java-benchmark-archetype
-DarchetypeVersion=1.25.2
-DgroupId=com.nereusyi.demos
-DartifactId=jmh-template
-Dversion=1.0
官方示例的命令行参数中,没有-DarchetypeVersion=1.25.2这行,那样默认会生成较老版本的模板项目,需要较多改动才能适配新版本的Java,所以最好加上该参数。也可以在maven仓库上查看JMH的最新版本并替换
部分输出如下:
[INFO] ----------------------------------------------------------------------------
[INFO] Using following parameters for creating project from Archetype: jmh-java-benchmark-archetype:1.25.2
[INFO] ----------------------------------------------------------------------------
[INFO] Parameter: groupId, Value: com.nereusyi.demos
[INFO] Parameter: artifactId, Value: jmh-template
[INFO] Parameter: version, Value: 1.0
[INFO] Parameter: package, Value: com.nereusyi.demos
[INFO] Parameter: packageInPathFormat, Value: com/nereusyi/demos
[INFO] Parameter: package, Value: com.nereusyi.demos
[INFO] Parameter: groupId, Value: com.nereusyi.demos
[INFO] Parameter: artifactId, Value: jmh-template
[INFO] Parameter: version, Value: 1.0
[INFO] Project created from Archetype in dir: G:\jmh-template
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
此时可以在执行命令的目录下看到文件夹名字为jmh-template
的Maven项目。
编写测试方法
在刚生成的jmh-template
里,可以看到一个生成的MyBenchmark.java
类:
package com.nereusyi.demos;
import org.openjdk.jmh.annotations.Benchmark;
public class MyBenchmark {
@Benchmark
public void testMethod() {
// This is a demo/sample template for building your JMH benchmarks. Edit as needed.
// Put your benchmark code here.
}
}
可以把需要测试的方法放到testMethod
中:
@Benchmark
public int[] testMethod() {
// This is a demo/sample template for building your JMH benchmarks. Edit as needed.
// Put your benchmark code here.
int[] input = IntStream.range(1, 10000).toArray();
int inputLength = input.length;
int temp;
boolean is_sorted;
for (int i = 0; i < inputLength; i++) {
is_sorted = true;
for (int j = 1; j < (inputLength - i); j++) {
if (input[j - 1] > input[j]) {
temp = input[j - 1];
input[j - 1] = input[j];
input[j] = temp;
is_sorted = false;
}
}
if (is_sorted) break;
}
return input;
}
这里把原来的
void
返回值修改了,是为了避免JVM的Dead Code
优化。下面会介绍更多相关技巧。
构建JMH项目
现在可以在命令行进入jmh-template
项目(cd jmh-template
)构建:
mvn clean package
构建之后,会生成target/benchmarks.jar
文件。
benchmarks.jar
文件包含运行基准测试所需要的所有依赖,所以可以把该文件复制到其他机器上运行测试。
运行基准测试
用java -jar
运行就可以:
java -jar tartget/benchmarks.jar
运行基准测试需要一些时间,因为JMH会做预热等操作。同时也需要在运行基准测试时,关闭其他应用程序,以便生成更可靠的基准测试结果。
测试结果输出:
Benchmark Mode Cnt Score Error Units
MyBenchmark.testMethod thrpt 25 142673.012 ± 369.963 ops/s
在应用代码中运行JMH
也可以通过编程方式运行JMH基准测试。首先需要在pom.xml
中引入JMH依赖:
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.25.2</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.25.2</version>
<scope>provided</scope>
</dependency>
测试代码如下:
public class JmhDemo {
@Benchmark
public int[] testMethod(){
int[] input = IntStream.range(1, 10000).toArray();
int inputLength = input.length;
int temp;
boolean is_sorted;
for (int i = 0; i < inputLength; i++) {
is_sorted = true;
for (int j = 1; j < (inputLength - i); j++) {
if (input[j - 1] < input[j]) {
temp = input[j - 1];
input[j - 1] = input[j];
input[j] = temp;
is_sorted = false;
}
}
if (is_sorted) break;
}
return input;
}
@Benchmark
public int[] testMethod2() {
int[] input = IntStream.range(1, 10000).toArray();
for (int i = 1; i < input.length; i++) {
int key = input[i];
int j = i - 1;
while (j >= 0 && input[j] < key) {
input[j + 1] = input[j];
j = j - 1;
}
input[j + 1] = key;
}
return input;
}
public static void main(String[] args) throws IOException {
org.openjdk.jmh.Main.main(args);
}
}
直接运行main
方法就可以运行JMH测试。
还有一种可以传入各种构建参数的API:
Options options = new OptionsBuilder()
.jvmArgs("-Xmx64m")
.shouldFailOnError(true)
.build();
new Runner(options).run();
配置JMH
JMH还有很多配置,可以根据需要进行配置。
BenchmarkMode
JMH可以用不同的模式运行,不同模式测试不同的指标,具体模式如下:
- Throughput :测试方法的吞吐量
- AverageTime:测试方法的平均执行时间
- SampleTime:测试方法的最长时间、最短时间等
- SingleShotTime:测试方法单次执行的时间(没有预热的情况下的表现,通常用于测试方法冷启动的性能)
- All : 测试以上所有情况
默认模式是Throughput。使用示例片段如下:
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Mode;
...
@Benchmark
@BenchmarkMode(Mode.Throughput)
public int[] testMethod() {
int[] input = IntStream.range(1, 10000).toArray();
int inputLength = input.length;
int temp;
boolean is_sorted;
for (int i = 0; i < inputLength; i++) {
is_sorted = true;
for (int j = 1; j < (inputLength - i); j++) {
if (input[j - 1] < input[j]) {
temp = input[j - 1];
input[j - 1] = input[j];
input[j] = temp;
is_sorted = false;
}
}
if (is_sorted) break;
}
return input;
}
State
State用于初始化一些参数。如果生成数据的代码不需要加入基准测试的执行时间,可以用State。State需要用Class定义,使用的时候作为方法参数传入测试的方法。示例如下:
public class JmhDemo {
@State(Scope.Benchmark)
public static class InputState {
int[] input = IntStream.range(1, 10000).toArray();
}
@Benchmark
@BenchmarkMode(Mode.Throughput)
public int[] testMethod(InputState inputState) {
int[] input = inputState.input;
int inputLength = input.length;
int temp;
boolean is_sorted;
for (int i = 0; i < inputLength; i++) {
is_sorted = true;
for (int j = 1; j < (inputLength - i); j++) {
if (input[j - 1] < input[j]) {
temp = input[j - 1];
input[j - 1] = input[j];
input[j] = temp;
is_sorted = false;
}
}
if (is_sorted) break;
}
return input;
}
@Benchmark
@BenchmarkMode(Mode.Throughput)
public int[] testMethod2(InputState inputState) {
int[] input = inputState.input;
for (int i = 1; i < input.length; i++) {
int key = input[i];
int j = i - 1;
while (j >= 0 && input[j] < key) {
input[j + 1] = input[j];
j = j - 1;
}
input[j + 1] = key;
}
return input;
}
public static void main(String[] args) throws IOException {
org.openjdk.jmh.Main.main(args);
}
}
在这个示例中,类InputState
作为参数传入Benchmark方法。
State Scope
@State注解接收一个Scope参数。State对象可以被多次初始化,Scope定义了具体的初始化行为,具体值如下:
- Thread:每个线程创建自己的State对象
- Group:一组线程创建一个State对象
- Benchmark:所有在线程在一个Benchmark中,共享同一个State对象
State类
被@State注解的类需要遵守以下几条规则:
- 类必须被
public
修饰 - 如果是嵌套类,必须被
static
修饰 - 类必须要有无参构造器
如果没有遵守以上规则,编译代码时会报错。
State类的@Setup和@TearDown
@Setup表示在传入Benchmark方法前,调用被注解的方法。@TearDown表示在执行完Benchmark方法后,调用被注解的方法。被@Setup和@TearDown注解的方法,其执行时间不会被算在Benchmark方法中。使用示例如下:
@State(Scope.Benchmark)
public static class InputState {
@Setup(Level.Trial)
public void setup(){
System.out.println("setup");
}
int[] input = IntStream.range(1, 10000).toArray();
@TearDown(Level.Trial)
public void tearDown(){
System.out.println("tearDown");
}
}
这两个注解可以传入一个Level的参数,这个参数有3个值:
- Level.Trial:在预热和循环执行Benchmark方法时调用一次
- Level.Iteration:每次开始循环执行Benchmark方法时调用一次
- Level.Invocation:每次调用Benchmark方法时调用一次
Benchmark TimeUnit
在JMH中,使用@OutputTimeUnit注解可以指定输出结果时间显示的单位,它接收java.util.concurrent.TimeUnit
作为参数。可以注解在类或具体方法上,示例如下:
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public class JmhDemo {
...
}
JMH基准测试代码注意事项
虽然JMH会对基准测试的代码做大部分工作,但想要写一个好的JMH基准测试代码,还是需要注意一些JVM的优化,不然测试结果很可能没有参考意义。
无用代码(Dead Code)优化
JVM如果发现计算的结果没有被使用,那么JVM就会认为这是一段无用代码并优化掉,导致基准测试结果没有参考意义。示例如下:
@Benchmark
public void testMethod3() {
int a = 1 ;
int b = 2 ;
int sum = a + b;
}
在这个示例中,结果sum
没有被其他地方使用,JVM会发现这个情况,然后优化掉这段代码,导致实际上被测试的方法里没有代码被执行。
下面介绍避免无用代码优化的办法。
把结果作为Benchmark方法返回值
可以把上面的testMethod3
改为下面的形式:
@Benchmark
public int testMethod3() {
int a = 1 ;
int b = 2 ;
int sum = a + b;
return sum;
}
现在sum
作为返回值返回,因为这个值在调用方可能会用到,所以JVM就不会优化这段代码了。
把结果传入Blackhole中
也可以把结果放入JMH提供的Blackhole对象中,示例如下:
@Benchmark
public void testMethod3(Blackhole blackhole) {
int a = 1 ;
int b = 2 ;
int sum = a + b;
blackhole.consume(sum);
}
现在sum
作为参数传入Blackhole对象中,因为这个值可能在Blackhole的consume
方法中使用,所以JVM也不会优化这段代码。
常量折叠(Constant Folding)优化
如果一个方法是基于常量的计算,并且结果总是同一个值,那么JVM发现之后,会在之后的调用中,直接把结果作为返回值返回,这就是常量折叠优化。拿上节的testMethod3
作为示例:
@Benchmark
public int testMethod3() {
int a = 1 ;
int b = 2 ;
int sum = a + b;
return sum;
}
在JVM发现sum
的计算是基于常量,并且结果也总是同一个值时,该方法有可能会被优化成如下代码:
@Benchmark
public int testMethod3() {
return 3;
}
甚至有可能在调用的地方直接把结果3内联起来。
为了避免JVM的常量折叠优化,可以把方法中的常量抽取成从其他对象(如:State对象)中获取。示例如下:
@State(Scope.Benchmark)
public static class InputState2 {
int a = 1;
int b = 2;
}
@Benchmark
public void testMethod3(InputState2 inputState2,Blackhole blackhole) {
int sum = inputState2.a + inputState2.b;
blackhole.consume(sum);
}
循环优化
JVM会对循环有一定的优化,所以最好不要在被基准测试的方法外再套一层循环,避免对测试结果的准确性有负面影响。
总结
本文主要总结了JMH的使用方法和注意事项,示例工程在Github上。
参考资料
[1]JMH的Github仓库:https://github.com/openjdk/jmh
[2]https://www.oracle.com/technical-resources/articles/java/architect-benchmarking.html
[3]http://tutorials.jenkov.com/java-performance/jmh.html
[4]Spring Framework的JMH实践策略:https://github.com/spring-projects/spring-framework/wiki/Micro-Benchmarks
评论
发表评论