计算机各个组件之间的速度往往很不均衡,比如 CPU 和硬盘,比兔子和乌龟的速度差还大,那么按照木桶理论,可以说这个系统是存在着短板的。当系统存在短板时,就会对性能造成较大的负面影响,比如当 CPU 的负载特别高时,任务就会排队,不能及时执行。而其中,CPU、内存、I/O 这三个系统组件,又往往容易成为瓶颈
首先是计算机中最重要的计算组件中央处理器 CPU,围绕 CPU 一般我们可以
- 通过 top 命令,来观测 CPU 的性能;
- 通过负载,评估 CPU 任务执行的排队情况;
- 通过 vmstat,看 CPU 的繁忙程度
1.top 命令 —— CPU 性能
当进入 top 命令后,按 1 键即可看到每核 CPU 的运行指标和详细性能
CPU 的使用有多个维度的指标,下面分别说明
- us 用户态所占用的 CPU 百分比,即引用程序所耗费的 CPU;
- sy 内核态所占用的 CPU 百分比,需要配合 vmstat 命令,查看上下文切换是否频繁;
- ni 高优先级应用所占用的 CPU 百分比;
- wa 等待 I/O 设备所占用的 CPU 百分比,经常使用它来判断 I/O 问题,过高输入输出设备可能存在非常明显的瓶颈;
- hi 硬中断所占用的 CPU 百分比;
- si 软中断所占用的 CPU 百分比;
- st 在平常的服务器上这个值很少发生变动,因为它测量的是宿主机对虚拟机的影响,即虚拟机等待宿主机 CPU 的时间占比,这在一些超卖的云服务器上,经常发生;
- id 空闲 CPU 百分比
2.负载 —— CPU 任务排队情况
如果我们评估 CPU 任务执行的排队情况,那么需要通过负载(load)来完成。除了 top 命令,使用 uptime 命令也能够查看负载情况,load 的效果是一样的,分别显示了最近 1min、5min、15min 的数值
如上图所示,以单核操作系统为例,将 CPU 资源抽象成一条单向行驶的马路,则会发生以下三种情况:
- 马路上的车只有 4 辆,车辆畅通无阻,load 大约是 0.5;
- 马路上的车有 8 辆,正好能首尾相接安全通过,此时 load 大约为 1;
- 马路上的车有 12 辆,除了在马路上的 8 辆车,还有 4 辆等在马路外面,需要排队,此时 load 大约为 1.5
load 为 1 代表的是啥?
很多人看到 load 的值达到 1,就认为系统负载已经到了极限。这在单核的硬件上没有问题,但在多核硬件上,这种描述就不完全正确,它还与 CPU 的个数有关。例如
- 单核的负载达到 1,总 load 的值约为 1;
- 双核的每核负载都达到 1,总 load 约为 2;
- 四核的每核负载都达到 1,总 load 约为 4
所以,对于一个 load 到了 10,却是 16 核的机器,你的系统还远没有达到负载极限
3.vmstat —— CPU 繁忙程度
要看 CPU 的繁忙程度,可以通过 vmstat 命令,下图是 vmstat 命令的一些输出信息
比较关注的有下面几列
- b 如果系统有负载问题,就可以看一下 b 列(Uninterruptible Sleep),它的意思是等待 I/O,可能是读盘或者写盘动作比较多;
- si/so 显示了交换分区的一些使用情况,交换分区对性能的影响比较大,需要格外关注;
- cs 每秒钟上下文切换(Context Switch)的数量,如果上下文切换过于频繁,就需要考虑是否是进程或者线程数开的过多
每个进程上下文切换的具体数量,可以通过查看内存映射文件获取,如下代码所示
[root@localhost ~]# cat /proc/2788(进程号)/status
...
voluntary_ctxt_switches: 93950
nonvoluntary_ctxt_switches: 171204
我们在平常写完代码后,比如写了一个 C++ 程序,去查看它的汇编,如果看到其中的内存地址,并不是实际的物理内存地址,那么应用程序所使用的,就是逻辑内存
逻辑地址可以映射到两个内存段上:物理内存和虚拟内存,那么整个系统可用的内存就是两者之和。比如你的物理内存是 4GB,分配了 8GB 的 SWAP 分区,那么应用可用的总内存就是 12GB
1. top 命令
如上图所示,我们看一下内存的几个参数,从 top 命令可以看到几列数据,注意方块框起来的三个区域,解释如下
VIRT 这里是指虚拟内存,一般比较大,不用做过多关注;
RES 我们平常关注的是这一列的数值,它代表了进程实际占用的内存,平常在做监控时,主要监控的也是这个数值;
SHR 指的是共享内存,比如可以复用的一些 so 文件等
2. CPU 缓存
由于 CPU 和内存之间的速度差异非常大,解决方式就是加入高速缓存。实际上,这些高速缓存往往会有多层,如下图所示
Java 有大部分知识点是围绕多线程的,那是因为,如果一个线程的时间片跨越了多个 CPU,那么就会存在同步问题,在 Java 中,和 CPU 缓存相关的最典型的知识点,就是在并发编程中,针对 Cache line 的伪共享(False Sharing)问题
伪共享指的是在这些高速缓存中,以缓存行为单位进行存储,哪怕你修改了缓存行中一个很小很小的数据,它都会整个刷新。所以,当多线程修改一些变量的值时,如果这些变量都在同一个缓存行里,就会造成频繁刷新,无意中影响彼此的性能
CPU 的每个核,基本是相同的,我们拿 CPU0 来说,可以通过以下的命令查看它的缓存行大小,这个值一般是 64
cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size
cat /sys/devices/system/cpu/cpu0/cache/index1/coherency_line_size
cat /sys/devices/system/cpu/cpu0/cache/index2/coherency_line_size
cat /sys/devices/system/cpu/cpu0/cache/index3/coherency_line_size
当然,通过 cpuinfo 也能得到一样的结果
# cat /proc/cpuinfo | grep cache
cache size : 33792 KB
cache_alignment : 64
cache size : 33792 KB
cache_alignment : 64
在 JDK8 以上的版本,通过开启参数 -XX:-RestrictContended,就可以使用注解 @sun.misc.Contended 进行补齐,来避免伪共享的问题
3. HugePage
上图有一个 TLB 组件,它的速度很快,但容量有限,在普通的 PC 机上没有什么瓶颈。但如果机器配置比较高,物理内存比较大,那就会产生非常多的映射表,CPU 的检索效率也会随之降低
传统的页大小是 4KB,在大内存时代这个值偏小了,解决的办法就是增加页的尺寸,比如将其增加到 2MB,这样,就可以使用较少的映射表来管理大内存。而这种将页增大的技术,就是 Huge Page
同时,HugePage 也伴随着一些副作用,比如竞争加剧,但在一些大内存的机器上,开启后在一定程度上会增加性能
4. 预先加载
一些程序的默认行为也会对性能有所影响,比如 JVM 的 -XX:+AlwaysPreTouch 参数
默认情况下,JVM 虽然配置了 Xmx、Xms 等参数,指定堆的初始化大小和最大大小,但它的内存在真正用到时,才会分配;但如果加上 AlwaysPreTouch 这个参数,JVM 会在启动的时候,就把所有的内存预先分配。这样,启动时虽然慢了些,但运行时的性能会增加
I/O 设备可能是计算机里速度最慢的组件了,它指的不仅仅是硬盘,还包括外围的所有设备,普通磁盘的随机写与顺序写相差非常大,但顺序写与 CPU 内存依旧不在一个数量级上
缓冲区依然是解决速度差异的唯一工具,但在极端情况下,比如断电时,就产生了太多的不确定性,这时这些缓冲区,都容易丢
1. iostat
最能体现 I/O 繁忙程度的,就是 top 命令和 vmstat 命令中的 wa%。如果你的应用写了大量的日志,I/O wait 就可能非常高
iostat
指标详细介绍如下所示
%util:我们非常关注这个数值,通常情况下,这个数字超过 80%,就证明 I/O 的负荷已经非常严重了
Device:表示是哪块硬盘,如果你有多块磁盘,则会显示多行
avgqu-sz:平均请求队列的长度,这和十字路口排队的汽车也非常类似。显然,这个值越小越好
awai:响应时间包含了队列时间和服务时间,它有一个经验值。通常情况下应该是小于 5ms 的,如果这个值超过了 10ms,则证明等待的时间过长了
svctm:表示操作 I/O 的平均服务时间。svctm 和 await 是强相关的,如果它们比较接近,则表示 I/O 几乎没有等待,设备的性能很好;但如果 await 比 svctm 的值高出很多,则证明 I/O 的队列等待时间太长,进而系统上运行的应用程序将变慢
2. 零拷贝
硬盘上的数据,在发往网络之前,需要经过多次缓冲区的拷贝,以及用户空间和内核空间的多次切换。如果能减少一些拷贝的过程,效率就能提升,所以零拷贝应运而生
零拷贝是一种非常重要的性能优化手段,比如常见的 Kafka、Nginx 等,就使用了这种技术。来看一下有无零拷贝之间的区别
如下图所示,传统方式中要想将一个文件的内容通过 Socket 发送出去,则需要经过以下步骤
将文件内容拷贝到内核空间;
将内核空间内存的内容,拷贝到用户空间内存,比如 Java 应用读取 zip 文件;
用户空间将内容写入到内核空间的缓存中;
Socket 读取内核缓存中的内容,发送出去
零拷贝有多种模式,我们用 sendfile 来举例。如下图所示,在内核的支持下,零拷贝少了一个步骤,那就是内核缓存向用户空间的拷贝,这样既节省了内存,也节省了 CPU 的调度时间,让效率更高
磁盘的速度这么慢,为什么 Kafka 操作磁盘,吞吐量还能那么高?
磁盘之所以慢,主要就是慢在寻道的操作上面。Kafka 官方测试表明,这个寻道时间长达 10ms。磁盘的顺序写和随机写的速度比,可以达到 6 千倍,Kafka 就是采用的顺序写的方式
nmon 便是一个老牌的 Linux 性能监控工具,它不仅有漂亮的监控界面,还能产出细致的监控报表
首先查看Linux系统内核版本(两种方式)
1,cat /proc/version
2,uname -a
接着下载nmon软件包
1、wget方式下载,地址: https://nchc.dl.sourceforge.net/project/nmon/nmon16d_x86.tar.gz
2、官网手动下载,地址:http://nmon.sourceforge.net/pmwiki.php?n=Site.Download
解压安装
下载完成后,可以新建一个目录,作为解压后存放的目录,这里为 nmon16d,如果是手工下载的,需要拷贝到虚拟机
mkdir nmon16d
输入解压命令:tar -zxvf nmon16d_x86.tar.gz -C nmon16d ,-C 是指定解压目录
tar -zxvf nmon16d_x86.tar.gz -C nmon16d
在nmon6d目录中,可以找到nmon_x86_64_centos7这个文件,并对它添加执行权限
chmod +x nmon_x86_64_centos7
启动:./nmon_x86_64_centos7
按 C 键可加入 CPU 面板;按 M 键可加入内存面板;按 N 键可加入网络;按 D 键可加入磁盘等
通过下面的命令,表示每 5 秒采集一次数据,共采集 12 次,它会把这一段时间之内的数据记录下来。比如本次生成了 localhost_202012-21.nmon 这个文件,把它从服务器上下载下来
./nmon_x86_64_centos7 -f -s 5 -c 12 -m /地址/
jvisualvm 原是随着 JDK 发布的一个工具,Java 9 之后开始单独发布。通过它,可以了解应用在运行中的内部情况。我们可以连接本地或者远程的服务器,监控大量的性能数据
通过插件功能,jvisualvm 能获得更强大的扩展。如下图所示,建议把所有的插件下载下来进行体验
要想监控远程的应用,还需要在被监控的 App 上加入 jmx 参数
// 开启 JMX 连接端口 14000
-Dcom.sun.management.jmxremote.port=14000
// 配置不需要 SSL 安全认证方式连接
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false
对于性能优化来说,我们主要用到它的采样器。注意,由于抽样分析过程对程序运行性能有较大的影响,一般我们只在测试环境中使用此功能
对于一个 Java 应用来说,除了要关注它的 CPU 指标,垃圾回收方面也是不容忽视的性能点,我们主要关注以下三点
CPU 分析:统计方法的执行次数和执行耗时,这些数据可用于分析哪个方法执行时间过长,成为热点等
内存分析:可以通过内存监视和内存快照等方式进行分析,进而检测内存泄漏问题,优化内存使用情况
线程分析:可以查看线程的状态变化,以及一些死锁情况
对于我们常用的 HotSpot 来说,有更强大的工具,那就是 JMC。 JMC 集成了一个非常好用的功能:JFR(Java Flight Recorder)
Flight Recorder 源自飞机的黑盒子,是用来录制信息然后事后分析的。在 Java11 中,它可以通过 jcmd 命令进行录制,主要包括 configure、check、start、dump、stop 这五个命令,其执行顺序为,start — dump — stop,例如
jcmd
JFR.start jcmd
JFR.dump filename=recording.jfr jcmd
JFR.stop
JFR 功能是建在 JVM 内部的,不需要额外依赖,可以直接使用,它能够监测大量数据。比如,我们提到的锁竞争、延迟、阻塞等;甚至在 JVM 内部,比如 SafePoint、JIT 编译等,也能去分析
JMC 集成了 JFR 的功能,介绍一下 JMC 的使用
1.概览
2.线程
3.内存
4.代码
5.I/O
6.系统
7.事件
Arthas 是一个 Java 诊断工具,可以排查内存溢出、CPU 飙升、负载高等内容,可以说是一个 jstack、jmap 等命令的大集合
输入dashboard,回车,仪表盘显示当前进程相关信息
wrk(点击进入 GitHub 网站查看)是一款 HTTP 压测工具,和 ab 命令类似,它也是一个命令行工具
Running 30s test @ http://127.0.0.1:8080/index.html
12 threads and 400 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 635.91us 0.89ms 12.92ms 93.69%
Req/Sec 56.20k 8.07k 62.00k 86.54%
22464657 requests in 30.00s, 17.76GB read
Requests/sec: 748868.53
Transfer/sec: 606.33MB
wrk 统计了常见的性能指标,对 Web 服务性能测试非常有用。同时,wrk 支持 Lua 脚本,用来控制 setup、init、delay、request、response 等函数,可以更好地模拟用户请求
nmon 获取系统性能数据;
jvisualvm 获取 JVM 性能数据;
jmc 获取 Java 应用详细性能数据;
arthas 获取单个请求的调用链耗时;
wrk 获取 Web 接口的性能数据
这些工具有偏低层的、有偏应用的、有偏统计的、有偏细节的,在定位性能问题时,你需要灵活地使用这些工具,既从全貌上掌握应用的属性,也从细节上找到性能的瓶颈,对应用性能进行全方位的掌控
但有时候,想要测量某段具体代码的性能情况,这时经常会写一些统计执行时间的代码,这些代码穿插在我们的逻辑中,进行一些简单的计时运算。比如下面这几行:
long start = System.currentTimeMillis();
//logic
long cost = System.currentTimeMillis() - start;
System.out.println("Logic cost : " + cost);
这段代码的统计结果,并不一定准确。举个例子来说,JVM 在执行时,会对一些代码块,或者一些频繁执行的逻辑,进行 JIT 编译和内联优化,在得到一个稳定的测试结果之前,需要先循环上万次进行预热。预热前和预热后的性能差别非常大
JMH(the Java Microbenchmark Harness)就是这样一个能做基准测试的工具,它的测量精度非常高,可达纳秒级别
JMH 已经在 JDK 12中被包含,其他版本的需要自行引入 maven
org.openjdk.jmh
jmh-core
1.23
org.openjdk.jmh
jmh-generator-annprocess
1.23
provided
JMH 是一个 jar 包,它和单元测试框架 JUnit 非常像,可以通过注解进行一些基础配置。这部分配置有很多是可以通过 main 方法的 OptionsBuilder 进行设置的
上图是JMH 程序执行的内容。通过开启多个进程,多个线程,先执行预热,然后执行迭代,最后汇总所有的测试数据进行分析。在执行前后,还可以根据粒度处理一些前置和后置操作
示例
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Thread)
@Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(1)
@Threads(2)
public class BenchmarkTest {
@Benchmark
public long shift() {
long t = 455565655225562L;
long a = 0;
for (int i = 0; i < 1000; i++) {
a = t >> 30;
}
return a;
}
@Benchmark
public long div() {
long t = 455565655225562L;
long a = 0;
for (int i = 0; i < 1000; i++) {
a = t / 1024 / 1024 / 1024;
}
return a;
}
public static void main(String[] args) throws Exception {
Options opts = new OptionsBuilder()
.include(BenchmarkTest.class.getSimpleName())
.resultFormat(ResultFormatType.JSON)
.build();
new Runner(opts).run();
}
}
1. @Warmup
@Warmup(
iterations = 5,
time = 1,
timeUnit = TimeUnit.SECONDS)
我们不止一次提到预热 warmup 这个注解,可以用在类或者方法上,进行预热配置。可以看到,它有几个配置参数
timeUnit:时间的单位,默认的单位是秒;
iterations:预热阶段的迭代数;
time:每次预热的时间;
batchSize:批处理大小,指定了每次操作调用几次方法
上面的注解,意思是对代码预热总计 5 秒(迭代 5 次,每次一秒)。预热过程的测试数据,是不记录测量结果的
执行效果
# Warmup: 3 iterations, 1 s each
# Warmup Iteration 1: 0.281 ops/ns
# Warmup Iteration 2: 0.376 ops/ns
# Warmup Iteration 3: 0.483 ops/ns
一般来说,基准测试都是针对比较小的、执行速度相对较快的代码块,这些代码有很大的可能性被 JIT 编译、内联,所以在编码时保持方法的精简,是一个好的习惯
说到预热,就不得不提一下在分布式环境下的服务预热。在对服务节点进行发布的时候,通常也会有预热过程,逐步放量到相应的服务节点,直到服务达到最优状态。如下图所示,负载均衡负责这个放量过程,一般是根据百分比进行放量
2. @Measurement
@Measurement(
iterations = 5,
time = 1,
timeUnit = TimeUnit.SECONDS)
Measurement 和 Warmup 的参数是一样的,不同于预热,它指的是真正的迭代次数
我们能够从日志中看到这个执行过程
# Measurement: 5 iterations, 1 s each
Iteration 1: 1646.000 ns/op
Iteration 2: 1243.000 ns/op
Iteration 3: 1273.000 ns/op
Iteration 4: 1395.000 ns/op
Iteration 5: 1423.000 ns/op
虽然经过预热之后,代码都能表现出它的最优状态,但一般和实际应用场景还是有些出入。如果你的测试机器性能很高,或者你的测试机资源利用已经达到了极限,都会影响测试结果的数值
所以,通常情况下,都会在测试时,给机器充足的资源,保持一个稳定的环境。在分析结果时,也会更加关注不同代码实现方式下的性能差异 ,而不是测试数据本身
3. @BenchmarkMode
此注解用来指定基准测试类型,对应 Mode 选项,用来修饰类和方法都可以。这里的 value,是一个数组,可以配置多个统计维度。比如
@BenchmarkMode({Throughput,Mode.AverageTime}),统计的就是吞吐量和平均执行时间两个指标
所谓的模式,其实就是我们说的一些指标,在 JMH 中,可以分为以下几种
Throughput: 整体吞吐量,比如 QPS,单位时间内的调用量等;
AverageTime: 平均耗时,指的是每次执行的平均时间。如果这个值很小不好辨认,可以把统计的单位时间调小一点;
SampleTime: 随机取样,这和 TP 值是一个概念;
SingleShotTime: 如果你想要测试仅仅一次的性能,比如第一次初始化花了多长时间,就可以使用这个参数,其实和传统的 main 方法没有什么区别;
All: 所有的指标,都算一遍,你可以设置成这个参数看下效果
拿平均时间,看一下一个大体的执行结果
Result "com.github.xjjdog.tuning.BenchmarkTest.shift":
2.068 ±(99.9%) 0.038 ns/op [Average]
(min, avg, max) = (2.059, 2.068, 2.083), stdev = 0.010
CI (99.9%): [2.030, 2.106] (assumes normal distribution)
由于我们声明的时间单位是纳秒,本次 shift 方法的平均响应时间就是 2.068 纳秒
我们也可以看下最终的耗时时间
Benchmark Mode Cnt Score Error Units
BenchmarkTest.div avgt 5 2.072 ± 0.053 ns/op
BenchmarkTest.shift avgt 5 2.068 ± 0.038 ns/op
由于是平均数,这里的 Error 值的是误差(或者波动)的意思
可以看到,在衡量这些指标的时候,都有一个时间维度,它就是通过 @OutputTimeUnit 注解进行配置的,这个就比较简单了,它指明了基准测试结果的时间类型。可用于类或者方法上,一般选择秒、毫秒、微秒,纳秒那是针对的速度非常快的方法
举个例子,@BenchmarkMode(Mode.Throughput) 和 @OutputTimeUnit(TimeUnit.MILLISECONDS) 进行组合,代表的就是每毫秒的吞吐量
如下面的关于吞吐量的结果,就是以毫秒计算的:
Benchmark Mode Cnt Score Error Units
BenchmarkTest.div thrpt 5 482999.685 ± 6415.832 ops/ms
BenchmarkTest.shift thrpt 5 480599.263 ± 20752.609 ops/ms
OutputTimeUnit 注解同样可以修饰类或者方法,通过更改时间级别,可以获取更加易读的结果
4. @Fork
fork 的值一般设置成 1,表示只使用一个进程进行测试;如果这个数字大于 1,表示会启用新的进程进行测试;但如果设置成 0,程序依然会运行,不过是这样是在用户的 JVM 进程上运行的,可以看下下面的提示,但不推荐这么做
# Fork: N/A, test runs in the host VM
# *** WARNING: Non-forked runs may silently omit JVM options, mess up profilers, disable compiler hints, etc. ***
# *** WARNING: Use non-forked runs only for debugging purposes, not for actual performance runs. ***
那么 fork 到底是在进程还是线程环境里运行呢?
通过阅读 JMH 的源码,发现每个 fork 进程是单独运行在 Proccess 进程里的,这样就可以做完全的环境隔离,避免交叉影响
它的输入输出流,通过 Socket 连接的模式,发送到我们的执行终端
其实 fork 注解有一个参数叫作 jvmArgsAppend,我们可以通过它传递一些 JVM 的参数
@Fork(value = 3, jvmArgsAppend = {"-Xmx2048m", "-server", "-XX:+AggressiveOpts"})
在平常的测试中,也可以适当增加 fork 数,来减少测试的误差
5. @Threads
fork 是面向进程的,而 Threads 是面向线程的。指定了这个注解以后,将会开启并行测试。如果配置了 Threads.MAX,则使用和处理机器核数相同的线程数
这个和我们平常编码中的习惯也是相同的,并不是说开的线程越多越好。线程多了,操作系统就需要耗费更多的时间在上下文切换上,造成了整体性能的下降
6. @Group
@Group 注解只能加在方法上,用来把测试方法进行归类。如果你单个测试文件中方法比较多,或者需要将其归类,则可以使用这个注解
与之关联的 @GroupThreads 注解,会在这个归类的基础上,再进行一些线程方面的设置。这两个注解都很少使用,除非是非常大的性能测试案例
7. @State
@State 指定了在类中变量的作用范围,用于声明某个类是一个“状态”,可以用 Scope 参数用来表示该状态的共享范围。这个注解必须加在类上,否则提示无法运行
Scope 有如下三种值
Benchmark :表示变量的作用范围是某个基准测试类
Thread :每个线程一份副本,如果配置了 Threads 注解,则每个 Thread 都拥有一份变量,它们互不影响
Group :联系上面的 @Group 注解,在同一个 Group 里,将会共享同一个变量实例
在 JMHSample_DefaultState 测试代码中,演示了变量 x 的默认作用范围是 Thread,关键代码如下
@State(Scope.Thread)
public class JMHSample_DefaultState {
double x = Math.PI;
@Benchmark
public void measure() {
x++;
}
}
8. @Setup 和 @TearDown
和单元测试框架 JUnit 类似,@Setup 用于基准测试前的初始化动作,@TearDown 用于基准测试后的动作,来做一些全局的配置
这两个注解,同样有一个 Level 值,标明了方法运行的时机,它有三个取值
Trial :默认的级别,也就是 Benchmark 级别
Iteration :每次迭代都会运行
Invocation :每次方法调用都会运行,这个是粒度最细的
如果你的初始化操作,是和方法相关的,那最好使用 Invocation 级别。但大多数场景是一些全局的资源,比如一个 Spring 的 DAO,那么就使用默认的 Trial,只初始化一次就可以
9. @Param
@Param 注解只能修饰字段,用来测试不同的参数,对程序性能的影响。配合 @State 注解,可以同时制定这些参数的执行范围
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(1)
@State(Scope.Benchmark)
public class JMHSample_Params {
@Param({"1", "31", "65", "101", "103"})
public int arg;
@Param({"0", "1", "2", "4", "8", "16", "32"})
public int certainty;
@Benchmark
public boolean bench() {
return BigInteger.valueOf(arg).isProbablePrime(certainty);
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(JMHSample_Params.class.getSimpleName())
// .param("arg", "41", "42") // Use this to selectively constrain/override parameters
.build();
new Runner(opt).run();
}
}
值得注意的是,如果你设置了非常多的参数,这些参数将执行多次,通常会运行很长时间。比如参数 1 M 个,参数 2 N 个,那么总共要执行 M*N 次
10. @CompilerControl
Java 中方法调用的开销是比较大的,尤其是在调用量非常大的情况下。拿简单的getter/setter 方法来说,这种方法在 Java 代码中大量存在。我们在访问的时候,就需要创建相应的栈帧,访问到需要的字段后,再弹出栈帧,恢复原程序的执行
如果能够把这些对象的访问和操作,纳入目标方法的调用范围之内,就少了一次方法调用,速度就能得到提升,这就是方法内联的概念。如下图所示,代码经过 JIT 编译之后,效率会有大的提升
这个注解可以用在类或者方法上,能够控制方法的编译行为,常用的有 3 种模式:
强制使用内联(INLINE),禁止使用内联(DONT_INLINE),甚至是禁止方法编译(EXCLUDE)等
使用 JMH 测试的结果,可以二次加工,进行图形化展示。结合图表数据,更加直观。通过运行时,指定输出的格式文件,即可获得相应格式的性能测试结果
比如下面这行代码,就是指定输出 JSON 格式的数据
Options opt = new OptionsBuilder()
.resultFormat(ResultFormatType.JSON)
.build();
1. JMH 支持 5 种格式结果
TEXT 导出文本文件
CSV 导出 csv 格式文件
SCSV 导出 scsv 等格式的文件
JSON 导出成 json 文件
LATEX 导出到 latex,一种基于 ΤΕΧ 的排版系统
一般来说,我们导出成 CSV 文件,直接在 Excel 中操作,生成如下相应的图形就可以了
2. 结果图形化制图工具
这里有一个开源的项目,通过导出 json 文件,上传至 JMH Visualizer(点击链接跳转),可得到简单的统计结果。由于很多操作需要鼠标悬浮在上面进行操作
相比较而言, JMH Visual Chart 这个工具,就相对直观一些
一个通用的 在线图表生成器,导出 CSV 文件后,做适当处理,即可导出精美图像
JMH 这个工具非常好用,它可以使用确切的测试数据,来支持我们的分析结果。一般情况下,如果定位到热点代码,就需要使用基准测试工具进行专项优化,直到性能有了显著的提升