乐趣区

关于java:Java-性能调优必备利器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(4)
@Fork(1)
@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 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.564 ns/op
Iteration 4: 805.675 ±(99.9%) 33.038 ns/op
Iteration 5: 821.020 ±(99.9%) 66.943 ns/op

Result “com.wupx.jmh.StringConnectTest.testStringBuilderAdd”:
819.329 ±(99.9%) 72.698 ns/op [Average]
(min, avg, max) = (805.675, 819.329, 851.421), stdev = 18.879
CI (99.9%): [746.631, 892.027] (assumes normal distribution)

Benchmark (length) Mode Cnt Score Error Units
StringConnectTest.testStringBuilderAdd 100 avgt 5 819.329 ± 72.698 ns/op

该局部显示测量迭代的状况,每一次迭代都显示了以后的执行速率,即一个操作所破费的工夫。在进行 5 次迭代后,进行统计,在本例中,length 为 100 的状况下 testStringBuilderAdd 办法的均匀执行破费工夫为 819.329 ns,误差为 72.698 ns。最初的测试后果如下所示:

Benchmark (length) Mode Cnt Score Error Units
StringConnectTest.testStringAdd 10 avgt 5 161.496 ± 17.097 ns/op
StringConnectTest.testStringAdd 50 avgt 5 1854.657 ± 227.902 ns/op
StringConnectTest.testStringAdd 100 avgt 5 6490.062 ± 327.626 ns/op
StringConnectTest.testStringBuilderAdd 10 avgt 5 68.769 ± 4.460 ns/op
StringConnectTest.testStringBuilderAdd 50 avgt 5 413.021 ± 30.950 ns/op
StringConnectTest.testStringBuilderAdd 100 avgt 5 819.329 ± 72.698 ns/op
结果表明,在拼接字符次数越多的状况下,StringBuilder.append() 的性能就更好。

生成 jar 包执行

对于一些小测试,间接用下面的形式写一个 main 函数手动执行就好了。对于大型的测试,须要测试的工夫比拟久、线程数比拟多,加上测试的服务器须要,个别要放在 Linux 服务器里去执行。JMH 官网提供了生成 jar 包的形式来执行,咱们须要在 maven 里减少一个 plugin,具体配置如下:

<plugins>

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-shade-plugin</artifactId>
    <version>2.4.1</version>
    <executions>
        <execution>
            <phase>package</phase>
            <goals>
                <goal>shade</goal>
            </goals>
            <configuration>
                <finalName>jmh-demo</finalName>
                <transformers>
                    <transformer
                            implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                        <mainClass>org.openjdk.jmh.Main</mainClass>
                    </transformer>
                </transformers>
            </configuration>
        </execution>
    </executions>
</plugin>

</plugins>

接着执行 maven 的命令生成可执行 jar 包并执行:

mvn clean install
java -jar target/jmh-demo.jar StringConnectTest

JMH 根底

为了可能更好地应用 JMH 的各项性能,上面对 JMH 的基本概念进行解说:

@BenchmarkMode
用来配置 Mode 选项,可用于类或者办法上,这个注解的 value 是一个数组,能够把几种 Mode 汇合在一起执行,如:@BenchmarkMode({Mode.SampleTime, Mode.AverageTime}),还能够设置为 Mode.All,即全副执行一遍。

Throughput:整体吞吐量,每秒执行了多少次调用,单位为 ops/time
AverageTime:用的均匀工夫,每次操作的均匀工夫,单位为 time/op
SampleTime:随机取样,最初输入取样后果的散布
SingleShotTime:只运行一次,往往同时把 Warmup 次数设为 0,用于测试冷启动时的性能
All:下面的所有模式都执行一次

@State

通过 State 能够指定一个对象的作用范畴,java 培训 JMH 依据 scope 来进行实例化和共享操作。@State 能够被继承应用,如果父类定义了该注解,子类则无需定义。因为 JMH 容许多线程同时执行测试,不同的选项含意如下:

Scope.Benchmark:所有测试线程共享一个实例,测试有状态实例在多线程共享下的性能
Scope.Group:同一个线程在同一个 group 里共享实例
Scope.Thread:默认的 State,每个测试线程调配一个实例

@OutputTimeUnit

为统计后果的工夫单位,可用于类或者办法注解

@Warmup

预热所须要配置的一些根本测试参数,可用于类或者办法上。个别前几次进行程序测试的时候都会比较慢,所以要让程序进行几轮预热,保障测试的准确性。参数如下所示:

iterations:预热的次数
time:每次预热的工夫
timeUnit:工夫的单位,默认秒
batchSize:批处理大小,每次操作调用几次办法

为什么须要预热?因为 JVM 的 JIT 机制的存在,如果某个函数被调用屡次之后,JVM 会尝试将其编译为机器码,从而进步执行速度,所以为了让 benchmark 的后果更加靠近真实情况就须要进行预热。

@Measurement

理论调用办法所须要配置的一些根本测试参数,可用于类或者办法上,参数和 @Warmup 雷同。

@Threads

每个过程中的测试线程,可用于类或者办法上。

@Fork

进行 fork 的次数,可用于类或者办法上。如果 fork 数是 2 的话,则 JMH 会 fork 出两个过程来进行测试。

@Param

指定某项参数的多种状况,特地适宜用来测试一个函数在不同的参数输出的状况下的性能,只能作用在字段上,应用该注解必须定义 @State 注解。在介绍完罕用的注解后,让咱们来看下 JMH 有哪些陷阱。

JMH 陷阱

在应用 JMH 的过程中,肯定要防止一些陷阱。比方 JIT 优化中的死码打消,比方以下代码:

@Benchmark
public void testStringAdd(Blackhole blackhole) {
    String a = "";
    for (int i = 0; i < length; i++) {a += i;}
}

JVM 可能会认为变量 a 素来没有应用过,从而进行优化把整个办法外部代码移除掉,这就会影响测试后果。JMH 提供了两种形式防止这种问题,一种是将这个变量作为办法返回值 return a,一种是通过 Blackhole 的 consume 来防止 JIT 的优化打消。其余陷阱还有常量折叠与常量流传、永远不要在测试中写循环、应用 Fork 隔离多个测试方法、办法内联、伪共享与缓存行、分支预测、多线程测试等,感兴趣的能够浏览 https://github.com/lexburner/… 理解全副的陷阱。

JMH 插件

大家还能够通过 IDEA 装置 JMH 插件使 JMH 更容易实现基准测试,在 IDEA 中点击 File->Settings…->Plugins,而后搜寻 jmh,抉择装置 JMH plugin:

JMH plugin 这个插件能够让咱们可能以 JUnit 雷同的形式应用 JMH,次要性能如下:

主动生成带有 @Benchmark 的办法
像 JUnit 一样,运行独自的 Benchmark 办法
运行类中所有的 Benchmark 办法
比方能够通过右键点击 Generate…,抉择操作 Generate JMH benchmark 就能够生成一个带有 @Benchmark 的办法。还有将光标挪动到办法申明并调用 Run 操作就运行一个独自的 Benchmark 办法。将光标移到类名所在行,右键点击 Run 运行,该类下的所有被 @Benchmark 注解的办法都会被执行。

JMH 可视化

比方将下面测试例子后果的 json 文件导入,就能够实现可视化:

总结

本文次要介绍了性能基准测试工具 JMH,它能够通过一些性能来躲避由 JVM 中的 JIT 或者其余优化对性能测试造成的影响。只须要将待测的业务逻辑用 @Benchmark 注解标识,就能够让 JMH 的注解处理器主动生成真正的性能测试代码,以及相应的性能测试配置文件。

退出移动版