您当前的位置:首页 > 电脑百科 > 程序开发 > 语言 > JAVA

JMH 性能测试框架

时间:2023-08-04 14:25:03  来源:微信公众号  作者:小技术君


概述

JMH 是一个由 OpenJDK/Oracle 里面那群开发了 JAVA 编译器的大牛们所开发的 Micro Benchmark Framework 。何谓 Micro Benchmark 呢?简单地说就是在 method 层面上的 benchmark,精度可以精确到微秒级。可以看出 JMH 主要使用在当你已经找出了热点函数,而需要对热点函数进行进一步的优化时,就可以使用 JMH 对优化的效果进行定量的分析。

比较典型的使用场景还有:

•想定量地知道某个函数需要执行多长时间,以及执行时间和输入 n 的相关性•一个函数有两种不同实现(例如实现 A 使用了 FixedThreadPool,实现 B 使用了 ForkJoinPool),不知道哪种实现性能更好

尽管 JMH 是一个相当不错的 Micro Benchmark Framework,但很无奈的是网上能够找到的文档比较少,而官方也没有提供比较详细的文档,对使用造成了一定的障碍。但是有个好消息是官方的 Code Sample 写得非常浅显易懂,推荐在需要详细了解 JMH 的用法时可以通读一遍——本文则会介绍 JMH 最典型的用法和部分常用选项。

第一个例子

如果你使用 maven 来管理你的 Java 项目的话,引入 JMH 是一件很简单的事情——只需要在 pom.xml 里增加 JMH 的依赖即可

<properties>    <jmh.version>1.14.1</jmh.version></properties>
<dependencies>    <dependency>        <groupId>org.openjdk.jmh</groupId>        <artifactId>jmh-core</artifactId>        <version>${jmh.version}</version>    </dependency>    <dependency>        <groupId>org.openjdk.jmh</groupId>        <artifactId>jmh-generator-annprocess</artifactId>        <version>${jmh.version}</version>        <scope>provided</scope>    </dependency></dependencies>

接下来再创建我们的第一个 Benchmark

package com.ckj.base.designPatternes.proxy.DynamicProxy;
import org.openjdk.jmh.annotations.*;import org.springframework.cglib.proxy.Enhancer;import org.springframework.cglib.proxy.MethodInterceptor;import org.springframework.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;import java.util.concurrent.TimeUnit;
/** * @author c.kj * @Description * @Date 2021-03-04 * @Time 21:55 **/@BenchmarkMode(Mode.AverageTime)@OutputTimeUnit(TimeUnit.MICROSECONDS)@State(Scope.Thread)@Fork(1)public class CglibProxy {
    @Benchmark    @Warmup(iterations = 5, time = 100, timeUnit = TimeUnit.MICROSECONDS)    @Measurement(iterations = 5, time = 100, timeUnit = TimeUnit.MICROSECONDS)    public int measureName() throws InterruptedException {       Thread.sleep(1);        getProxyInstance();        return 0;    }
    public Object getProxyInstance() {        Enhancer enhancer = new Enhancer();        enhancer.setSuperclass(CglibTarget.class);        MethodInterceptor methodInterceptor = new MethodInterceptor() {            @Override            public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy)                    throws Throwable {
                System.out.println("intercept start....");                Object o1 = methodProxy.invokeSuper(o, objects);                System.out.println("intercept end....");
                return o1;            }        };        enhancer.setCallback(methodInterceptor);        Object o = enhancer.create();        return o;
    }
}

有不少你可能是第一次见到的注解,不过不着急,接下来会解释这些注解的意义。我们先来跑一下这个 benchmark 吧 :)

# JMH 1.14.1 (released 1780 days ago, please consider updating!)# VM version: JDK 1.8.0_201, VM 25.201-b09# VM invoker: /Library/Java/JavaVirtualmachines/jdk1.8.0_201.jdk/Contents/Home/jre/bin/java# VM options: -Dvisualvm.id=94033462347806 -javaagent:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=56278:/Applications/IntelliJ IDEA.app/Contents/bin -Dfile.encoding=UTF-8# Warmup: 5 iterations, 100 us each# Measurement: 5 iterations, 100 us each# Timeout: 10 min per iteration# Threads: 1 thread, will synchronize iterations# Benchmark mode: Average time, time/op# Benchmark: com.ckj.base.designPatternes.proxy.DynamicProxy.CglibProxy.measureName
# Run progress: 0.00% complete, ETA 00:00:00# Fork: 1 of 1# Warmup Iteration   1: 68584.809 us/op# Warmup Iteration   2: 1312.436 us/op# Warmup Iteration   3: 1329.796 us/op# Warmup Iteration   4: 1329.558 us/op# Warmup Iteration   5: 1296.309 us/opIteration   1: 1296.129 us/opIteration   2: 1121.620 us/opIteration   3: 1603.035 us/opIteration   4: 1306.412 us/opIteration   5: 1194.444 us/op

