JMH,即Java Microbenchmark Harness,是专门用于Java代码微基准测试的工具套件。由OpenJDK开发的,主要是基于方法层面的基准测试,精度可以达到纳秒级。当定位到热点方法,希望进一步优化方法性能的时候,就可以使用JMH对优化的结果进行量化分析。
使用maven项目演示,这里使用最新版本1.28
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.28</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.28</version>
<scope>provided</scope>
</dependency>
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
public class JMHSample_01_HelloWorld {
@Benchmark
public void wellHelloThere() {
// this method was intentionally left blank.
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(JMHSample_01_HelloWorld.class.getSimpleName())
.build();
new Runner(opt).run();
}
}
以上便是官方最简单的测试,其中度量的方法为wellHelloThere,执行后可判断出该方法的测试基础信息(个人不建议执行,默认的参数执行的时间太长)。
我们再看一个稍微复杂的例子,比对int和Integer的性能:
@BenchmarkMode(Mode.Throughput)
@Warmup(iterations = 1, time = 1)
@Measurement(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS)
@Threads(1)
@Fork(1)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class MMBenchmarkInt {
@Benchmark
public int mmTestInt() {
int result = 0;
for (int i = 0; i < 100; i++) {
result += i;
}
return result;
}
@Benchmark
public Integer mmTestInteger() {
Integer result = 0;
for (int i = 0; i < 100; i++) {
result += i;
}
return result;
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(MMBenchmarkInt.class.getSimpleName())
.build();
new Runner(opt).run();
}
}
@BenchmarkMode为使用模式,参数Mode表示JMH的测量方式和角度,有以下4种:
@Warmup为配置预热次数,本例是每次执行1秒,执行1次。
@Measurement为配置执行次数,本例为每次执行1秒,执行3次。在性能对比时,采用默认1秒即可,如果用jvisualvm来做性能监控,则可指定一个较长的运行时间。
@Threads为配置多少个线程同时执行,
@Fork为启动单独的JVM进程分别测试每个方法,这里指定为每个方法启动1个进程。
@OutputTimeUnit为统计结果的时间单元,本例是TImeUnit.NANOSECONDS,运行后会看到输出结果是统计的每纳秒的吞吐量。
开始会输出本次测试的参数:预测次数、执行次数、测量模式、测试的方法等等。
# Warmup: 1 iterations, 1 s each
# Measurement: 3 iterations, 1 ns each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: com.mm.mmspringboot.test.MMBenchmarkString.mmTest1
接着,这里的Fork表示子进程,我们只配置了一个,因此只有一个进程的执行结果;然后下面包含了Warmup(预热1次,每次1秒);再下面为Measurement(执行3次,每次1秒)。
# Run progress: 0.00% complete, ETA 00:00:08
# Fork: 1 of 1
# Warmup Iteration 1: 0.462 ops/ns
Iteration 1: 0.455 ops/ns
Iteration 2: 0.595 ops/ns
Iteration 3: 0.592 ops/ns
执行完每个测试方法,都会打印一个每个方法的汇总信息。
汇总信息包含了多次测试后的最小值、最大值、均值、标准差(stdev)和置信区间(CI,Confidence interval)。
Result "com.mm.mmspringboot.test.MMBenchmarkInt.mmTestInt":
0.547 ±(99.9%) 1.459 ops/ns [Average]
(min, avg, max) = (0.455, 0.547, 0.595), stdev = 0.080
CI (99.9%): [≈ 0, 2.007] (assumes normal distribution)
最后,JMH会为我们打印出多个方法的测试对比结果,可看出int比Integer的吞吐量高许多:
Benchmark Mode Cnt Score Error Units
MMBenchmarkInt.mmTestInt thrpt 3 0.547 ± 1.459 ops/ns
MMBenchmarkInt.mmTestInteger thrpt 3 0.004 ± 0.002 ops/ns
该例中测量模式为吞吐量
上例中,我们使用的测试模式为Mode.Throughput(吞吐量),单位为ops(Operation Per Second),结合输出单位,就是每纳秒的吞吐量。
下面我们来看看其他测量模式的比对结果,Mode.AverageTime(平局时间),单位为ns/op,即每次吞吐的纳秒,结果如下:
Benchmark Mode Cnt Score Error Units
MMBenchmarkInt.mmTestInt avgt 3 1.842 ± 4.844 ns/op
MMBenchmarkInt.mmTestInteger avgt 3 261.227 ± 380.288 ns/op
Mode.SampleTime(随机采样),可看到分布图(如p0.99的Score为100,表名了99%的每次吞吐的纳秒在100以内),分布结果如下:
可看到在Warmup(预热)和Measurement(执行)中都用到了iterations,它是JMH的测量单位,每次迭代都会统计出相关的吞吐量、平均时间等。
由于Java虚拟机的JIT的存在,同一个方法在JIT编译前后的时间将会不同。通常只考虑方法在JIT编译后的性能,所以需要预热。
配置类用于指定一些参数,如指定测试类(include)、使用的进程个数(fork)、执行迭代次数(measurementIterations)等等。
如上例中的注解属性:
@BenchmarkMode(Mode.Throughput)
@Warmup(iterations = 1, time = 1)
@Measurement(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS)
@Threads(1)
@Fork(1)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class MMBenchmarkProperties {
完全可在配置类里声明:
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(MMBenchmarkProperties.class.getSimpleName())
.mode(Mode.Throughput)
.warmupIterations(1).warmupTime(new TimeValue(1, TimeUnit.SECONDS))
.measurementIterations(3).measurementTime(new TimeValue(1, TimeUnit.SECONDS))
.threads(1)
.forks(1)
.timeUnit(TimeUnit.NANOSECONDS)
.build();
new Runner(opt).run();
}
通过State指定一个对象的作用范围(Scope),取值如下:
使用官方的例子改改,测试如下:
@BenchmarkMode(Mode.Throughput)
@Warmup(iterations = 1, time = 1)
@Measurement(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS)
@Threads(5)
@Fork(1)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class JMHSample_03_States {
@State(Scope.Benchmark)
public static class BenchmarkState {
volatile double x = Math.PI;
}
@State(Scope.Thread)
public static class ThreadState {
volatile double x = Math.PI;
}
@Benchmark
public void ThreadScope(BenchmarkState state) {
state.x++;
}
@Benchmark
public void BenchmarkScope(ThreadState state) {
state.x++;
}
@State(Scope.Group)
public static class GroupState {
volatile double x = Math.PI;
}
@Benchmark
@Group("mmGroup")
@GroupThreads(4)
public double GroupScopeRead(GroupState state) {
return state.x;
}
@Benchmark
@Group("mmGroup")
@GroupThreads(1)
public void GroupScopeWrite(GroupState state) {
state.x++;
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(JMHSample_03_States.class.getSimpleName())
.build();
new Runner(opt).run();
}
}
Benchmark Mode Cnt Score Error Units
BenchmarkScope thrpt 3 0.871 ± 0.054 ops/ns
ThreadScope thrpt 3 0.042 ± 0.002 ops/ns
mmGroup thrpt 3 0.333 ± 0.033 ops/ns
mmGroup:GroupScopeRead thrpt 3 0.294 ± 0.038 ops/ns
mmGroup:GroupScopeWrite thrpt 3 0.039 ± 0.007 ops/ns
控制台输出中,BenchmarkScope和ThreadScope代表着多线程和单线程下的比对结果。mmGroup代表着是一个组,这里为了测试比对数据,通过@GroupThreads分配了读4个线程,写1个线程。
编写JMH用例时需要考虑虚拟机的优化,避免性能测试结果不准。下例由于measureWrong并没有返回值,所以虚拟机会优化掉这个方法。
...省略头信息
public class JMHSample_08_DeadCode {
private double x = Math.PI;
@Benchmark
public void baseline() {
//基准线,什么都不做
}
@Benchmark
public void measureWrong() {
//错误:结果未被使用,整个方法被优化掉了
Math.log(x);
}
@Benchmark
public double measureRight() {
//正确:结果被使用
return Math.log(x);
}
...省略运行信息
}
Benchmark Mode Cnt Score Error Units
JMHSample_08_DeadCode.baseline thrpt 3 4.188 ± 0.840 ops/ns
JMHSample_08_DeadCode.measureRight thrpt 3 0.060 ± 0.012 ops/ns
JMHSample_08_DeadCode.measureWrong thrpt 3 3.405 ± 1.065 ops/ns
使用Blackholes来避免JIT忽略未被使用的信息。
...省略头信息
public class JMHSample_09_Blackholes {
double x1 = Math.PI;
double x2 = Math.PI * 2;
/*
* 基准
*/
@Benchmark
public double baseline() {
return Math.log(x1);
}
/*
* Math.log(x2)正常,Math.log(x1)被优化掉
*/
@Benchmark
public double measureWrong() {
Math.log(x1);
return Math.log(x2);
}
@Benchmark
public double measureRight_1() {
return Math.log(x1) + Math.log(x2);
}
/*
* 使用Blackhole对象将值存储这里,避免被优化掉
*/
@Benchmark
public void measureRight_2(Blackhole bh) {
bh.consume(Math.log(x1));
bh.consume(Math.log(x2));
}
...省略运行信息
}
Benchmark Mode Cnt Score Error Units
JMHSample_09_Blackholes.baseline thrpt 3 0.058 ± 0.026 ops/ns
JMHSample_09_Blackholes.measureRight_1 thrpt 3 0.028 ± 0.050 ops/ns
JMHSample_09_Blackholes.measureRight_2 thrpt 3 0.026 ± 0.018 ops/ns
JMHSample_09_Blackholes.measureWrong thrpt 3 0.055 ± 0.024 ops/ns
JIT认为被测试方法总是返回常量,从而在优化时直接返回常量给调用者而不再调用方法。
...省略头信息
public class JMHSample_10_ConstantFold {
private double x = Math.PI;
private final double wrongX = Math.PI;
@Benchmark
public double baseline() {
// 基准线
return Math.PI;
}
@Benchmark
public double measureWrong_1() {
//错误:源是可预见的,计算是可折叠的
return Math.log(Math.PI);
}
@Benchmark
public double measureWrong_2() {
//错误:源是可预见的,计算是可折叠的
return Math.log(wrongX);
}
@Benchmark
public double measureRight() {
//正确,源是不可预见的
return Math.log(x);
}
...省略运行信息
}
Benchmark Mode Cnt Score Error Units
JMHSample_10_ConstantFold.baseline thrpt 3 0.537 ± 1.565 ops/ns
JMHSample_10_ConstantFold.measureRight thrpt 3 0.060 ± 0.019 ops/ns
JMHSample_10_ConstantFold.measureWrong_1 thrpt 3 0.422 ± 1.372 ops/ns
JMHSample_10_ConstantFold.measureWrong_2 thrpt 3 0.406 ± 1.132 ops/ns
JMH不建议使用循环,因为JIT会种循环做优化,以消除循环调用成本。
...省略头信息
public class JMHSample_11_Loops {
int x = 1;
int y = 2;
@Benchmark
public int measureRight() {
return (x + y);
}
private int reps(int reps) {
int s = 0;
for (int i = 0; i < reps; i++) {
s += (x + y);
}
return s;
}
@Benchmark
@OperationsPerInvocation(1)
public int measureWrong_1() {
return reps(1);
}
@Benchmark
@OperationsPerInvocation(10)
public int measureWrong_10() {
return reps(10);
}
@Benchmark
@OperationsPerInvocation(100)
public int measureWrong_100() {
return reps(100);
}
@Benchmark
@OperationsPerInvocation(1_000)
public int measureWrong_1000() {
return reps(1_000);
}
@Benchmark
@OperationsPerInvocation(10_000)
public int measureWrong_10000() {
return reps(10_000);
}
@Benchmark
@OperationsPerInvocation(100_000)
public int measureWrong_100000() {
return reps(100_000);
}
...省略运行信息
}
上例中,@OperationsPerInvocation为每次Benchmark调用的操作数,其告诉JMH统计性能的时候需要做修正,比如@OperationsPerInvocation(10)调用10次。
Benchmark Mode Cnt Score Error Units
JMHSample_11_Loops.measureRight thrpt 3 0.439 ± 0.447 ops/ns
JMHSample_11_Loops.measureWrong_1 thrpt 3 0.392 ± 0.757 ops/ns
JMHSample_11_Loops.measureWrong_10 thrpt 3 3.755 ± 3.118 ops/ns
JMHSample_11_Loops.measureWrong_100 thrpt 3 30.362 ± 63.189 ops/ns
JMHSample_11_Loops.measureWrong_1000 thrpt 3 34.975 ± 161.861 ops/ns
JMHSample_11_Loops.measureWrong_10000 thrpt 3 51.644 ± 18.460 ops/ns
JMHSample_11_Loops.measureWrong_100000 thrpt 3 56.236 ± 13.568 ops/ns
默认情况下,JMH是分叉测试的。
...省略头信息
public class JMHSample_12_Forking {
public interface Counter {
int inc();
}
public static class Counter1 implements Counter {
private int x;
@Override
public int inc() {
return x++;
}
}
public static class Counter2 implements Counter {
private int x;
@Override
public int inc() {
return x++;
}
}
/**
* And this is how we measure it.
* Note this is susceptible for same issue with loops we mention in previous examples.
*/
public int measure(Counter c) {
int s = 0;
for (int i = 0; i < 10; i++) {
s += c.inc();
}
return s;
}
Counter c1 = new Counter1();
Counter c2 = new Counter2();
//首先单独测量Counter1
//Fork(0)帮助在同一JVM中运行
@Benchmark
@Fork(0)
public int measure_1_c1() {
return measure(c1);
}
//单独Counter2
@Benchmark
@Fork(0)
public int measure_2_c2() {
return measure(c2);
}
//单独Counter1
@Benchmark
@Fork(0)
public int measure_3_c1_again() {
return measure(c1);
}
/*
* 下面两个是带有@Fork注解。
* JMH将此注解作为在forked JVM中运行测试的请求,通过命令选项“-f”强制对所有测试执行此行为更简单。
* forking是默认的,但我们仍使用注释来保持一致性。
*/
@Benchmark
@Fork(1)
public int measure_4_forked_c1() {
return measure(c1);
}
@Benchmark
@Fork(1)
public int measure_5_forked_c2() {
return measure(c2);
}
/*
* 结果可看到fork(0)的方法会越来越慢,因为它们是在同一JVM中运行,相互影响。
* c1,c2,c1_again 的实现相同,跑分却不同,因为运行在同一个 JVM 中;而 forked_c1 和 forked_c2 则表现出了一致的性能。
*/
...省略运行信息
}
Benchmark Mode Cnt Score Error Units
JMHSample_12_Forking.measure_1_c1 thrpt 3 0.508 ± 0.298 ops/ns
JMHSample_12_Forking.measure_2_c2 thrpt 3 0.052 ± 0.199 ops/ns
JMHSample_12_Forking.measure_3_c1_again thrpt 3 0.075 ± 0.072 ops/ns
JMHSample_12_Forking.measure_4_forked_c1 thrpt 3 0.350 ± 0.521 ops/ns
JMHSample_12_Forking.measure_5_forked_c2 thrpt 3 0.346 ± 0.502 ops/ns
JMH基本介绍到这里就结束了。如果大家需要更多的入门实例,可参照官方地址:
http://hg.openjdk.java.net/code-tools/jmh/file/
https://github.com/openjdk/jmh/tree/master/jmh-samples
JMH的可视化非常简单,只需要在运行时导出为json格式的报告,然后再利用可视化工具分析即可,具体如下:
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(JMHSample_09_Blackholes.class.getSimpleName())
.result("result.json")
.resultFormat(ResultFormatType.JSON)
.build();
new Runner(opt).run();
}
运行后,项目下会生成一个result.json的结果报告。
目前JMH的在线可视化主要有如下两款工具,将json结果报告导入即可:
https://jmh.morethan.io/
http://deepoove.com/jmh-visual-chart/