Java 性能优化实战——工具介绍

哪些资源,容易成为瓶颈?

计算机各个组件之间的速度往往很不均衡,比如 CPU 和硬盘,比兔子和乌龟的速度差还大,那么按照木桶理论,可以说这个系统是存在着短板的。当系统存在短板时,就会对性能造成较大的负面影响,比如当 CPU 的负载特别高时,任务就会排队,不能及时执行。而其中,CPU、内存、I/O 这三个系统组件,又往往容易成为瓶颈

CPU

首先是计算机中最重要的计算组件中央处理器 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

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 等,就使用了这种技术。来看一下有无零拷贝之间的区别

  • (1)没有采取零拷贝手段

如下图所示,传统方式中要想将一个文件的内容通过 Socket 发送出去,则需要经过以下步骤

  • 将文件内容拷贝到内核空间;

  • 将内核空间内存的内容,拷贝到用户空间内存,比如 Java 应用读取 zip 文件;

  • 用户空间将内容写入到内核空间的缓存中;

  • Socket 读取内核缓存中的内容,发送出去

  • (2)采取了零拷贝手段

零拷贝有多种模式,我们用 sendfile 来举例。如下图所示,在内核的支持下,零拷贝少了一个步骤,那就是内核缓存向用户空间的拷贝,这样既节省了内存,也节省了 CPU 的调度时间,让效率更高

磁盘的速度这么慢,为什么 Kafka 操作磁盘,吞吐量还能那么高?

磁盘之所以慢,主要就是慢在寻道的操作上面。Kafka 官方测试表明,这个寻道时间长达 10ms。磁盘的顺序写和随机写的速度比,可以达到 6 千倍,Kafka 就是采用的顺序写的方式

 

如何获取代码性能数据?

nmon —— 获取系统性能数据

nmon 便是一个老牌的 Linux 性能监控工具,它不仅有漂亮的监控界面,还能产出细致的监控报表

Nmon下载安装

首先查看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 —— 获取 JVM 性能数据

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 分析:统计方法的执行次数和执行耗时,这些数据可用于分析哪个方法执行时间过长,成为热点等

  • 内存分析:可以通过内存监视和内存快照等方式进行分析,进而检测内存泄漏问题,优化内存使用情况

  • 线程分析:可以查看线程的状态变化,以及一些死锁情况

JMC —— 获取 Java 应用详细性能数据

对于我们常用的 HotSpot 来说,有更强大的工具,那就是 JMC。 JMC 集成了一个非常好用的功能:JFR(Java Flight Recorder)

Flight Recorder 源自飞机的黑盒子,是用来录制信息然后事后分析的。在 Java11 中,它可以通过 jcmd 命令进行录制,主要包括 configure、check、start、dump、stop 这五个命令,其执行顺序为,start — dump — stop,例如

jcmd <pid> JFR.start

jcmd <pid> JFR.dump filename=recording.jfr

jcmd <pid> JFR.stop

JFR 功能是建在 JVM 内部的,不需要额外依赖,可以直接使用,它能够监测大量数据。比如,我们提到的锁竞争、延迟、阻塞等;甚至在 JVM 内部,比如 SafePoint、JIT 编译等,也能去分析

JMC 集成了 JFR 的功能,介绍一下 JMC 的使用

1.概览

2.线程

3.内存

4.代码

5.I/O

6.系统

7.事件

 

Arthas —— 获取单个请求的调用链耗时

Arthas 是一个 Java 诊断工具,可以排查内存溢出、CPU 飙升、负载高等内容,可以说是一个 jstack、jmap 等命令的大集合

输入dashboard,回车,仪表盘显示当前进程相关信息

wrk —— 获取 Web 接口的性能数据

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 接口的性能数据

这些工具有偏低层的、有偏应用的、有偏统计的、有偏细节的,在定位性能问题时,你需要灵活地使用这些工具,既从全貌上掌握应用的属性,也从细节上找到性能的瓶颈,对应用性能进行全方位的掌控

 

基准测试 JMH,精确测量方法性能

但有时候,想要测量某段具体代码的性能情况,这时经常会写一些统计执行时间的代码,这些代码穿插在我们的逻辑中,进行一些简单的计时运算。比如下面这几行:

long start = System.currentTimeMillis(); 

//logic 

long cost = System.currentTimeMillis() - start; 

System.out.println("Logic cost : " + cost);

这段代码的统计结果,并不一定准确。举个例子来说,JVM 在执行时,会对一些代码块,或者一些频繁执行的逻辑,进行 JIT 编译和内联优化,在得到一个稳定的测试结果之前,需要先循环上万次进行预热。预热前和预热后的性能差别非常大

 

JMH—基准测试工具

JMH(the Java Microbenchmark Harness)就是这样一个能做基准测试的工具,它的测量精度非常高,可达纳秒级别

JMH 已经在 JDK 12中被包含,其他版本的需要自行引入 maven

<dependencies> 
        <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> 
            <scope>provided</scope> 
        </dependency> 
</dependencies>

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. 结果图形化制图工具

  • JMH Visualizer

这里有一个开源的项目,通过导出 json 文件,上传至 JMH Visualizer(点击链接跳转),可得到简单的统计结果。由于很多操作需要鼠标悬浮在上面进行操作

  • JMH Visual Chart

相比较而言, JMH Visual Chart 这个工具,就相对直观一些

  • meta-chart

一个通用的 在线图表生成器,导出 CSV 文件后,做适当处理,即可导出精美图像

JMH 这个工具非常好用,它可以使用确切的测试数据,来支持我们的分析结果。一般情况下,如果定位到热点代码,就需要使用基准测试工具进行专项优化,直到性能有了显著的提升

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值