Result "measureName":  1304.328 ±(99.9%) 706.764 us/op [Average]  (min, avg, max) = (1121.620, 1304.328, 1603.035), stdev = 183.544  CI (99.9%): [597.564, 2011.092] (assumes normal distribution)

# Run complete. Total time: 00:00:01
Benchmark                                                  Mode  Cnt     Score     Error  UnitsdesignPatternes.proxy.DynamicProxy.CglibProxy.measureName  avgt    5  1304.328 ± 706.764  us/op
Process finished with exit code 0

对 getProxyInstance() 的测试结果显示执行时间平均约为1304.328微秒。因为我们的测试对象 1304.328正好就是睡眠1000微秒,所以 JMH 显示的结果可以说很符合我们的预期。

那好,现在我们再来详细地解释代码的意义。不过在这之前,需要先了解一下 JMH 的几个基本概念。

基本概念

Mode

Mode 表示 JMH 进行 Benchmark 时所使用的模式。通常是测量的维度不同,或是测量的方式不同。目前 JMH 共有四种模式:

Throughput: 整体吞吐量,例如“1秒内可以执行多少次调用”。•AverageTime: 调用的平均时间,例如“每次调用平均耗时xxx毫秒”。•SampleTime: 随机取样,最后输出取样结果的分布,例如“99%的调用在xxx毫秒以内,99.99%的调用在xxx毫秒以内”•SingleShotTime: 以上模式都是默认一次 iteration 是 1s,唯有 SingleShotTime 是只运行一次。往往同时把 warmup 次数设为0,用于测试冷启动时的性能。

Iteration

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

Warmup

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

注解

现在来解释一下上面例子中使用到的注解,其实很多注解的意义完全可以望文生义 :)

@Benchmark

表示该方法是需要进行 benchmark 的对象,用法和 JUnit 的 @Test 类似。

@Mode

Mode 如之前所说,表示 JMH 进行 Benchmark 时所使用的模式。

@State

State 用于声明某个类是一个“状态”,然后接受一个 Scope 参数用来表示该状态的共享范围。因为很多 benchmark 会需要一些表示状态的类,JMH 允许你把这些类以依赖注入的方式注入到 benchmark 函数里。Scope 主要分为两种。

Thread: 该状态为每个线程独享。•Benchmark: 该状态在所有线程间共享。

关于State的用法,官方的 code sample 里有比较好的例子。

@OutputTimeUnit

benchmark 结果所使用的时间单位。

启动选项

解释完了注解,再来看看 JMH 在启动前设置的参数。

Options opt = new OptionsBuilder()        .include(FirstBenchmark.class.getSimpleName())        .forks(1)        .warmupIterations(5)        .measurementIterations(5)        .build();
new Runner(opt).run();

include

benchmark 所在的类的名字,注意这里是使用正则表达式对所有类进行匹配的。

fork

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

warmupIterations

预热的迭代次数。

measurementIterations

实际测量的迭代次数。

第二个例子

在看过第一个完全只为示范的例子之后,再来看一个有实际意义的例子。

问题:

计算 1 ~ n 之和,比较串行算法和并行算法的效率,看 n 在大约多少时并行算法开始超越串行算法

首先定义一个表示这两种实现的接口

public interface Calculator {    /**     * calculate sum of an integer array     * @param numbers     * @return     */    public long sum(int[] numbers);
    /**     * shutdown pool or reclAIm any related resources     */    public void shutdown();}

由于这两种算法的实现不是这篇文章的重点,而且本身并不困难,所以实际代码就不赘述了。如果真的感兴趣的话,可以看最后的附录。以下仅说明一下我所指的串行算法和并行算法的含义。

•串行算法:使用 for-loop 来计算 n 个正整数之和。•并行算法:将所需要计算的 n 个正整数分成 m 份,交给 m 个线程分别计算出和以后,再把它们的结果相加。

进行 benchmark 的代码如下

