Oracle官方推荐的性能测试工具!简单、精准又直观!

这是我的第 207 期分享

作者 | 武培轩

来源 | 武培轩(ID:wupeixuan404)

分享 | Java中文社群(ID:javacn666)

磊哥在前面的所有性能优化的文章中都是用了这款性能测试工具——JMH,一直没来得及给大家写一篇入门的教程,刚好看到了这篇,觉得非常不错,于是今天就分享给大家,希望对你有帮助。

if 快还是 switch 快?HashMap 的初始化 size 要不要指定,指定之后性能可以提高多少?各种序列化方法哪个耗时更短?

无论出自何种原因需要进行性能评估,量化指标总是必要的。

在大部分场合,简单地回答谁快谁慢是远远不够的,如何将程序性能量化呢?

这就需要我们的主角 JMH 登场了!

JMH 简介

JMH(Java Microbenchmark Harness)是用于代码微基准测试的工具套件,主要是基于方法层面的基准测试,精度可以达到纳秒级。该工具是由 Oracle 内部实现 JIT 的大牛们编写的,他们应该比任何人都了解 JIT 以及 JVM 对于基准测试的影响。

当你定位到热点方法,希望进一步优化方法性能的时候,就可以使用 JMH 对优化的结果进行量化的分析。

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

  1. 想准确地知道某个方法需要执行多长时间,以及执行时间和输入之间的相关性

  2. 对比接口不同实现在给定条件下的吞吐量

  3. 查看多少百分比的请求在多长时间内完成

下面我们以字符串拼接的两种方法为例子使用 JMH 做基准测试。

加入依赖

因为 JMH 是 JDK9 自带的,如果是 JDK9 之前的版本需要加入如下依赖(目前 JMH 的最新版本为 1.23):

<dependency><groupId>org.openjdk.jmh</groupId><artifactId>jmh-core</artifactId><version>1.23</version>
</dependency>
<dependency><groupId>org.openjdk.jmh</groupId><artifactId>jmh-generator-annprocess</artifactId><version>1.23</version>
</dependency>

编写基准测试

接下来,创建一个 JMH 测试类,用来判断 +StringBuilder.append() 两种字符串拼接哪个耗时更短,具体代码如下所示:

@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;@Benchmarkpublic void testStringAdd(Blackhole blackhole) {String a = "";for (int i = 0; i < length; i++) {a += i;}blackhole.consume(a);}@Benchmarkpublic void testStringBuilderAdd(Blackhole blackhole) {StringBuilder sb = new StringBuilder();for (int i = 0; i < length; i++) {sb.append(i);}blackhole.consume(sb.toString());}public static void main(String[] args) throws RunnerException {Options opt = new OptionsBuilder().include(StringConnectTest.class.getSimpleName()).result("result.json").resultFormat(ResultFormatType.JSON).build();new Runner(opt).run();}
}

其中需要测试的方法用 @Benchmark 注解标识,这些注解的具体含义将在下面介绍。

在 main() 函数中,首先对测试用例进行配置,使用 Builder 模式配置测试,将配置参数存入 Options 对象,并使用 Options 对象构造 Runner 启动测试。

另外大家可以看下官方提供的 jmh 示例 demo:http://hg.openjdk.java.net/code-tools/jmh/file/tip/jmh-samples/src/main/java/org/openjdk/jmh/samples/

执行基准测试

准备工作做好了,接下来,运行代码,等待片刻,测试结果就出来了,下面对结果做下简单说明:

# JMH version: 1.23
# VM version: JDK 1.8.0_201, Java HotSpot(TM) 64-Bit Server VM, 25.201-b09
# VM invoker: D:\Software\Java\jdk1.8.0_201\jre\bin\java.exe
# VM options: -javaagent:D:\Software\JetBrains\IntelliJ IDEA 2019.1.3\lib\idea_rt.jar=61018:D:\Software\JetBrains\IntelliJ IDEA 2019.1.3\bin -Dfile.encoding=UTF-8
# Warmup: 3 iterations, 1 s each
# Measurement: 5 iterations, 5 s each
# Timeout: 10 min per iteration
# Threads: 4 threads, will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: com.wupx.jmh.StringConnectTest.testStringBuilderAdd
# Parameters: (length = 100)

该部分为测试的基本信息,比如使用的 Java 路径,预热代码的迭代次数,测量代码的迭代次数,使用的线程数量,测试的统计单位等。

# Warmup Iteration   1: 1083.569 ±(99.9%) 393.884 ns/op
# Warmup Iteration   2: 864.685 ±(99.9%) 174.120 ns/op
# Warmup Iteration   3: 798.310 ±(99.9%) 121.161 ns/op

