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

简介: 【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的配置文件中增加如下依赖,最新的依赖版本可以参考:

pom.xml 添加依赖

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方法都运行在独立的进程中,互不干涉。

java

复制代码

@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:上边所有的都执行一遍。

java

复制代码

@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

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

java

复制代码

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

@Iteration

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

@WarmUp

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

预热的目的和意义

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

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



JAVA

复制代码

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

@Measurement

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

java

复制代码

@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:每个线程组共享一个实例;

java

复制代码

@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 方法的开销将计入到最终结果。

java

复制代码

@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 出两个进程来进行测试。

java

复制代码

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

@Threads

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

java

复制代码

@Threads(4)
public class JmhTest {}
总体的修饰JMH的注解

java

复制代码

@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次。

java

复制代码

@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,具体配置如下:

xml

复制代码

<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>

可视化

相关文章
|
1月前
|
监控 算法 Java
Java虚拟机(JVM)垃圾回收机制深度剖析与优化策略####
本文作为一篇技术性文章,深入探讨了Java虚拟机(JVM)中垃圾回收的工作原理,详细分析了标记-清除、复制算法、标记-压缩及分代收集等主流垃圾回收算法的特点和适用场景。通过实际案例,展示了不同GC(Garbage Collector)算法在应用中的表现差异,并针对大型应用提出了一系列优化策略,包括选择合适的GC算法、调整堆内存大小、并行与并发GC调优等,旨在帮助开发者更好地理解和优化Java应用的性能。 ####
40 0
|
10天前
|
缓存 算法 搜索推荐
Java中的算法优化与复杂度分析
在Java开发中,理解和优化算法的时间复杂度和空间复杂度是提升程序性能的关键。通过合理选择数据结构、避免重复计算、应用分治法等策略,可以显著提高算法效率。在实际开发中,应该根据具体需求和场景,选择合适的优化方法,从而编写出高效、可靠的代码。
25 6
|
15天前
|
Java
Java基础却常被忽略:全面讲解this的实战技巧!
本次分享来自于一道Java基础的面试试题,对this的各种妙用进行了深度讲解,并分析了一些关于this的常见面试陷阱,主要包括以下几方面内容: 1.什么是this 2.this的场景化使用案例 3.关于this的误区 4.总结与练习
|
1月前
|
存储 监控 小程序
Java中的线程池优化实践####
本文深入探讨了Java中线程池的工作原理,分析了常见的线程池类型及其适用场景,并通过实际案例展示了如何根据应用需求进行线程池的优化配置。文章首先介绍了线程池的基本概念和核心参数,随后详细阐述了几种常见的线程池实现(如FixedThreadPool、CachedThreadPool、ScheduledThreadPool等)的特点及使用场景。接着,通过一个电商系统订单处理的实际案例,分析了线程池参数设置不当导致的性能问题,并提出了相应的优化策略。最终,总结了线程池优化的最佳实践,旨在帮助开发者更好地利用Java线程池提升应用性能和稳定性。 ####
|
24天前
|
存储 Java
Java 11 的String是如何优化存储的?
本文介绍了Java中字符串存储优化的原理和实现。通过判断字符串是否全为拉丁字符,使用`byte`代替`char`存储,以节省空间。具体实现涉及`compress`和`toBytes`方法,前者用于尝试压缩字符串,后者则按常规方式存储。代码示例展示了如何根据配置决定使用哪种存储方式。
|
1月前
|
存储 算法 Java
Java 内存管理与优化:掌控堆与栈,雕琢高效代码
Java内存管理与优化是提升程序性能的关键。掌握堆与栈的运作机制,学习如何有效管理内存资源,雕琢出更加高效的代码,是每个Java开发者必备的技能。
55 5
|
1月前
|
存储 监控 算法
Java虚拟机(JVM)垃圾回收机制深度解析与优化策略####
本文旨在深入探讨Java虚拟机(JVM)的垃圾回收机制,揭示其工作原理、常见算法及参数调优方法。通过剖析垃圾回收的生命周期、内存区域划分以及GC日志分析,为开发者提供一套实用的JVM垃圾回收优化指南,助力提升Java应用的性能与稳定性。 ####
|
1月前
|
Java 程序员
Java基础却常被忽略:全面讲解this的实战技巧!
小米,29岁程序员,分享Java中`this`关键字的用法。`this`代表当前对象引用,用于区分成员变量与局部变量、构造方法间调用、支持链式调用及作为参数传递。文章还探讨了`this`在静态方法和匿名内部类中的使用误区,并提供了练习题。
32 1
|
2月前
|
存储 缓存 安全
Java 集合框架优化:从基础到高级应用
《Java集合框架优化:从基础到高级应用》深入解析Java集合框架的核心原理与优化技巧,涵盖列表、集合、映射等常用数据结构,结合实际案例,指导开发者高效使用和优化Java集合。
44 4
|
Java 开发工具 git
Java开发初级6.24.3
5.在Git使用过程中,进行Git配置的操作命令是哪个() A. config B. config -g C. config -a D. git config 相关知识点: 在git中,经常使用git config 命令用来配置git的配置文件,git配置级别主要有:仓库级别 local 【优先级最高】、用户级别 global【优先级次之】、系统级别 system【优先级最低】 正确答案:D 10.RDBMS是什么? A. Rela Database Management Systems B. Relational Database Management Systems C. Relation
142 0