【JUC进阶】10. 使用JMH进行性能测试

目录

1、前言

2、传统的性能测试

2、什么是JMH

3、Hello JMH

3.1、Maven相关依赖

3.2、编写简单示例

4、基本属性配置

4.1、@BenchmarkMode

4.2、@Benchmark

4.3、OptionsBuilder & Options

扫描二维码关注公众号,回复: 15668353 查看本文章

4.4、迭代Iteration

4.5、预热(Warmup)

4.6、状态State

5、IDEA JMH插件

6、小结


1、前言

软件开发中,除要写出正确的代码之外,还需要写出高效的代码。这在并发编程中更加重要,原因主要有两点。首先,一部分并发程序由串行程序改造而来,其目的就是提高系统性能,因此,自然需要有一种方法对两种算法进行性能比较。其次,由于业务原因引入的多线程有可能因为线程并发控制导致性能损耗,因此要评估损耗的比重是否可以接受。无论出自何种原因需要进行性能评估,量化指标总是必要的。在大部分场合,简单地回答谁快谁慢是远远不够的,如何将程序性能量化呢? 这就是本节要介绍的 Java 微基准测试框架JMH。

2、传统的性能测试

传统的性能测试,一般会在方法前后打印时间戳,然后通过时间差来判断执行的耗时。

public static void dealHelloWorld() throws InterruptedException {
    // 这里模拟该方法执行
    Thread.sleep(1000);
}

public static void main(String[] args) throws InterruptedException {
    long start0 = System.currentTimeMillis();
    dealHelloWorld();
    long end0 = System.currentTimeMillis();
    System.out.println("执行耗时:" + (end0-start0) + "ms");
}

执行结果:

但是如果代码量较大,而且较为复杂的话,通常需要打印较多的时间戳,然后分段进行计算。就像这样:

这样的话,一方面业务代码中会融入很多的计算时间的代码,增加代码可阅读性;另一方面由于JVM可能会对代码进行运行时优化,比如循环展开、运行时编译等,这样会导致某组未经优化的性能数据参与统计计算。那么这时候就需要JMH了。

3、什么是JMH

JMH(Java Microbenchmark Harness)是Java语言的微基准测试框架,用于准确、可靠地测量和评估Java代码的性能。它是由OpenJDK团队开发的,专门针对Java应用程序的性能测试和基准测试。通过JMH 可以对多个方法的性能进行定量分析。比如,当要知道执行一个函数需要多少时间,或者当对一个算法有多种不同实现时,需要选取性能最好的那个。

JMH官网地址:OpenJDK: jmh

Github地址:https://github.com/openjdk/jmh/tags

4、Hello JMH

我们先来简单尝试使用一下。要使用JMH测试很简单,我们可以联想一下Junit单元测试步骤:

  1. 添加junt相关依赖
  2. 声明测试类,@SpringbootTest,如果使用Mock,需要声明Mock相关初始配置
  3. 声明测试套件,@JunitSuit;也可以直接编写测试类@Test

同样的,JMH也是这些步骤,只是依赖包些许不同。

4.1、Maven相关依赖

<dependencies>

    <!-- JMH核心代码 -->
    <dependency>
        <groupId>org.openjdk.jmh</groupId>
        <artifactId>jmh-core</artifactId>
        <version>1.35</version>
    </dependency>

    <!-- JMH注解相关依赖 -->
    <dependency>
        <groupId>org.openjdk.jmh</groupId>
        <artifactId>jmh-generator-annprocess</artifactId>
        <version>1.35</version>
    </dependency>
</dependencies>

4.2、编写简单示例

/**
 * @author Shamee loop
 * @date 2023/7/1
 */
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.SECONDS)
public class JMHTestHello01 {

    /**
     * @Benchmark 类似于Junit,表示被度量代码标注
     */
    @Benchmark
    public void dealHelloWorld() throws InterruptedException {
        // 这里模拟该方法执行
        Thread.sleep(1000);
    }