该部分为每一次热身中的性能指标,预热测试不会作为最终的统计结果。预热的目的是让 JVM 对被测代码进行足够多的优化,比如,在预热后,被测代码应该得到了充分的 JIT 编译和优化。

Iteration   1: 810.667 ±(99.9%) 51.505 ns/op
Iteration   2: 807.861 ±(99.9%) 13.163 ns/op
Iteration   3: 851.421 ±(99.9%) 33.564 ns/op
Iteration   4: 805.675 ±(99.9%) 33.038 ns/op
Iteration   5: 821.020 ±(99.9%) 66.943 ns/opResult "com.wupx.jmh.StringConnectTest.testStringBuilderAdd":819.329 ±(99.9%) 72.698 ns/op [Average](min, avg, max) = (805.675, 819.329, 851.421), stdev = 18.879CI (99.9%): [746.631, 892.027] (assumes normal distribution)Benchmark                               (length)  Mode  Cnt     Score     Error  Units
StringConnectTest.testStringBuilderAdd       100  avgt    5   819.329 ±  72.698  ns/op

该部分显示测量迭代的情况,每一次迭代都显示了当前的执行速率,即一个操作所花费的时间。在进行 5 次迭代后,进行统计,在本例中,length 为 100 的情况下 testStringBuilderAdd 方法的平均执行花费时间为 819.329 ns,误差为 72.698 ns

最后的测试结果如下所示:

Benchmark                               (length)  Mode  Cnt     Score     Error  Units
StringConnectTest.testStringAdd               10  avgt    5   161.496 ±  17.097  ns/op
StringConnectTest.testStringAdd               50  avgt    5  1854.657 ± 227.902  ns/op
StringConnectTest.testStringAdd              100  avgt    5  6490.062 ± 327.626  ns/op
StringConnectTest.testStringBuilderAdd        10  avgt    5    68.769 ±   4.460  ns/op
StringConnectTest.testStringBuilderAdd        50  avgt    5   413.021 ±  30.950  ns/op
StringConnectTest.testStringBuilderAdd       100  avgt    5   819.329 ±  72.698  ns/op

结果表明,在拼接字符次数越多的情况下,StringBuilder.append() 的性能就更好。

生成 jar 包执行

对于一些小测试,直接用上面的方式写一个 main 函数手动执行就好了。

对于大型的测试,需要测试的时间比较久、线程数比较多,加上测试的服务器需要,一般要放在 Linux 服务器里去执行。

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><transformerimplementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"><mainClass>org.openjdk.jmh.Main</mainClass></transformer></transformers></configuration></execution></executions></plugin>
</plugins>

接着执行 maven 的命令生成可执行 jar 包并执行:

mvn clean install
java -jar target/jmh-demo.jar StringConnectTest

JMH 基础

为了能够更好地使用 JMH 的各项功能,下面对 JMH 的基本概念进行讲解:

@BenchmarkMode

用来配置 Mode 选项,可用于类或者方法上,这个注解的 value 是一个数组,可以把几种 Mode 集合在一起执行,如:@BenchmarkMode({Mode.SampleTime, Mode.AverageTime}),还可以设置为 Mode.All,即全部执行一遍。

  1. Throughput:整体吞吐量,每秒执行了多少次调用,单位为 ops/time

  2. AverageTime:用的平均时间,每次操作的平均时间,单位为 time/op

  3. SampleTime:随机取样,最后输出取样结果的分布

  4. SingleShotTime:只运行一次,往往同时把 Warmup 次数设为 0,用于测试冷启动时的性能

  5. All:上面的所有模式都执行一次

@State

通过 State 可以指定一个对象的作用范围,JMH 根据 scope 来进行实例化和共享操作。@State 可以被继承使用,如果父类定义了该注解,子类则无需定义。由于 JMH 允许多线程同时执行测试,不同的选项含义如下:

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

  2. Scope.Group:同一个线程在同一个 group 里共享实例

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

@OutputTimeUnit

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

@Warmup

预热所需要配置的一些基本测试参数,可用于类或者方法上。一般前几次进行程序测试的时候都会比较慢,所以要让程序进行几轮预热,保证测试的准确性。参数如下所示:

  1. iterations:预热的次数

  2. time:每次预热的时间

  3. timeUnit:时间的单位,默认秒

  4. batchSize:批处理大小,每次操作调用几次方法

为什么需要预热?

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

@Measurement

实际调用方法所需要配置的一些基本测试参数,可用于类或者方法上,参数和 @Warmup 相同。

@Threads

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

@Fork

进行 fork 的次数,可用于类或者方法上。如果 fork 数是 2 的话,则 JMH 会 fork 出两个进程来进行测试。

@Param

