写点什么

【Java 优化实战】「微基准系列」带你脚踏实地的进行开发和使用 JMH 测试和提升应用程序和服务指南

作者:洛神灬殇
  • 2023-03-05
    江苏
  • 本文字数:4682 字

    阅读完需:约 15 分钟

【Java优化实战】「微基准系列」带你脚踏实地的进行开发和使用JMH测试和提升应用程序和服务指南

什么是 JMH(微基准测试)

JMH,全称 Java Microbenchmark Harness (微基准测试框架),是专门用于 Java 代码微基准测试的一套测试工具 API,是由 Java 虚拟机团队开发的的,一般用于代码的性能调优。

基准测试 BenchMark

BenchMark 又叫做基准测试,主要用来测试一些方法的性能,可以根据不同的参数以不同的单位进行计算(例如可以使用吞吐量为单位,也可以使用平均时间作为单位,在 BenchmarkMode 里面进行调整)。

微基准测试 MicroBenchmark

MicroBenchmark 就是在 method 层面上的 benchmark,精度可以精确到微秒级、甚至可以达到纳秒级别,适用于 java 以及其他基于 JVM 的语言。与 Apache JMeter 不同,JMH 测试的对象可以是任一方法,颗粒度更小,而不仅限于接口以及 API 层面。

JMH 比较典型的应用场景如下:

  • 想要知道某个函数需要执行多长时间,以及执行时间和输入之间的相关性

  • 想要对比接口不同实现在给定条件下的吞吐量大小

  • 想要知道百分之 N 的请求在多长时间内完成

  • 想要找出了热点函数,需要对热点函数进行进一步优化时

  • 针对于函数的多种实现方式(例如 JSON 序列化/反序列化有 Jackson 和 Gson 实现),不知道哪种实现性能更好

JMH 的开发准备

JMH 的使用可以参考官方示例

JMH 的 Maven 依赖

在 maven 的配置文件中增加如下依赖,最新的依赖版本可以参考:


  • https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-core

  • https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-generator-annprocess

pom.xml 添加依赖

<!-- JMH的核心包 https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-core --><dependency>    <groupId>org.openjdk.jmh</groupId>    <artifactId>jmh-core</artifactId>    <version>1.35</version></dependency>
<!-- JMH依赖注解,需要注解处理包 https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-generator-annprocess --><dependency> <groupId>org.openjdk.jmh</groupId> <artifactId>jmh-generator-annprocess</artifactId> <version>1.35</version></dependency>
复制代码

JMH 基础及常用注解说明

JMH 主要是通过注解的形式编写测试单元,告诉 JMH 如何测试,JMH 自动生成测试代码,所以在使用 JMH 进行微基准测试时一定要先了对 JMH 注解有一定了解,下面就介绍下 JMH 的注解。

@Benchmark

@Benchmark 用于告诉 JMH 哪些方法需要进行测试,只能注解在方法上,JMH 会针对注解了 @Benchmark 的方法生成 Benchmark 方法代码。通常情况下,每个 Benchmark 方法都运行在独立的进程中,互不干涉。


    @Benchmark    public Object testString(BenchmarkState state) {        return state.str.replace(TGT, REPLACEMENT);    }
@Benchmark public Object testStringUtils(BenchmarkState state) { return StringUtils.replace(state.str, TGT, REPLACEMENT); }
@Benchmark public Object testLang3StringUtils(BenchmarkState state) { return org.apache.commons.lang3.StringUtils.replace(state.str, TGT, REPLACEMENT); }
复制代码

@BenchmarkMode

方法注解,表示该方法是需要进行 benchmark 的对象。使用 @BenchmarkMode 指定测试模式,@BenchmarkMode 用于指定当前 Benchmark 方法使用哪种模式测试。JMH 提供了 4 种不同的模式,用于输出不同的结果指标,如下:


  • Throughput:整体吞吐量(时间内程序的执行次数),ops/time。单位时间内执行操作的平均次数

  • AverageTime:平均时间,执行程序的平均耗时,time/op。执行每次操作所需的平均时间

  • SampleTime:执行时间随机取样,输出执行时间的结果分布,time/op,最后输出取样结果的分布。例如“99%的调用在 xxx 毫秒以内,99.99%的调用在 xxx 毫秒以内”

  • SingleShotTime:运行一次,测试冷启动时间,time/op。这种模式的结果存在较大随机性。

  • All:上边所有的都执行一遍。


@Benchmark@BenchmarkMode(Mode.Throughput) // 吞吐量public void measureThroughput() throws InterruptedException {    /* 仅测试吞吐量 */    TimeUnit.MILLISECONDS.sleep(100);}
@Benchmark@BenchmarkMode({Mode.Throughput, Mode.AverageTime, Mode.SampleTime})public void measureMultiple() throws InterruptedException { /* 测试吞吐量、平均时间和抽样时间 */ TimeUnit.MILLISECONDS.sleep(100);}
@Benchmark@BenchmarkMode(Mode.All)public void measureAll() throws InterruptedException { /* 测试所有,即吞吐量、平均时间、抽样时间和启动时间 */ TimeUnit.MILLISECONDS.sleep(100);}
复制代码


单位中的 op 代表的是一次操作,默认一次操作指的是执行一次测试方法。但是我们可以指定调用多少次测试方法算作一次操作。在 JMH 中称作操作中的批处理次数,例如我们可以设置执行五次测试方法算作一次操作。

@OutputTimeUnit

输出的时间单位,为统计结果的时间单位,可用于类或者方法注解。