    public static void main(String[] args) throws RunnerException {
        Options options = new OptionsBuilder()
                .include(JMHTestHello01.class.getSimpleName())
                .warmupIterations(3)    //  预热的次数, 3次
                .warmupTime(TimeValue.seconds(2))   // 预热的时间,2s
                .forks(1)   // 测试的执行线程数量
                .build();
        new Runner(options).run();
    }
}

执行结果:

# ...... 这里省略部分信息,这些都是描述JDK和JMH的基础信息,基本信息等同于当下的环境以及option中的配置
# Benchmark: org.shamee.jmh.demo.JMHTestHello01.dealHelloWorld

# Run progress: 0.00% complete, ETA 00:00:56
# Fork: 1 of 1
# ......  这里开始预热测试,我们指定了预热3次
# Warmup Iteration   1: 1.005 s/op
# Warmup Iteration   2: 1.010 s/op
# Warmup Iteration   3: 1.007 s/op
# ...... 这里迭代测试进行了5次,以及每次的时间 
Iteration   1: 1.007 s/op
Iteration   2: 1.011 s/op
Iteration   3: 1.012 s/op
Iteration   4: 1.008 s/op
Iteration   5: 1.007 s/op


Result "org.shamee.jmh.demo.JMHTestHello01.dealHelloWorld":
  1.009 ±(99.9%) 0.009 s/op [Average]
  (min, avg, max) = (1.007, 1.009, 1.012), stdev = 0.002
  CI (99.9%): [1.000, 1.018] (assumes normal distribution)


# Run complete. Total time: 00:00:59

REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
experiments, perform baseline and negative tests that provide experimental control, make sure
the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
Do not assume the numbers tell you what you want them to tell.

# ...... 这里显示的汇总结果,cnt 执行了5次  score最后的结果  Error误差±0.009s units时间单位
Benchmark                      Mode  Cnt  Score   Error  Units
JMHTestHello01.dealHelloWorld  avgt    5  1.009 ± 0.009   s/op

Process finished with exit code 0

5、基本属性配置

通过上面的示例代码可以发现,JMH的使用并不复杂,代码量也并不多;很多的功能都是通过配置注解,或者生成Options的属性来进行配置的。因此我们要更好的使用JMH其他功能,就需要对他的一些基本配置要有所了解。

5.1、@BenchmarkMode

基准测试的模式。只有一个Mode属性。而这个Mode属性表示JMH度量的模式,或测试方式。

/**
 * <p>Benchmark mode declares the default modes in which this benchmark
 * would run. See {@link Mode} for available benchmark modes.</p>
 *
 * <p>This annotation may be put at {@link Benchmark} method to have effect
 * on that method only, or at the enclosing class instance to have the effect
 * over all {@link Benchmark} methods in the class. This annotation may be
 * overridden with the runtime options.</p>
 */
@Inherited
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface BenchmarkMode {

    /**
     * @return Which benchmark modes to use.
     * @see Mode
     */
    Mode[] value();

}

Mode提供了多种方式:

  • Throughput整体吞吐量,表示1秒内可以执行多少次调用。
Throughput("thrpt", "Throughput, ops/time")
  • AverageTime 调用的平均时间,指每秒调用所需要的时间。
AverageTime("avgt", "Average time, time/op")
  • SampleTime 随机取样,最后输出取样结果的分布,例如“99%的调用在xxx毫秒内,99.99%的调用在xxx毫秒以内”。
SampleTime("sample", "Sampling time")
  • SingleShotTime 以上模式都是默认1次Iteration为1秒,而这个表示只运行一次。往往会把warmup次数设为0,用于测试冷启动时的性能。
SingleShotTime("ss", "Single shot invocation time")
  • All 将上述的几种模式全部执行一遍。
All("all", "All benchmark modes")

5.2、@Benchmark

