如何在Java中做基准测试?JMH使用初体验
大家好,我是王有志,欢迎和我聊技术,聊漂泊在外的生活。快来加入我们的Java提桶跑路群:共同富裕的Java人。
最近公司在搞新项目,由于是实验性质,且不会直接面对客户的项目,这次的技术选型非常激进,如,直接使用了Java 17。
作为公司里练习两年半的个人练习生,我自然也是深度的参与到了技术选型的工作中。不知道大家在技术选型中有没有关注过技术组件给出的基准测试?比如说,HikariCP的基准测试:
又或者是Caffeine的基准测试:
如果你仔细阅读过它们的基准测试报告,你会发现一项很有意思的技术:Java Microbenchmark Harness,简称JMH。
Tips:有些技术只需要学会如何使用即可,没有必要非得“卷”源码;有些“小众”技术你没有听过,也不必慌,没有人是什么都会的。
认识JMH
接触JMH之前,我通常用System.currentTimeMillis()
来计算方法的执行时间:
long start = System.currentTimeMillis();
......
long duration = System.currentTimeMillis() - start;
大部分时候这么做都很灵,但某些场景下JVM会进行JIT编译和内联优化,导致代码在优化前后的执行效率差别非常大,此时这个“土”方法就不灵了。那么该如何准确的计算方法的执行时间呢?
Java团队为开发者提供了JMH基准测试套件:
JMH is a Java harness for building, running, and analysing nano/micro/milli/macro benchmarks written in Java and other languages targeting the JVM.
JMH是用于构建,运行和分析Java和其它基于JVM的语言编写的程序的基准测试套件。JMH提供了预热的能力,通过预热让JVM知道哪些是热点代码,除此之外,JMH还提供了吞吐量的测试指标。相较于“土”方法,JMH可以支持更多种的测试场景,而且基于JMH得出的测试结果也会更全面,更准确。
使用JMH
项目中引入JMH的依赖:
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.36</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.36</version>
</dependency>
引入依赖后就可以编写一个简单的基准测试了,这里使用简化后的JMH官方示例:
package org.openjdk.jmh.samples;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import java.util.concurrent.TimeUnit;
public class JMHSample_02_BenchmarkModes {
@Benchmark
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public void measureAvgTime() throws InterruptedException {
TimeUnit.MILLISECONDS.sleep(100);
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(JMHSample_02_BenchmarkModes.class.getSimpleName())
.forks(1)
.build();
new Runner(opt).run();
}
}
执行这个示例,会输出如下结果:
以空行为分割的话,JMH的输出可以分为3个部分:
- 基础信息,包括环境信息和基准测试配置;
- 测试信息,每次预热(Warmup)和正式执行(Iteration)的信息;
- 结果信息,基准测试的结果。
Tips:
- IDEA中不能使用DeBug模式运行,否则会报错;
- 注意依赖中的scope标签为test,在src\main\java路径下是无法访问到JMH的。
启动测试
从示例中不难发现,在IDEA中执行测试需要先构建Options
,并通过Runner
去执行。我们来构建一个最简单的Options
:
Options opt = new OptionsBuilder().build();
new Runner(opt).run();
这样的Options
会执行散落在程序各处的基准测试方法(使用Benchmark
注解的方法)。如果不需要执行所有的基准测试方法,通常在构建Options
时会指定测试的范围:
Options opt = new OptionsBuilder()
.include(JMHSample_02_BenchmarkModes.class.getSimpleName())
.build();
这时基准测试仅限于Test
类中的基准测试方法。除此之外,你可能还会嫌弃控制台输出样式丑陋,或者要提交的基准测试报告中需要用图示来直观的表达,这个时候可以控制输出结果的格式并指定结果输出文件:
Options opt = new OptionsBuilder()
.include(JMHSample_02_BenchmarkModes.class.getSimpleName())
.result("result.json")
.resultFormat(ResultFormatType.JSON)
.build();
再结合以下网站,可以很轻松的构建出测试结果图示:
例如,我通过JMH Visual Chart构建出的测试结果:
实际上,OptionsBuilder
提供的功能远不止如此,不过其中大部分功能都可以通过下文中提到注解进行配置,在此就不进行多余的说明了。
常用注解
JMH可以通过注解非常简单的完成基准测试的配置,接下来对其中常用的15个注解进行详细说明。
注解:Benchmark
注解Benchmark
的声明:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Benchmark {
}
Benchmark
用于方法上且该方法必须使用public
修饰,表明该方法为基准测试方法。
注解:BenchmarkMode
注解BenchmarkMode
的声明:
@Inherited
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface BenchmarkMode {
Mode[] value();
}
BenchmarkMode
用于方法或类上,表明测试指标。枚举类Mode提供了4种测试指标:
Mode.Throughput
,吞吐量,单位时间内执行的次数;Mode.AverageTime
,平均时间,执行方法的平均耗时;Mode.SampleTime
,操作时间采样,并输出结果分布;Mode.SingleShotTime
,单次操作时间,通常在不进行预热时测试冷启动的时间。
我们来看下Mode.SampleTime
的输出结果:
除单独使用以上测试指标外,还可以指定Mode.All
进行全部指标的基准测试。
注解:OutputTimeUnit
注解OutputTimeUnit
的声明:
@Inherited
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface OutputTimeUnit {
TimeUnit value();
}
OutputTimeUnit
用于方法或类上,表明输出结果的时间单位。好了,示例中的注解我们已经了解完毕,接下来我们看其它较为关键的注解。
注解:Timeout
注解Timeout
的声明:
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface Timeout {
int time();
TimeUnit timeUnit() default TimeUnit.SECONDS;
}
Timeout
用于方法或类上,指定了基准测试方法的超时时间**。
注解:Warmup
注解Warmup
的声明:
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface Warmup {
int BLANK_ITERATIONS = -1;
int BLANK_TIME = -1;
int BLANK_BATCHSIZE = -1;
int iterations() default BLANK_ITERATIONS;
int time() default BLANK_TIME;
TimeUnit timeUnit() default TimeUnit.SECONDS;
int batchSize() default BLANK_BATCHSIZE;
}
Warmup
用于方法或类上,用于做预热配置。提供了4个参数:
iterations
,预热迭代的次数;time
,每个预热迭代的时间;timeUnit
,时间单位;batchSize
,每个操作调用的次数。
预热的执行结果并不会被统计到测试结果中,因为JIT机制的存在某些方法被反复调用后,JVM会将其便以为机器码,使其执行效率大大提高。
注解:Measurement
注解Measurement
的声明:
@Inherited
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Measurement {
int BLANK_ITERATIONS = -1;
int BLANK_TIME = -1;
int BLANK_BATCHSIZE = -1;
int iterations() default BLANK_ITERATIONS;
int time() default BLANK_TIME;
TimeUnit timeUnit() default TimeUnit.SECONDS;
int batchSize() default BLANK_BATCHSIZE;
}
Measurement
与Warmup
的使用方法完全一致,参数含义也完全相同,区别在于Measurement
属于正式测试的配置,结果会被统计。
注解:Group
注解Group
的声明:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Group {
String value() default "group";
}
Group
用于方法上,为测试方法分组。
注解:State
注解State
的声明:
@Inherited
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface State {
Scope value();
}
State
用于类上,表明了类中变量的作用范围。枚举类Scope
提供了3种作用域:
Scope.Benchmark
,每个测试方法中使用一个变量;Scope.Group
,每个分组中使用同一个变量;Scope.Thread
,每个线程中使用同一个变量。
忘记了是在哪看到有人说Scope.Benchmark
的作用域是所有的基准测试方法,这个是错误的,Scope.Benchmark
会为每个基准测试方法生成一个对象,例如:
@State(Scope.Benchmark)
public static class ThreadState {
}
@Benchmark
@BenchmarkMode(Mode.SingleShotTime)
public void test1(State state) {
System.out.println("test1执行" + VM.current().addressOf(state));
}
@Benchmark
@BenchmarkMode(Mode.SingleShotTime)
public void test2(State state) {
System.out.println("test2执行" + VM.current().addressOf(state));
}
这个例子中,test1
和test2
使用的是不同的State对象。
Tips:VM.current().addressOf()
是jol-core
中提供的功能。
注解:Setup
注解Setup
的声明:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Setup {
Level value() default Level.Trial;
}
Setup
用于方法上,基准测试前的初始化操作。枚举类Level
提供了3个级别:
Level.Trial
,所有基准测试执行时;Level.Iteration
,每次迭代时;Level.Invocation
,每次方法调用时。
Tips:一次迭代中,可能会出现多次方法调用。
注解:TearDown
注解TearDown
的声明:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TearDown {
Level value() default Level.Trial;
}
TearDown
用于方法上,与Setup
的作用相反,是基准测试后的操作,同样使用Level
提供了3个级别。
注解:Param
注解Param
的声明:
@Inherited
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Param {
String BLANK_ARGS = "blank_blank_blank_2014";
String[] value() default { BLANK_ARGS };
}
Param
用于字段上,用于指定不同的参数,需要搭配State注解来使用。举个例子:
@State(Scope.Benchmark)
public class Test {
@Param({"10", "100", "1000", "10000"})
int count;
@Benchmark
@Warmup(iterations = 0)
@BenchmarkMode(Mode.SingleShotTime)
public void loop() throws InterruptedException {
for(int i = 0; i < count; i++) {
TimeUnit.MILLISECONDS.sleep(1);
}
}
}
上述代码测试了程序在循环10次,100次,1000次和10000次时的性能。
注解:Threads
注解Threads
的声明:
@Inherited
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Threads {
int MAX = -1;
int value();
}
Threads
用于方法和类上,指定基准测试中的并行线程数。当使用MAX时,将会使用所有可用线程进行测试,即Runtime.getRuntime().availableProcessors()
返回的线程数。
注解:GroupThreads
注解GroupThreads
的声明:
@Inherited
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface GroupThreads {
int value() default 1;
}
GroupThreads
用于方法上,指定基准测试分组中使用的线程数。
注解:Fork
注解Fork
的声明:
@Inherited
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Fork {
int BLANK_FORKS = -1;
String BLANK_ARGS = "blank_blank_blank_2014";
int value() default BLANK_FORKS;
int warmups() default BLANK_FORKS;
String jvm() default BLANK_ARGS;
String[] jvmArgs() default { BLANK_ARGS };
String[] jvmArgsPrepend() default { BLANK_ARGS };
String[] jvmArgsAppend() default { BLANK_ARGS };
}
Fork
用于方法和类上,指定基准测试中Fork的子进程。Fork
提供了6个参数:
value
,表示Fork出的子进程数量;warmups
,预热次数;jvm
,JVM的位置;jvmArgs
,需要替换的JVM参数;jvmArgsPrepend
,需要添加的JVM参数;jvmArgsAppend
,需要追加的JVM参数。
将Fork
设置为0时,JMH会在当前JVM中运行基准测试。由于可能处于用户的JVM中,无法反应真实的服务端场景,无法准确的反应实际性能,因此JMH推荐进行 Fork
设置。
另外可以利用Fork
提供的JVM设置,将JVM设置为Server模式:
@Fork(value = 1, jvmArgsAppend = {"-Xmx1024m", "-server"})
注解:CompilerControl
注解CompilerControl
的声明:
@Target({ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface CompilerControl {
Mode value();
enum Mode {
BREAK("break"),
PRINT("print"),
EXCLUDE("exclude"),
INLINE("inline"),
DONT_INLINE("dontinline"),
COMPILE_ONLY("compileonly");
}
}
CompilerControl
用于方法,构造器或类上,指定编译方式。其内部枚举类提供了6种编译方式:
BREAK
,将断点插入到编译后的代码;PRINT
,打印方法及其配置;EXCLUDE
,禁止编译;INLINE
,使用内联;DONT_INLINE
,禁止内联;COMPILE_ONLY
,仅编译;
结语
关于JMH的使用,我们就聊到这里了,希望今天的内容能够帮助你学习并掌握一种更准确的性能测试方法。
最后提供一个练习使用JMH的思路:大家都看到了文章开头Caffeine给出的基准测试结果了,但由于是Caffeine作者自己提供的基准测试,难免有些“既当裁判又当选手”的嫌疑,或者说他选取了一些对Caffeine有利的角度来展示结果,那么可以结合你自己的实际使用场景,给Caffeine及其竞品做一次基准测试。
好了,今天就到这里了,Bye~~
如何在Java中做基准测试?JMH使用初体验的更多相关文章
- 如何在Java中调用Python代码
有时候,我们会碰到这样的问题:与A同学合作写代码,A同学只会写Python,而不会Java, 而你只会写Java并不擅长Python,并且发现难以用Java来重写对方的代码,这时,就不得不想方设法“调 ...
- 用代码说话:如何在Java中实现线程
并发编程是Java语言的重要特性之一,"如何在Java中实现线程"是学习并发编程的入门知识,也是Java工程师面试必备的基础知识.本文从线程说起,然后用代码说明如何在Java中实现 ...
- 如何在 Java 中实现 Dijkstra 最短路算法
定义 最短路问题的定义为:设 \(G=(V,E)\) 为连通图,图中各边 \((v_i,v_j)\) 有权 \(l_{ij}\) (\(l_{ij}=\infty\) 表示 \(v_i,v_j\) 间 ...
- 如何在JAVA中实现一个固定最大size的hashMap
如何在JAVA中实现一个固定最大size的hashMap 利用LinkedHashMap的removeEldestEntry方法,重载此方法使得这个map可以增长到最大size,之后每插入一条新的记录 ...
- 如何在java中使用sikuli进行自动化测试
很早之前写过一篇介绍sikuli的文章.本文简单介绍如何在java中使用sikuli进自动化测试. 图形脚本语言sikuli sikuli IDE可以完成常见的单击.右击.移动到.拖动等鼠标操作,ja ...
- [Web 前端] 如何在React中做Ajax 请求?
cp from : https://segmentfault.com/a/1190000007564792 如何在React中做Ajax 请求? 首先:React本身没有独有的获取数据的方式.实际上, ...
- 如何在java中跳出当前多重嵌套循环?有几种方法?
如何在java中跳出当前多重嵌套循环?有几种方法? - 两种方法 - 1.在外层循环定义标记 ok: for(int i=0;i<100;i++){ ...
- 如何在Java中测试类是否是线程安全的
通过优锐课的java核心笔记中,我们可以看到关于如何在java中测试类是否线程安全的一些知识点汇总,分享给大家学习参考. 线程安全性测试与典型的单线程测试不同.为了测试一个方法是否是线程安全的,我们需 ...
- 如何在 Java 中实现无向环和有向环的检测
无向环 一个含有环的无向图如下所示,其中有两个环,分别是 0-2-1-0 和 2-3-4-2: 要检测无向图中的环,可以使用深度优先搜索.假设从顶点 0 出发,再走到相邻的顶点 2,接着走到顶点 2 ...
- 如何在 Java 中实现最小生成树算法
定义 在一幅无向图 \(G=(V,E)\) 中,\((u, v)\) 为连接顶点 \(u\) 和顶点 \(v\) 的边,\(w(u,v)\) 为边的权重,若存在边的子集 \(T\subseteq E\ ...
随机推荐
- Java基础Day4-Java方法
一.方法的定义 Java的方法类似于其它语言的函数,是一段用来完成特定功能的代码片段. 方法包括一个方法头和一个方法体. 修饰符 返回值类型 方法名(参数类型 参数名){ 方法体 ...
- 20193314 白晨阳 实验三 Socket编程技术
学号 2019-2020-2 <Python程序设计>实验三报告 课程:<Python程序设计> 班级: 201933 姓名: 白晨阳 学号: 20193314 实验教师:王志 ...
- python实现录屏功能(亲测好用)
更新时间:2020年03月02日 13:59:52 作者:linnahan https://www.jb51.net/article/181757.htm import time,threading ...
- NODEJS的误打误撞
我接触nodejs 纯属是误打误撞,之前在做一个房地产项目的时候,客户提出了一个需求,我大概整理一些"我们需要员工只能在公司登陆房管系统并进行操作,回家是不允许进行登录的",其实对 ...
- SQL语句底层执行顺序
1. SELECT 语句的完整结构 SQL92语法: SELECT ...,....,(存在聚合函数) FROM ...,...,... WHERE 多表的连接条件 AND 不包含聚合函数的过滤条件 ...
- js字符串常用的方法
1. charAt( ) 获取指定下标处的字符 let str = 'hello' console.log(str.charAt(0));//h 2. charCodeAt 获取下标出的字符的Un ...
- HashMap记录
1.HashMap接收null的键值 2.HashMap是非synchronized的 3.HashMap使用hashCode找到bucket的位置.bucket中存储的是键和值 4.当HashCod ...
- 转 oracle 无法使用sys用户登录 connection as SYS should be as SYSDBA OR SYSOPER
转自: https://blog.csdn.net/u012004128/article/details/80781979 安装Oracle11g后,为了测试安装是否成功,通过cmd命令打开了sql ...
- [Cisco] IOS NAT Load-Balancing for Two ISP Connections
interface FastEthernet0 ip address dhcp ip nat outside ip virtual-reassembly ! interface FastEtherne ...
- vue打包记录
这里的确是css以及js文件的路径问题,但解决时并不需要手动改路径或者加一段判断去修改,最方便的办法时在项目打包前的vue.config.js里面将publicPath属性添加或者修改为 public ...