Oracle官方推荐的性能测试工具!简单、精准又直观
ccwgpt 2024-11-26 01:07 29 浏览 0 评论
在前面的所有性能优化的文章中都是用了这款性能测试工具——JMH,一直没来得及给大家写一篇入门的教程,刚好看到了这篇,觉得非常不错,于是今天就分享给大家,希望对你有帮助。
if 快还是 switch 快?HashMap 的初始化 size 要不要指定,指定之后性能可以提高多少?各种序列化方法哪个耗时更短?
无论出自何种原因需要进行性能评估,量化指标总是必要的。
在大部分场合,简单地回答谁快谁慢是远远不够的,如何将程序性能量化呢?
这就需要我们的主角 JMH 登场了!
JMH 简介
JMH(Java Microbenchmark Harness)是用于代码微基准测试的工具套件,主要是基于方法层面的基准测试,精度可以达到纳秒级。该工具是由 Oracle 内部实现 JIT 的大牛们编写的,他们应该比任何人都了解 JIT 以及 JVM 对于基准测试的影响。
当你定位到热点方法,希望进一步优化方法性能的时候,就可以使用 JMH 对优化的结果进行量化的分析。
JMH 比较典型的应用场景如下:
- 想准确地知道某个方法需要执行多长时间,以及执行时间和输入之间的相关性
- 对比接口不同实现在给定条件下的吞吐量
- 查看多少百分比的请求在多长时间内完成
下面我们以字符串拼接的两种方法为例子使用 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;
@Benchmark
public void testStringAdd(Blackhole blackhole) {
String a = "";
for (int i = 0; i < length; i++) {
a += i;
}
blackhole.consume(a);
}
@Benchmark
public 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/op
Result "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.879
CI (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>
<transformer
implementation="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,即全部执行一遍。
- Throughput:整体吞吐量,每秒执行了多少次调用,单位为 ops/time
- AverageTime:用的平均时间,每次操作的平均时间,单位为 time/op
- SampleTime:随机取样,最后输出取样结果的分布
- SingleShotTime:只运行一次,往往同时把 Warmup 次数设为 0,用于测试冷启动时的性能
- All:上面的所有模式都执行一次
@State
通过 State 可以指定一个对象的作用范围,JMH 根据 scope 来进行实例化和共享操作。@State 可以被继承使用,如果父类定义了该注解,子类则无需定义。由于 JMH 允许多线程同时执行测试,不同的选项含义如下:
- Scope.Benchmark:所有测试线程共享一个实例,测试有状态实例在多线程共享下的性能
- Scope.Group:同一个线程在同一个 group 里共享实例
- Scope.Thread:默认的 State,每个测试线程分配一个实例
@OutputTimeUnit
为统计结果的时间单位,可用于类或者方法注解
@Warmup
预热所需要配置的一些基本测试参数,可用于类或者方法上。一般前几次进行程序测试的时候都会比较慢,所以要让程序进行几轮预热,保证测试的准确性。参数如下所示:
- iterations:预热的次数
- time:每次预热的时间
- timeUnit:时间的单位,默认秒
- 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,主要功能如下:
- 自动生成带有 @Benchmark 的方法
- 像 JUnit 一样,运行单独的 Benchmark 方法
- 运行类中所有的 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高并发程序设计》
相关推荐
- 一个基于.Net Core遵循Clean Architecture原则开源架构
-
今天给大家推荐一个遵循CleanArchitecture原则开源架构。项目简介这是基于Asp.netCore6开发的,遵循CleanArchitecture原则,可以高效、快速地构建基于Ra...
- AI写代码翻车无数次,我发现只要提前做好这3步,bug立减80%
-
写十万行全是bug之后终于找到方法了开发"提示词管理助手"新版本那会儿,我差点被bug整崩溃。刚开始两周,全靠AI改代码架构,结果十万行程序漏洞百出。本来以为AI说没问题就稳了,结果...
- OneCode低代码平台的事件驱动设计:架构解析与实践
-
引言:低代码平台的事件驱动范式在现代软件开发中,事件驱动架构(EDA)已成为构建灵活、松耦合系统的核心范式。OneCode低代码平台通过创新性的注解驱动设计,将事件驱动理念深度融入平台架构,实现了业务...
- 国内大厂AI插件评测:根据UI图生成Vue前端代码
-
在IDEA中安装大厂的AI插件,打开ruoyi增强项目:yudao-ui-admin-vue31.CodeBuddy插件登录腾讯的CodeBuddy后,大模型选择deepseek-v3,输入提示语:...
- AI+低代码技术揭秘(二):核心架构
-
本文档介绍了为VTJ低代码平台提供支持的基本架构组件,包括Engine编排层、Provider服务系统、数据模型和代码生成管道。有关UI组件库和widget系统的信息,请参阅UI...
- GitDiagram用AI把代码库变成可视化架构图
-
这是一个名为gitdiagram的开源工具,可将GitHub仓库实时转换为交互式架构图,帮助开发者快速理解代码结构。核心功能一键可视化:替换GitHubURL中的"hub...
- 30天自制操作系统:第六天:代码架构整理与中断处理
-
1.拆开bootpack.c文件。根据设计模式将对应的功能封装成独立的文件。2.初始化pic:pic(可编程中断控制器):在设计上,cpu单独只能处理一个中断。而pic是将8个中断信号集合成一个中断...
- AI写代码越帮越忙?2025年研究揭露惊人真相
-
近年来,AI工具如雨后春笋般涌现,许多人开始幻想程序员的未来就是“对着AI说几句话”,就能轻松写出完美的代码。然而,2025年的一项最新研究却颠覆了这一期待,揭示了一个令人意外的结果。研究邀请了16位...
- 一键理解开源项目:两个自动生成GitHub代码架构图与说明书工具
-
一、GitDiagram可以一键生成github代码仓库的架构图如果想要可视化github开源项目:https://github.com/luler/reflex_ai_fast,也可以直接把域名替换...
- 5分钟掌握 c# 网络通讯架构及代码示例
-
以下是C#网络通讯架构的核心要点及代码示例,按协议类型分类整理:一、TCP协议(可靠连接)1.同步通信//服务器端usingSystem.Net.Sockets;usingTcpListene...
- 从复杂到优雅:用建造者和责任链重塑代码架构
-
引用设计模式是软件开发中的重要工具,它为解决常见问题提供了标准化的解决方案,提高了代码的可维护性和可扩展性,提升了开发效率,促进了团队协作,提高了软件质量,并帮助开发者更好地适应需求变化。通过学习和应...
- 低代码开发当道,我还需要学习LangChain这些框架吗?| IT杂谈
-
专注LLM深度应用,关注我不迷路前两天有位兄弟问了个问题:当然我很能理解这位朋友的担忧:期望效率最大化,时间用在刀刃上,“不要重新发明轮子”嘛。铺天盖地的AI信息轰炸与概念炒作,很容易让人浮躁与迷茫。...
- 框架设计并不是简单粗暴地写代码,而是要先弄清逻辑
-
3.框架设计3.框架设计本节我们要开发一个UI框架,底层以白鹭引擎为例。框架设计的第一步并不是直接撸代码,而是先想清楚设计思想,抽象。一个一个的UI窗口是独立的吗?不是的,...
- 大佬用 Avalonia 框架开发的 C# 代码 IDE
-
AvalonStudioAvalonStudio是一个开源的跨平台的开发编辑器(IDE),AvalonStudio的目标是成为一个功能齐全,并且可以让开发者快速使用的IDE,提高开发的生产力。A...
- 轻量级框架Lagent 仅需20行代码即可构建自己的智能代理
-
站长之家(ChinaZ.com)8月30日消息:Lagent是一个专注于基于LLM模型的代理开发的轻量级框架。它的设计旨在简化和提高这种模型下代理的开发效率。LLM模型是一种强大的工具,可以...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- 框架图 (58)
- flask框架 (53)
- quartz框架 (51)
- abp框架 (47)
- springmvc框架 (49)
- 分布式事务框架 (65)
- scrapy框架 (56)
- shiro框架 (61)
- 定时任务框架 (56)
- java日志框架 (61)
- mfc框架 (52)
- abb框架断路器 (48)
- beego框架 (52)
- java框架spring (58)
- grpc框架 (65)
- tornado框架 (48)
- 前端框架bootstrap (54)
- orm框架有哪些 (51)
- 知识框架图 (52)
- ppt框架 (55)
- 框架图模板 (59)
- 内联框架 (52)
- cad怎么画框架 (58)
- ssm框架实现登录注册 (49)
- oracle字符串长度 (48)