指定某项参数的多种情况,特别适合用来测试一个函数在不同的参数输入的情况下的性能,只能作用在字段上,使用该注解必须定义 @State 注解。

在介绍完常用的注解后,让我们来看下 JMH 有哪些陷阱。

JMH 陷阱

在使用 JMH 的过程中,一定要避免一些陷阱。

比如 JIT 优化中的死码消除,比如以下代码:

@Benchmark
public void testStringAdd(Blackhole blackhole) {String a = "";for (int i = 0; i < length; i++) {a += i;}
}

JVM 可能会认为变量 a 从来没有使用过,从而进行优化把整个方法内部代码移除掉,这就会影响测试结果。

JMH 提供了两种方式避免这种问题,一种是将这个变量作为方法返回值 return a,一种是通过 Blackhole 的 consume 来避免 JIT 的优化消除。

其他陷阱还有常量折叠与常量传播、永远不要在测试中写循环、使用 Fork 隔离多个测试方法、方法内联、伪共享与缓存行、分支预测、多线程测试等,感兴趣的可以阅读 https://github.com/lexburner/JMH-samples 了解全部的陷阱。

JMH 插件

大家还可以通过 IDEA 安装 JMH 插件使 JMH 更容易实现基准测试,在 IDEA 中点击 File->Settings...->Plugins,然后搜索 jmh,选择安装 JMH plugin:

JMH plugin

这个插件可以让我们能够以 JUnit 相同的方式使用 JMH,主要功能如下:

  1. 自动生成带有 @Benchmark 的方法

  2. 像 JUnit 一样,运行单独的 Benchmark 方法

  3. 运行类中所有的 Benchmark 方法

比如可以通过右键点击 Generate...,选择操作 Generate JMH benchmark 就可以生成一个带有 @Benchmark 的方法。

还有将光标移动到方法声明并调用 Run 操作就运行一个单独的 Benchmark 方法。

将光标移到类名所在行,右键点击 Run 运行,该类下的所有被 @Benchmark 注解的方法都会被执行。

JMH 可视化

除此以外,如果你想将测试结果以图表的形式可视化,可以试下这些网站:

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

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

比如将上面测试例子结果的 json 文件导入,就可以实现可视化:

总结

本文主要介绍了性能基准测试工具 JMH,它可以通过一些功能来规避由 JVM 中的 JIT 或者其他优化对性能测试造成的影响。

只需要将待测的业务逻辑用 @Benchmark 注解标识,就可以让 JMH 的注解处理器自动生成真正的性能测试代码,以及相应的性能测试配置文件。

最好的关系就是互相成就,大家的在看、转发、留言三连就是我创作的最大动力。

参考

http://openjdk.java.net/projects/code-tools/jmh/

深入拆解Java虚拟机

《实战Java高并发程序设计》

往期推荐

阿里巴巴为什么让初始化集合时必须指定大小?

啪啪打脸!领导说:try-catch要放在循环体外!

关注公众号发送”进群“,磊哥拉你进读者群。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/546094.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

系统美化 XP主题及其他

赢狗系统主题&#xff1a;下载马头桌名主题&#xff1a;下载windows美化大师&#xff1a;下载虽然电脑系统层出不穷但是仍对win xp有爱

zoj 3488 conic section

题目见zoj 3488 很简单的题目&#xff0c;却没能一次搞定&#xff0c;因为没看清楚题目中输入数据都是实数。 该题目考察浮点数的比较&#xff08;因为浮点数在计算机中存储是近似存储&#xff0c;所以不能直接将两个浮点数直接用大于或小于符号相比较&#xff09; /* zoj 3…

Hadoop开发第2期---虚拟机中搭建Linux

注&#xff1a;关于如何将hadoop源码导入Eclipse详见http://pan.baidu.com/s/1hq8ArUs 一、Hadoop配置软件&#xff08;我的电脑是Windows7旗舰--64bit&#xff09; 1. VMWare专用CentOS镜像(Centos是Linux操作系统的一种)2. VMware-workstation103. hadoop-1.1.2.tar.gz4. jdk…

c构造函数和析构函数_C ++构造函数,析构函数能力问题和答案(第2组)

c构造函数和析构函数C 构造函数和析构函数能力问题列表 (List of C Constructor and Destructor Aptitude Questions & Answers) 1) Constructor(s) which is/are added automatically with a class, if we do not create our own constructor? 1)如果我们不创建自己的构造…

看故事学知识,这篇Java代理的文章妙啊!

这是我的第 208 期分享作者 | java金融来源 | java金融&#xff08;ID&#xff1a;java4299&#xff09;分享 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;什么是代理代理模式是常用的java设计模式&#xff0c;他的特征是代理类与委托类有同样的接口&#x…