@OutputTimeUnit(TimeUnit.MILLISECONDS) // 结果所使用的时间单位public class JmhExample{}
复制代码

@Iteration

Iteration 是 JMH 进行测试的最小单位。在大部分模式下,一次 iteration 代表的是一秒,JMH 会在这一秒内不断调用需要 Benchmark 的方法,然后根据模式对其采样,计算吞吐量,计算平均执行时间等。

@WarmUp

Warmup 是指在实际进行 Benchmark 前先进行预热的行为。

预热的目的和意义

JVM 的 JIT 机制的存在,如果某个函数被调用多次之后,JVM 会尝试将其编译成为机器码从而提高执行速度。为了让 Benchmark 的结果更加接近真实情况就需要进行预热。


由于 JVM 会使用 JIT 即时编译器对热点代码进行编译,因此同一份代码可能由于执行次数的增加而导致执行时间差异太大,因此我们可以让代码先预热几轮,预热时间不算入测量计时。



@Warmup(iterations = 5) // 先预热5轮public class JmhSeample {}
复制代码

@Measurement

@Measurement 注解可作用于类或者方法上,用于指定测试的次数、时间和批处理数量,提供真正的测试阶段参数,指定迭代的次数,每次迭代的运行时间和每次迭代测试调用的数量。


@Measurement(iterations = 2) // 进行2轮测试public class JmhSeample {}
复制代码
参数为:
  • iterations:测量次数,默认是 5 次。

  • time:单次测量持续时间,默认是 10。

  • timeUnit:时间单位,指定 time 的单位,默认是秒。

  • batchSize:每次操作的批处理次数,默认是 1,即调用一次测试方法算作一次操作。


@Warmup 和 @Measurement 分别用于配置预热迭代和测试迭代。其中,iterations 用于指定迭代次数,time 和 timeUnit 用于每个迭代的时间,batchSize 表示执行多少次 Benchmark 方法为一个 invocation。

@State

该注解修饰类,JMH 测试类必须使用 @State 注解,它定义了一个类实例的生命周期,可以类比 Spring Bean 的 Scope。由于 JMH 允许多线程同时执行测试,不同的选项含义如下:


  • Scope.Thread:默认的 State,每个测试线程分配一个实例;

  • Scope.Benchmark:所有测试线程共享一个实例,用于测试有状态实例在多线程共享下的性能;

  • Scope.Group:每个线程组共享一个实例;


@State(Scope.Thread) // 每个测试线程分配一个实例public class JMHSample {  public void prepare() {        System.err.println("init............");    }}
复制代码

@Setup

方法注解,会在执行 benchmark 之前被执行,正如其名,主要用于初始化。

@TearDown

方法注解,与 @Setup 相对的,会在所有 benchmark 执行结束以后执行,主要用于资源的回收等。@Setup/@TearDown 注解使用 Level 参数来指定何时调用 fixture。


| 名称 | 描述 || - | :-: | -: || Level.Trial | 默认 level。Benchmark 开始前或结束后执行,如下。Level 为 Benchmark 的 Setup 和 TearDown 方法的开销不会计入到最终结果。 || Level.Iteration | Benchmark 里每个 Iteration 开始前或结束后执行,Level 为 Iteration 的 Setup 和 TearDown 方法的开销不会计入到最终结果。 || Level.Invocation | Iteration 里每次方法调用开始前或结束后执行,如 Level 为 Invocation 的 Setup 和 TearDown 方法的开销将计入到最终结果。 |


@State(Scope.Thread)public class JmhSample22 {
@Setup(Level.Iteration) public void prepare() { System.err.println("init............"); }
@TearDown(Level.Iteration) public void check() { System.err.println("destroy............"); }
@Benchmark public void measureRight() { x++; }}
复制代码

@Fork

进行 fork 的次数。如果 fork 数是 2 的话,则 JMH 会 fork 出两个进程来进行测试。


@Fork(2) // Fork进行的数目public class BenchMark {}
复制代码

@Threads

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


@Threads(4)public class JmhTest {}
复制代码
总体的修饰 JMH 的注解
@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 JmhTest {}
复制代码

@Param

成员注解,可以用来指定某项参数的多种情况。特别适合用来测试一个函数在不同的参数输入的情况下的性能。@Param 注解接收一个 String 数组,在 @Setup 方法执行前转化为为对应的数据类型。多个 @Param 注解的成员之间是乘积关系,譬如有两个用 @Param 注解的字段,第一个有 5 个值,第二个字段有 2 个值,那么每个测试方法会跑 5* 2=10 次。


@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); }
public static void main(String[] args) throws RunnerException { Options opt = new OptionsBuilder() .include(StringConnectTest.class.getSimpleName()); new Runner(opt).run(); }}
复制代码

生成 jar 包执行

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>
复制代码

可视化

  • JMH Visual Chart:http://deepoove.com/jmh-visual-chart/

  • JMH Visualizer:https://jmh.morethan.io/

用户头像

洛神灬殇

关注

🏆 InfoQ写作平台-签约作者 🏆 2020-03-25 加入

【个人简介】酷爱计算机科学、醉心编程技术、喜爱健身运动、热衷悬疑推理的“极客达人” 【技术格言】任何足够先进的技术都与魔法无异 【技术范畴】Java领域、Spring生态、MySQL专项、微服务/分布式体系和算法设计等

评论

发布
暂无评论
【Java优化实战】「微基准系列」带你脚踏实地的进行开发和使用JMH测试和提升应用程序和服务指南_Java_洛神灬殇_InfoQ写作社区