什么是 JMH
JMH 是 Java Microbenchmark Harness 的缩写。中文意思大致是 “JAVA 微基准测试套件”。首先先明白什么是“基准测试”。百度百科给的定义如下:
基准测试是指通过设计科学的测试方法、测试工具和测试系统,实现对一类测试对象的某项性能指标进行定量的和可对比的测试。
可以简单的类比成我们电脑常用的鲁大师,或者手机常用的跑分软件安兔兔之类的性能检测软件。都是按一定的基准或者在特定条件下去测试某一对象的的性能,比如显卡、IO、CPU之类的。
为什么要使用 JMH
基准测试的特质有如下几种:
①、可重复性:可进行重复性的测试,这样做有利于比较每次的测试结果,得到性能结果的长期变化趋势,为系统调优和上线前的容量规划做参考。
②、可观测性:通过全方位的监控(包括测试开始到结束,执行机、服务器、数据库),及时了解和分析测试过程发生了什么。
③、可展示性:相关人员可以直观明了的了解测试结果(web界面、仪表盘、折线图树状图等形式)。
④、真实性:测试的结果反映了客户体验到的真实的情况(真实准确的业务场景+与生产一致的配置+合理正确的测试方法)。
⑤、可执行性:相关人员可以快速的进行测试验证修改调优(可定位可分析)。
可见要做一次符合特质的基准测试,是很繁琐也很困难的。外界因素很容易影响到最终的测试结果。特别对于 JAVA的基准测试。
有些文章会告诉我们 JAVA是 C++编写的,一般来说 JAVA编写的程序不太可能比 C++编写的代码运行效率更好。但是JAVA在某些场景的确要比 C++运行的更高效。不要觉得天方夜谭。其实 JVM随着这些年的发展已经变得很智能,它会在运行期间不断的去优化。
这对于我们程序来说是好事,但是对于性能测试就头疼的。你运行的次数与时间不同可能获得的结果也不同,很难获得一个比较稳定的结果。对于这种情况,有一个解决办法就是大量的重复调用,并且在真正测试前还要进行一定的预热,使结果尽可能的准确。
除了这些,对于结果我们还需要一个很好的展示,可以让我们通过这些展示结果判断性能的好坏。
而这些JMH都有!😊
如何使用 JMH
下面我们以字符串拼接的几种方法为例子使用JMH做基准测试。
1. 导入依赖
JMH是 JDK9自带的,如果你是 JDK9 之前的版本也可以通过导入 openjdk
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.19</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.19</version>
</dependency>
2. 目录结构
.
├── pom.xml
└── src
├── main
│ └── java
│ └── cn
│ └── coder4j
│ └── study
│ └── demo
│ └── jmh
│ ├── benchmark
│ │ └── StringConnectBenchmark.java
│ └── runner
│ └── StringBuilderRunner.java
└── test
└── java
└── cn
└── coder4j
└── study
└── demo
3. 具体代码
- StringBuilderRunner.java
/**
* coder4j.cn
* Copyright (C) 2013-2018 All Rights Reserved.
*/
package cn.coder4j.study.demo.jmh.runner;
import cn.coder4j.study.demo.jmh.benchmark.StringConnectBenchmark;
import org.openjdk.jmh.annotations.Mode;
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;
/**
* @author buhao
* @version StringBuilderRunner.java, v 0.1 2018-12-25 09:53 buhao
*/
public class StringBuilderRunner {
public static void main( String[] args ) throws RunnerException {
Options opt = new OptionsBuilder()
// 导入要测试的类
.include(StringConnectBenchmark.class.getSimpleName())
// 预热5轮
.warmupIterations(5)
// 度量10轮
.measurementIterations(10)
.mode(Mode.Throughput)
.forks(3)
.build();
new Runner(opt).run();
}
}
- StringConnectBenchmark.java
/**
* coder4j.cn
* Copyright (C) 2013-2018 All Rights Reserved.
*/
package cn.coder4j.study.demo.jmh.benchmark;
import org.openjdk.jmh.annotations.Benchmark;
/**
* @author buhao
* @version StringConnectBenchmark.java, v 0.1 2018-12-25 09:29 buhao
*/
public class StringConnectBenchmark {
/**
* 字符串拼接之 StringBuilder 基准测试
*/
@Benchmark
public void testStringBuilder() {
print(new StringBuilder().append(1).append(2).append(3).toString());
}
/**
* 字符串拼接之直接相加基准测试
*/
@Benchmark
public void testStringAdd() {
print(new String()+ 1 + 2 + 3);
}
/**
* 字符串拼接之String Concat基准测试
*/
@Benchmark
public void testStringConcat() {
print(new String().concat("1").concat("2").concat("3"));
}
/**
* 字符串拼接之 StringBuffer 基准测试
*/
@Benchmark
public void testStringBuffer() {
print(new StringBuffer().append(1).append(2).append(3).toString());
}
/**
* 字符串拼接之 StringFormat 基准测试
*/
@Benchmark
public void testStringFormat(){
print(String.format("%s%s%s", 1, 2, 3));
}
public void print(String str) {
}
}
4. 运行结果
# Run progress: 93.33% complete, ETA 00:00:15
# Fork: 3 of 3
objc[12440]: Class JavaLaunchHelper is implemented in both /Library/Java/JavaVirtualMachines/jdk1.8.0_91.jdk/Contents/Home/jre/bin/java (0x106a7d4c0) and /Library/Java/JavaVirtualMachines/jdk1.8.0_91.jdk/Contents/Home/jre/lib/libinstrument.dylib (0x106af74e0). One of the two will be used. Which one is undefined.
# Warmup Iteration 1: 747281.755 ops/s
# Warmup Iteration 2: 924220.081 ops/s
# Warmup Iteration 3: 1129741.585 ops/s
# Warmup Iteration 4: 1135268.541 ops/s
# Warmup Iteration 5: 1062994.936 ops/s
Iteration 1: 1142834.160 ops/s
Iteration 2: 1143207.472 ops/s
Iteration 3: 1178363.827 ops/s
Iteration 4: 1156408.897 ops/s
Iteration 5: 1123123.829 ops/s
Iteration 6: 1086029.992 ops/s
Iteration 7: 1108795.147 ops/s
Iteration 8: 1125522.731 ops/s
Iteration 9: 1120021.744 ops/s
Iteration 10: 1119916.181 ops/s
Result "cn.coder4j.study.demo.jmh.benchmark.StringConnectBenchmark.testStringFormat":
1132633.183 ±(99.9%) 16252.303 ops/s [Average]
(min, avg, max) = (1082146.355, 1132633.183, 1182418.648), stdev = 24325.684
CI (99.9%): [1116380.879, 1148885.486] (assumes normal distribution)
# Run complete. Total time: 00:03:57
Benchmark Mode Cnt Score Error Units
StringConnectBenchmark.testStringAdd thrpt 30 63728919.269 ± 906608.141 ops/s
StringConnectBenchmark.testStringBuffer thrpt 30 112423521.098 ± 1157072.848 ops/s
StringConnectBenchmark.testStringBuilder thrpt 30 110558976.274 ± 654163.111 ops/s
StringConnectBenchmark.testStringConcat thrpt 30 44820009.200 ± 524305.660 ops/s
StringConnectBenchmark.testStringFormat thrpt 30 1132633.183 ± 16252.303 ops/s
5. 代码解析
- StringBuilderRunner
这个 runner 类的作用,就是启动基准测试。
JMH 通常有两种方式启动,一种就是通过命令行使用 maven 命令执行。这种适合对于大型基准测试,像那些要运行很多很多次,并且运行的时间也很长的情况下。你可以直接打个 jar包,发到服务器上,敲个命令就不用管它,过几十分钟、几小时、几天的时间再回来看结果。
但是很多情况下,我们只是想简单测试一个小功能,没必要还要搞台服务器去跑。所以 JMH 还提供了一种通过 Main方法运行的方式,就如上面代码所示。
在 Main 方法中,通过 org.openjdk.jmh.runner.Runner 类去运行 org.openjdk.jmh.runner.options.Options 实例即可。这里的重点在于 Options 对象的构建。官方提供了一个OptionsBuilder对象去构建。这个 Builder对象是流式的。它的常用方法及对应的注解形式如下:
方法名 | 参数 | 作用 | 对应注解 |
---|---|---|---|
include | 要运行基准测试类的简单名称 eg. StringConnectBenchmark | 指定要运行的基准测试类 | - |
exclude | 不要运行基准测试类的简单名称 eg. StringConnectBenchmark | 指定不要运行的基准测试类 | - |
warmupIterations | 预热的迭代次数 | 指定预热的迭代次数 | @Warmup |
warmupBatchSize | 预热批量的大小 | 指定预热批量的大小 | @Warmup |
warmupForks | 预热模式:INDI,BULK,BULK_INDI | 指定预热模式 | @Warmup |
warmupMode | 预热的模式 | 指定预热的模式 | @Warmup |
warmupTime | 预热的时间 | 指定预热的时间 | @Warmup |
measurementIterations | 测试的迭代次数 | 指定测试的迭代次数 | @Measurement |
measurementBatchSize | 测试批量的大小 | 指定测试批量的大小 | @Measurement |
measurementTime | 测试的时间 | 指定测试的时间 | @Measurement |
mode | 测试模式: Throughput(吞吐量), AverageTime(平均时间),SampleTime(在测试中,随机进行采样执行的时间),SingleShotTime(在每次执行中计算耗时),All | 指定测试的模式 | @BenchmarkMode |
- StringConnectBenchmark
这个就是真正执行基准测试的类,这个类很像单元测试的类,每个测试方法中写上你要执行的测试代码。只不过这里把@Test换成了@Benchmark注解。
而加上了这个就指明这个方法是基准测试方法,当 Runner类的 Main方法运行时,它就会找这些被注解修饰的方法,再按指定的规则去进行基准测试。当然可能不同的方法有时候需要不同的规则,这个时间可以通过上面方法对应的注解形式去单独指定某个方法的规则即可。
6. 结果解析
结果主要分成三个部分。
第一部分以 “#Warmup Iteration。。。。”这种形式的内容。这表明每次预热迭代的结果。
另一部分以“Iteration。。。”形式内容,这表明每次基准测试迭代的结果。
最后一部分以“Result。。。”形式的内容,这就是所有迭代跑完最终的结果。第一段结果告诉了我们最大值、最小值、平均值的信息。
而最最后的表格结构的信息才是我们分析的重点,但是它输出的结果有点错位,刚开始我一直在纠结 Error是± 906608.141代表什么意思,google了一圈发现,Error它其实什么都没输出,而且 Score 是63728919.269 ± 906608.141。我用表格排板了一下,解释如下:
Benchmark | Mode | Cnt | Score | Error | Units |
---|---|---|---|---|---|
基准测试执行的方法 | 测试模式,这里是吞吐量 | 运行多少次 | 分数 | 错误 | 单位 |
StringConnectBenchmark.testStringAdd | thrpt | 30 | 63728919.269 ± 906608.141 | ops/s | |
StringConnectBenchmark.testStringBuffer | thrpt | 30 | 112423521.098 ± 1157072.848 | ops/s | |
StringConnectBenchmark.testStringBuilder | thrpt | 30 | 110558976.274 ± 654163.111 | ops/s | |
StringConnectBenchmark.testStringConcat | thrpt | 30 | 44820009.200 ± 524305.660 | ops/s | |
StringConnectBenchmark.testStringFormat | thrpt | 30 | 1132633.183 ± 16252.303 | ops/s |
结论:
StringBuffer >= StringBuilder > String直接相加 > StringConcat >> StringFormat
可见 StringBuffer 与 StringBuilder 大致性能相同,都比直接相加高几个数量级,而且直接相加与 Concat 方法相加差不多。但是这里不管哪种都比 StringFormat高 N 个数量级。所以 String的 Format方法一定要慎用、不用、禁用!!!
相关链接
参考链接
JMH 学习笔记 ← 很不错