@BenchmarkMode(Mode.AverageTime)@OutputTimeUnit(TimeUnit.MICROSECONDS)@State(Scope.Benchmark)public class SecondBenchmark {    @Param({"10000", "100000", "1000000"})    private int length;
    private int[] numbers;    private Calculator singleThreadCalc;    private Calculator multiThreadCalc;
    public static void main(String[] args) throws RunnerException {        Options opt = new OptionsBuilder()                .include(SecondBenchmark.class.getSimpleName())                .forks(2)                .warmupIterations(5)                .measurementIterations(5)                .build();
        new Runner(opt).run();    }
    @Benchmark    public long singleThreadBench() {        return singleThreadCalc.sum(numbers);    }
    @Benchmark    public long multiThreadBench() {        return multiThreadCalc.sum(numbers);    }
    @Setup    public void prepare() {        numbers = IntStream.rangeClosed(1, length).toArray();        singleThreadCalc = new SinglethreadCalculator();        multiThreadCalc = new MultithreadCalculator(Runtime.getRuntime().availableProcessors());    }
    @TearDown    public void shutdown() {        singleThreadCalc.shutdown();        multiThreadCalc.shutdown();    }}

注意到这里用到了3个之前没有使用的注解。

@Param

@Param 可以用来指定某项参数的多种情况。特别适合用来测试一个函数在不同的参数输入的情况下的性能。

@Setup

@Setup 会在执行 benchmark 之前被执行,正如其名,主要用于初始化。

@TearDown

@TearDown 和 @Setup 相对的,会在所有 benchmark 执行结束以后执行,主要用于资源的回收等。

最后来猜猜看实际结果如何?并行算法在哪个问题集下能够超越串行算法?

我在自己的 mac 上跑下来的结果,总数在10000时并行算法不如串行算法,总数达到100000时并行算法开始和串行算法接近,总数达到1000000时并行算法所耗时间约是串行算法的一半左右。

常用选项

还有一些 JMH 的常用选项没有提及的,简单地在此介绍一下

CompilerControl

控制 compiler 的行为,例如强制 inline,不允许编译等。

Group

可以把多个 benchmark 定义为同一个 group,则它们会被同时执行,主要用于测试多个相互之间存在影响的方法。

Level

用于控制 @Setup@TearDown 的调用时机,默认是 Level.Trial,即benchmark开始前和结束后。

Profiler

JMH 支持一些 profiler,可以显示等待时间和运行时间比,热点函数等。

延伸阅读

IDE插件

IntelliJ 有 JMH 的插件,提供 benchmark 方法的自动生成等便利功能。

JMH 教程

Jenkov 的 JMH 教程,相比于这篇文章介绍得更为详细,非常推荐。顺便 Jenkov 的其他 Java 教程也非常值得一看。

附录

代码清单

