10JMH基准测试框架
面试中容易问到性能测试问题:
Java程序在运行过程中,JIT即时编译器会实时对代码进行性能优化,所以仅凭少量的测试是无法真实反应运行系统最终给用户提供的性能。如下图,随着执行次数的增加,程序性能会逐渐优化。
所以简单地打印时间是不准确的,JIT有可能还没有对程序进行性能优化,我们拿到的测试数据和最终用户使用的数据是不一致的。
OpenJDK中提供了一款叫JMH(Java Microbenchmark Harness)的工具,可以准确地对Java代码进行基准测试,量化方法的执行性能。 官网地址:https://github.com/openjdk/jmhc JMH会首先执行预热过程,确保JIT对代码进行优化之后再进行真正的迭代测试,最后输出测试的结果。
1.JMH环境搭建
创建基准测试项目,在CMD窗口中,使用以下命令创建JMH环境项目:
mvn archetype:generate \
-DinteractiveMode=false \
-DarchetypeGroupId=org.openjdk.jmh \
-DarchetypeArtifactId=jmh-java-benchmark-archetype \
-DgroupId=org.sample \
-DartifactId=test \
-Dversion=1.0
修改POM文件中的JDK版本号和JMH版本号,JMH最新版本号参考Github。
2.JMH编写测试方法
编写测试方法,几个需要注意的点:
- 死代码问题
- 黑洞的用法
初始代码:
packageorg.sample;
importorg.openjdk.jmh.annotations.*;
importorg.openjdk.jmh.results.format.ResultFormatType;
importorg.openjdk.jmh.runner.Runner;
importorg.openjdk.jmh.runner.RunnerException;
importorg.openjdk.jmh.runner.options.Options;
importorg.openjdk.jmh.runner.options.OptionsBuilder;
importjava.text.SimpleDateFormat;
importjava.time.LocalDateTime;
importjava.time.format.DateTimeFormatter;
importjava.util.Date;
importjava.util.concurrent.TimeUnit;
//执行5轮预热,每次持续1秒
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
//执行一次测试
@Fork(value = 1, jvmArgsAppend = {"-Xms1g", "-Xmx1g"})
//显示平均时间,单位纳秒
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Benchmark)
publicclassHelloWorldBench {
@Benchmark
publicinttest1() {
inti = 0;
i++;
returni;
}
publicstaticvoidmain(String[] args) throwsRunnerException {
Options opt = newOptionsBuilder()
.include(HelloWorldBench.class.getSimpleName())
.resultFormat(ResultFormatType.JSON)
.forks(1)
.build();
newRunner(opt).run();
}
}
如果不降i返回,JIT会直接将这段代码去掉,因为它认为你不会使用i那么我们对i进行的任何处理都是没有意义的,这种代码无法执行的现象称之为死代码
我们可以将i返回,或者添加黑洞来消费这些变量,让JIT无法消除这些代码:
通过maven的verify命令,检测代码问题并打包成jar包。通过 java -jar target/benchmarks.jar 命令执行基准测试。
添加这行参数,可以生成JSON文件,测试结果通过JMH Visualizer生成可视化的结果。
3.实际案例
在JDK8中,可以使用Date进行日期的格式化,也可以使用LocalDateTime进行格式化,使用JMH对比这两种格式化的性能。
解决思路:
1、搭建JMH测试环境。
2、编写JMH测试代码。
3、进行测试。
4、比对测试结果。
packageorg.sample;
importorg.openjdk.jmh.annotations.*;
importorg.openjdk.jmh.results.format.ResultFormatType;
importorg.openjdk.jmh.runner.Runner;
importorg.openjdk.jmh.runner.RunnerException;
importorg.openjdk.jmh.runner.options.Options;
importorg.openjdk.jmh.runner.options.OptionsBuilder;
importjava.text.SimpleDateFormat;
importjava.time.LocalDateTime;
importjava.time.format.DateTimeFormatter;
importjava.util.Date;
importjava.util.concurrent.TimeUnit;
//执行5轮预热,每次持续1秒
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
//执行一次测试
@Fork(value = 1, jvmArgsAppend = {"-Xms1g", "-Xmx1g"})
//显示平均时间,单位纳秒
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Thread)
publicclassDateBench {
privatestaticString sDateFormatString = "yyyy-MM-dd HH:mm:ss";
privateDate date = newDate();
privateLocalDateTime localDateTime = LocalDateTime.now();
privatestaticThreadLocal<SimpleDateFormat> simpleDateFormatThreadLocal = newThreadLocal();
privatestaticfinalDateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Setup
publicvoidsetUp() {
SimpleDateFormat sdf = newSimpleDateFormat(sDateFormatString);
simpleDateFormatThreadLocal.set(sdf);
}
@Benchmark
publicString date() {
SimpleDateFormat simpleDateFormat = newSimpleDateFormat(sDateFormatString);
returnsimpleDateFormat.format(date);
}
@Benchmark
publicString localDateTime() {
returnlocalDateTime.format(formatter);
}
@Benchmark
publicString localDateTimeNotSave() {
returnlocalDateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
@Benchmark
publicString dateThreadLocal() {
returnsimpleDateFormatThreadLocal.get().format(date);
}
publicstaticvoidmain(String[] args) throwsRunnerException {
Options opt = newOptionsBuilder()
.include(DateBench.class.getSimpleName())
.resultFormat(ResultFormatType.JSON)
.forks(1)
.build();
newRunner(opt).run();
}
}