性能调优必备利器之 JMH
if 快还是 switch 快?HashMap 的初始化 size 要不要指定,指定之后性能可以提高多少?各种序列化方法哪个耗时更短?
无论出自何种原因需要进行性能评估,量化指标总是必要的。
在大部分场合,简单地回答谁快谁慢是远远不够的,如何将程序性能量化呢?
这就需要我们的主角 JMH 登场了!
JMH 简介
JMH(Java Microbenchmark Harness)是用于代码微基准测试的工具套件,主要是基于方法层面的基准测试,精度可以达到纳秒级。该工具是由 Oracle 内部实现 JIT 的大牛们编写的,他们应该比任何人都了解 JIT 以及 JVM 对于基准测试的影响。
当你定位到热点方法,希望进一步优化方法性能的时候,就可以使用 JMH 对优化的结果进行量化的分析。
JMH 比较典型的应用场景如下:
- 想准确地知道某个方法需要执行多长时间,以及执行时间和输入之间的相关性
- 对比接口不同实现在给定条件下的吞吐量
- 查看多少百分比的请求在多长时间内完成
下面我们以字符串拼接的两种方法为例子使用 JMH 做基准测试。
加入依赖
因为 JMH 是 JDK9 自带的,如果是 JDK9 之前的版本需要加入如下依赖(目前 JMH 的最新版本为 1.23
):
<dependency> <groupId>org.openjdk.jmh</groupId> <artifactId>jmh-core</artifactId> <version>1.23</version> </dependency> <dependency> <groupId>org.openjdk.jmh</groupId> <artifactId>jmh-generator-annprocess</artifactId> <version>1.23</version> </dependency>
编写基准测试
接下来,创建一个 JMH 测试类,用来判断 +
和 StringBuilder.append()
两种字符串拼接哪个耗时更短,具体代码如下所示:
@BenchmarkMode(Mode.AverageTime) @Warmup(iterations = 3, time = 1) @Measurement(iterations = 5, time = 5) @Threads @Fork @State(value = Scope.Benchmark) @OutputTimeUnit(TimeUnit.NANOSECONDS) public class StringConnectTest { @Param(value = {"10", "50", "100"}) private int length; @Benchmark public void testStringAdd(Blackhole blackhole) { String a = ""; for (int i = 0; i < length; i++) { a += i; } blackhole.consume(a); } @Benchmark public void testStringBuilderAdd(Blackhole blackhole) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < length; i++) { sb.append(i); } blackhole.consume(sb.toString()); } public static void main(String[] args) throws RunnerException { Options opt = new OptionsBuilder() .include(StringConnectTest.class.getSimpleName()) .result("result.json") .resultFormat(ResultFormatType.JSON).build(); new Runner(opt).run(); } }
其中需要测试的方法用 @Benchmark
注解标识,这些注解的具体含义将在下面介绍。
在 main() 函数中,首先对测试用例进行配置,使用 Builder 模式配置测试,将配置参数存入 Options 对象,并使用 Options 对象构造 Runner 启动测试。
另外大家可以看下官方提供的 jmh 示例 demo:http://hg.openjdk.java.net/code-tools/jmh/file/tip/jmh-samples/src/main/java/org/openjdk/jmh/samples/
执行基准测试
准备工作做好了,接下来,运行代码,等待片刻,测试结果就出来了,下面对结果做下简单说明:
# JMH version: 1.23 # VM version: JDK 1.8.0_201, Java HotSpot(TM) 64-Bit Server VM, 25.201-b09 # VM invoker: D:\Software\Java\jdk1.8.0_201\jre\bin\java.exe # VM options: -javaagent:D:\Software\JetBrains\IntelliJ IDEA 2019.1.3\lib\idea_rt.jar=61018:D:\Software\JetBrains\IntelliJ IDEA 2019.1.3\bin -Dfile.encoding=UTF-8 # Warmup: 3 iterations, 1 s each # Measurement: 5 iterations, 5 s each # Timeout: 10 min per iteration # Threads: 4 threads, will synchronize iterations # Benchmark mode: Average time, time/op # Benchmark: com.wupx.jmh.StringConnectTest.testStringBuilderAdd # Parameters: (length = 100)
该部分为测试的基本信息,比如使用的 Java 路径,预热代码的迭代次数,测量代码的迭代次数,使用的线程数量,测试的统计单位等。
# Warmup Iteration 1: 1083.569 ±(99.9%) 393.884 ns/op # Warmup Iteration 2: 864.685 ±(99.9%) 174.120 ns/op # Warmup Iteration 3: 798.310 ±(99.9%) 121.161 ns/op
该部分为每一次热身中的性能指标,预热测试不会作为最终的统计结果。预热的目的是让 JVM 对被测代码进行足够多的优化,比如,在预热后,被测代码应该得到了充分的 JIT 编译和优化。
Iteration 1: 810.667 ±(99.9%) 51.505 ns/op Iteration 2: 807.861 ±(99.9%) 13.163 ns/op Iteration 3: 851.421 ±(99.9%) 33.
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
专注分享后端技术干货,包括 Java 基础、Java 并发、JVM、Elasticsearch、Zookeeper、Nginx、微服务、消息队列、源码解析、数据库、设计模式、面经等,助你编程之路少走弯路。