@Benchmark 类似于@Test,用于告诉JMH测试覆盖哪些方法。只能注解在方法上,有点类似在测试项目进行package时,JMH会针对注解了@Benchmark的方法生成Benchmark方法代码。通常情况下,每个Benchmark方法都运行在独立的进程中,互不干涉。

5.3、OptionsBuilder & Options

这个是配置类,对测试进行配置。通常需要指定一些参数,如执行测试类(include)、使用的进程个数(fork)、预热迭代次数(warmupInterations)等。在配置启动测试时执行,如上述代码:

Options options = new OptionsBuilder()
        .include(JMHTestHello01.class.getSimpleName())
        .warmupIterations(3)    //  预热的次数, 3次
        .warmupTime(TimeValue.seconds(2))   // 预热的时间,2s
        .forks(1)   // 测试的执行线程数量
        .build();

5.4、迭代Iteration

迭代是JMH 的一次测量单位。在大部分测量模式下,一次迭代表示1秒。在这一秒内会不间断调用被测方法,并采样计算吞吐量、平均时间等。

可以使用OptionsBuilder来配置,也可以使用注解。

Options options = new OptionsBuilder()
        .include(JMHTestHello01.class.getSimpleName())
        .measurementIterations(3).build();    //  执行的次数, 3次

@Measurement(iterations = 3)
@Benchmark
public void dealHelloWorld() throws InterruptedException {
    // 这里模拟该方法执行
    Thread.sleep(1000);
}

5.5、预热(Warmup)

由于 Java 虚拟机的 JIT 的存在,同一个方法在 JIT 编译前后的时间将会不同。通常只考虑方法在 JIT 编译后的性能。预热测试不会作为最终的统计结果,预热的目的是让Java虚拟机对被测试代码进行足够多的优化。

同样的,预热也可以通过OptionsBuilder来配置,也可以使用注解。

Options options = new OptionsBuilder()
        .include(JMHTestHello01.class.getSimpleName())
        .warmupIterations(3).build();    //  预热的次数, 3次

@Warmup(iterations = 3)
@Benchmark
public void dealHelloWorld() throws InterruptedException {
    // 这里模拟该方法执行
    Thread.sleep(1000);
}

5.6、状态State

通过 State 可以指定一个对象的作用范围,JMH中通过Scope来进行实例化和共享操作。

@Inherited
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface State {

    /**
     * State scope.
     * @return state scope
     * @see Scope
     */
    Scope value();

}
  1. Scope.Benchmark:基准测试范围。所有测试线程共享一个实例,测试有状态实例在多线程共享下的性能;
  2. Scope.Group:同一个线程在同一个 group 里共享实例
  3. Scope.Thread:默认的 State,线程范围。也就是个对象只会被一个线程访问。在多线程池测试时,会为每一个线程生成一个对象。
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.SECONDS)
@State(Scope.Benchmark)
public class JMHTestHello01 {
}

6、IDEA JMH插件

类似Junit测试类的代码生成工具,JMH也有相应的测试代码自动生成工具插件。

下载安装插件 JMH Java Microbenchmark Harness。

安装完成后,在需要i生成测试代码的地方鼠标右键 -> Generate -> Generate JMH Benchmark,就可以自动生成。

然后只需要按照实际需求更改需要测试的属性配置,就可以直接鼠标右键运行,查看结果了。

7、小结

实际项目中,通过使用JMH,开发人员可以准确地测量和分析Java代码的性能,并进行性能调优和优化。它可以帮助开发人员更好地理解代码在不同环境下的性能表现,识别性能瓶颈,并找到优化的方向和策略。但是需要注意的是,JMH虽然功能强大,但在使用时需要谨慎选择测试场景和参数,并理解其使用的统计方法和度量指标,以确保测试结果的准确性和可靠性。

猜你喜欢

转载自blog.csdn.net/p793049488/article/details/131583756