public class SinglethreadCalculator implements Calculator {    public long sum(int[] numbers) {        long total = 0L;        for (int i : numbers) {            total += i;        }        return total;    }
    @Override    public void shutdown() {        // nothing to do    }}
public class MultithreadCalculator implements Calculator {    private final int nThreads;    private final ExecutorService pool;
    public MultithreadCalculator(int nThreads) {        this.nThreads = nThreads;        this.pool = Executors.newFixedThreadPool(nThreads);    }
    private class SumTask implements Callable<Long> {        private int[] numbers;        private int from;        private int to;
        public SumTask(int[] numbers, int from, int to) {            this.numbers = numbers;            this.from = from;            this.to = to;        }
        public Long call() throws Exception {            long total = 0L;            for (int i = from; i < to; i++) {                total += numbers[i];            }            return total;        }    }
    public long sum(int[] numbers) {        int chunk = numbers.length / nThreads;
        int from, to;        List<SumTask> tasks = new ArrayList<SumTask>();        for (int i = 1; i <= nThreads; i++) {            if (i == nThreads) {                from = (i - 1) * chunk;                to = numbers.length;            } else {                from = (i - 1) * chunk;                to = i * chunk;            }            tasks.add(new SumTask(numbers, from, to));        }
        try {            List<Future<Long>> futures = pool.invokeAll(tasks);
            long total = 0L;            for (Future<Long> future : futures) {                total += future.get();            }            return total;        } catch (Exception e) {            // ignore            return 0;        }    }
    @Override    public void shutdown() {        pool.shutdown();    }}


Tags:JMH   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
JMH 性能测试框架
概述JMH 是一个由 OpenJDK/Oracle 里面那群开发了 Java 编译器的大牛们所开发的 Micro Benchmark Framework 。何谓 Micro Benchmark 呢?简单地说就是在 method 层面上的 be...【详细内容】
2023-08-04  Search: JMH  点击:(156)  评论:(0)  加入收藏
使用JMH提升Java程序的性能
1 简介作为Java开发人员,在提高应用程序的性能方面,我们经常面临挑战。Java Microbenchmark Harness(JMH)是一个强大的工具,可以帮助我们识别瓶颈,优化我们的代码以提高性能。在...【详细内容】
2023-05-12  Search: JMH  点击:(368)  评论:(0)  加入收藏
Java JMH 基准测试工具
任何新工具的出现,都是为了解决某个具体问题而诞生的,否则就没有存在的必要了本文章将会概述一下 基准测试的概念 、StopWatch的基本使用、 JMH的基本使用与用户JMH中常用注解...【详细内容】
2020-09-07  Search: JMH  点击:(205)  评论:(0)  加入收藏
Java性能优化-掌握JMH
不同需要性能比较方法放到一个虚拟机里调用,有可能会互相影响。最好的办法是分成俩个独立的进程运行,确保俩个对比方法不相互影响。...【详细内容】
2019-08-27  Search: JMH  点击:(1098)  评论:(0)  加入收藏
▌简易百科推荐
Java 8 内存管理原理解析及内存故障排查实践
本文介绍Java8虚拟机的内存区域划分、内存垃圾回收工作原理解析、虚拟机内存分配配置,以及各垃圾收集器优缺点及场景应用、实践内存故障场景排查诊断,方便读者面临内存故障时...【详细内容】
2024-03-20  vivo互联网技术    Tags:Java 8   点击:(14)  评论:(0)  加入收藏
如何编写高性能的Java代码
作者 | 波哥审校 | 重楼在当今软件开发领域,编写高性能的Java代码是至关重要的。Java作为一种流行的编程语言,拥有强大的生态系统和丰富的工具链,但是要写出性能优异的Java代码...【详细内容】
2024-03-20    51CTO  Tags:Java代码   点击:(21)  评论:(0)  加入收藏
在Java应用程序中释放峰值性能:配置文件引导优化(PGO)概述
译者 | 李睿审校 | 重楼在Java开发领域,优化应用程序的性能是开发人员的持续追求。配置文件引导优化(Profile-Guided Optimization,PGO)是一种功能强大的技术,能够显著地提高Ja...【详细内容】
2024-03-18    51CTO  Tags:Java   点击:(24)  评论:(0)  加入收藏
Java生产环境下性能监控与调优详解
堆是 JVM 内存中最大的一块内存空间,该内存被所有线程共享,几乎所有对象和数组都被分配到了堆内存中。堆被划分为新生代和老年代,新生代又被进一步划分为 Eden 和 Survivor 区,...【详细内容】
2024-02-04  大雷家吃饭    Tags:Java   点击:(56)  评论:(0)  加入收藏
在项目中如何避免和解决Java内存泄漏问题
在Java中,内存泄漏通常指的是程序中存在一些不再使用的对象或数据结构仍然保持对内存的引用,从而导致这些对象无法被垃圾回收器回收,最终导致内存占用不断增加,进而影响程序的性...【详细内容】
2024-02-01  编程技术汇  今日头条  Tags:Java   点击:(68)  评论:(0)  加入收藏
Java中的缓存技术及其使用场景
Java中的缓存技术是一种优化手段,用于提高应用程序的性能和响应速度。缓存技术通过将计算结果或者经常访问的数据存储在快速访问的存储介质中,以便下次需要时可以更快地获取。...【详细内容】
2024-01-30  编程技术汇    Tags:Java   点击:(72)  评论:(0)  加入收藏
JDK17 与 JDK11 特性差异浅谈
从 JDK11 到 JDK17 ,Java 的发展经历了一系列重要的里程碑。其中最重要的是 JDK17 的发布,这是一个长期支持(LTS)版本,它将获得长期的更新和支持,有助于保持程序的稳定性和可靠性...【详细内容】
2024-01-26  政采云技术  51CTO  Tags:JDK17   点击:(88)  评论:(0)  加入收藏
Java并发编程高阶技术
随着计算机硬件的发展,多核处理器的普及和内存容量的增加,利用多线程实现异步并发成为提升程序性能的重要途径。在Java中,多线程的使用能够更好地发挥硬件资源,提高程序的响应...【详细内容】
2024-01-19  大雷家吃饭    Tags:Java   点击:(105)  评论:(0)  加入收藏
这篇文章彻底让你了解Java与RPA
前段时间更新系统的时候,发现多了一个名为Power Automate的应用,打开了解后发现是一个自动化应用,根据其描述,可以自动执行所有日常任务,说的还是比较夸张,简单用了下,对于office、...【详细内容】
2024-01-17  Java技术指北  微信公众号  Tags:Java   点击:(95)  评论:(0)  加入收藏
Java 在 2023 年仍然流行的 25 个原因
译者 | 刘汪洋审校 | 重楼学习 Java 的过程中,我意识到在 90 年代末 OOP 正值鼎盛时期,Java 作为能够真正实现这些概念的语言显得尤为突出(尽管我此前学过 C++,但相比 Java 影响...【详细内容】
2024-01-10  刘汪洋  51CTO  Tags:Java   点击:(74)  评论:(0)  加入收藏
站内最新
站内热门
站内头条