[下载]青岛交通旅游地图[download]

清闲无事 所以就把青岛地图扫描了一下有需要青岛旅游地图的可以下载the download 【下载】

Java Thread类的静态void sleep(long time_in_ms)方法,带示例

线程类静态无效睡眠(long time_in_ms) (Thread Class static void sleep(long time_in_ms)) This method is available in package java.lang.Thread.sleep(long time_in_ms). 软件包java.lang.Thread.sleep(long time_in_ms)中提供了此方法。 sleep(long time_in_ms) method i…

阿里《Java开发手册》中的 1 个bug!

这是我的第 210 期分享作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;本来打算写一篇《阿里巴巴为什么不允许日志输出时&#xff0c;使用字符串拼接&#xff1f;》的文章&a…

zoj 1006 do the untwist

题目见zoj 1006 或poj 1317 简单的解密算法&#xff0c;直接套用题目中公式即可。 /* zoj 1006 Do the Untwist */ #include <stdio.h> #include <string.h>#define MAXLEN 80 #define MAGICNUM 28char num2Char(int n); int char2Num(char c); int main(void)…

安装完SqlServer2008,wamp服务器无法启动的问题

"开始"->"程序"->Microsoft SQL Server 2008->配置工具->SQL Server配置管理器->SQL Server服务: 只保留SQL Server(MSSQLSERVER)(正在运行)&#xff0c;其他的全部设为停止。 重启wamp服务器成功!

猫版超级玛丽 附下载

不再多言 玩者自知しょぼんのアクション猫版超级玛丽 下载

驳《阿里「Java开发手册」中的1个bug》?

这是我的第 211 期分享作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;前两天写了一篇关于《阿里Java开发手册中的 1 个bug》的文章&#xff0c;评论区有点炸锅了&#xff0…

Java String indexOf(String substr,int fromIndex)方法,带示例

字符串indexOf(String substr&#xff0c;int fromIndex)方法 (String indexOf(String substr, int fromIndex) Method) indexOf(String substr, int fromIndex) is a String method in Java and it is used to get the index of a specified substring in the string from giv…

zoj 1078 palindrom numbers

题目见zoj 1078 主要是判断一个整数在基数为2-16之间的某个数字时是否为回文&#xff0c;我是直接该整数转换成对应基数的表示的逆序列&#xff0c;并计算出该表示下的值&#xff0c;判断是否等于这个整数值&#xff0c;如果相等&#xff0c;那么就是回文&#xff0c;如果不相…

iredmail邮件服务器之修改默认的web服务端口号

安装iredmail之后&#xff0c;由于需要在路由器上做端口映射以便在外网访问webmail&#xff0c;因此端口不能和WEB服务的端口好冲突&#xff0c;所以需要修改邮件服务器的httpd服务的端口。 一、apache/httpd的http服务和https服务端口号都要修改。 基本服务端口好办&#xff0…

轻松学算法的秘密!可视化算法网站汇总!(附动图)

对于「算法」的第一印象&#xff0c;我相信大部分人都是一样的&#xff0c;就是一个“难”字了得。而我比较特殊&#xff0c;我的第一印象、第二印象以至第 N 印象都觉得很难&#xff0c;所以为了更好的学习和理解算法&#xff0c;我千金一掷一下买了一堆的算法书&#xff0c;有…

从100套真题中提炼而出的100个经典句子

1. Typical of the grassland dwellers of the continent is the American antelope, or pronghorn. 1.美洲羚羊&#xff0c;或称叉角羚&#xff0c;是该大陆典型的草原动物。 2. Of the millions who saw Haley’s comet in 1986, how many people will live long enough to s…

字符串大写转小写库函数_PHP程序无需使用库函数即可将字符串转换为大写

字符串大写转小写库函数Given a string and we have to convert it into uppercase string without using any library function. 给定一个字符串&#xff0c;我们必须在不使用任何库函数的情况下将其转换为大写字符串。 PHP code: PHP代码&#xff1a; <?php//function …

zoj 1091 Knight Moves

题目见zoj 1091 使用宽度搜索优先来求解&#xff0c;这个算法已经忘记的差不多了&#xff0c;所以写出来的代码很罗嗦&#xff0c;看起来很不清晰。 好像还可以直接用公式或者神经网络算法求解,详见Knights Tour /* zoj 1091 Knight Moves */ #include <stdio.h> #incl…

1.基本类型的指针

例子一&#xff1a; #include<stdio.h>int main() {int i10;int * p&i; //p是个变量名字&#xff0c;int * 表示该p变量只能存储int类型变量的地址。//int *p&i;等价于 int *p; p&i;//p存放了i的地址,p指向i,*p就是i变量本身printf("%d\n",p);// …