性能测试方法-性能测试4原则

原则1:测试真实应用

性能测试应当在真实的使用环境中进行。性能测试大体分为3中:

  • 微基准测试
  • 宏基准测试
  • 介基准测试

微基准测试

看如下代码,试图使用微基准测试方法计算出第50个斐波那契数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void doTest() { 
//主循环
double l;
long then = System.currentTimeMillis();
for (int i = 0; i < nLoops; i++) {
l = fibImpl1(50);
}
long now = System.currentTimeMillis();
System.out.println("Elapsed time: " + (now - then));
}

private double fibImpl1(int n) {
if (n < 0) throw new IllegalArgumentException("Must be > 0");
if (n == 0) return 0d;
if (n == 1) return 1d;
double d = fibImpl1(n - 2) + fibImpl(n - 1);
if (Double.isInfinite(d)) throw new ArithmeticException("Overflow");
return d;
}

该代码存在问题:

  • 未使用斐波那契数结果,编译器可能会讲循环去除。最终真正执行代码:
1
2
3
4
5
//主循环 
double l;
long then = System.currentTimeMillis();
long now = System.currentTimeMillis();
System.out.println("Elapsed time: " + (now - then));

可通过将变量L定义为实例变量(并用volatile声明)就可以测试;如果涉及到多线程还需要考虑资源竞争等导致的性能消耗,而不仅仅是程序代码导致的性能消耗。

  • 不要包含无关的操作。如果编译器足够智能,就会发现上面有些迭代时不必要的,从而减少迭代次数。
  • 必须输入合理的参数。在上面的代码中,如果输入参数超过合理的范围,需要思考对性能的影响是否时合理的。

宏基准测试

整个系统的处理流程是复杂的,如果单纯的对某个模块进行测试而缺少关注全局,会导致忽略一些重要的问题。比如:影响系统吞吐量的不仅仅是JVM,还有数据库、网络等各方面的原因。宏基准测试就是站在一个更大的角度整体的测试整个系统的性能。

介基准测试

介基准测试相对于红基准测试是一种折中方案,在模块级别或者操作级别隔离性能来进行测试。它表示一些应用的实际操作,但不是完整应用的基准测试。

原则2:理解批处理流逝时间、吞吐量和响应时间

批处理流逝时间

批处理流逝时间主要关注执行完成任务花费的时间。但在Java世界中,需要特变关注代码优化的热身期:对于即时编译,虚拟机会花费一段时间优化代码以最高的性能去执行。初次之外,还有注入读取数据库时会应用缓存。

吞吐量

吞吐量表示一段时间内完成的工作量。在客户端-服务器的吞吐量测试中,吞吐量并不考虑客户端的处理时间。吞吐量仅表示在客户端发送请求到接受响应并持续发送请求的一个过程,在一定时间内,客户端请求响应的总数。通常时每秒完成的操作量。这个指标也被称作每秒事务数(TPS)、每秒请求数(RPS)、每秒操作数(OPS)。

需要注意:吞吐量测试通常在程序的热身其之后进行。

响应时间

响应时间表示从客户端发送请求到收到响应之间的流逝时间。

原则3:用统计方法来应对性能变化

性能测试会随着时间的变化而产生不同的结果,因为有很多因素会影响程序运行:比如后台进程、网络拥堵等。这种随机性问题会导致测试不准确,对此可以对结果求平均来解决这个问题。

原则4:尽早频繁测试

性能测试应该作为开发周期不可或缺的一部分。

Java性能调优工具箱

操作系统的工具和分析

Unix:vmstat、iostat、prstat
Windows:typeperf

CPU使用率

CPU使用率分为两类:

  • 用户态时间:CPU执行应用代码所占时间的百分比
  • 系统态时间:CPU执行内核代码所占时间的百分比

系统态时间与应用相关,比如IO操作,系统会执行内核代码从磁盘读取文件,或者将缓冲数据发送到网络。

性能调优的目的是尽可能的提高CPU的使用率。如果我们应用代码不能尽可能的使CPU的使用率达到100%,考虑一下原因:

  • 应用被同步原语阻塞,直至锁释放才能继续执行
  • 应用再等待某些东西,例如数据库调用所返回的响应
  • 应用的确无所事事

在linux系统,我们可以通过 vmstat 命令查看CPU使用率

1
2
3
4
5
6
7
8
9
10
#每秒统计一次
> vmstat 1

#执行结果如下:
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
1 0 0 6318336 7356 32224 0 0 1 202 1 4 0 0 100 0 0
0 0 0 6318336 7356 32224 0 0 0 0 5 26 0 0 100 0 0
0 0 0 6318336 7356 32224 0 0 0 0 5 28 0 0 100 0 0
0 0 0 6318336 7356 32224 0 0 0 0 5 29 0 0 100 0 0

字段说明如下:

procs(进程信息字段):

  • r:等待运行的进程数,数量越大,系统越繁忙。
  • b:不可被唤醒的进程数量,数量越大,系统越繁忙。

memory(内存信息字段):

  • swpd:虚拟内存的使用情况,单位为 KB。
  • free:空闲的内存容量,单位为 KB。
  • buff:缓冲的内存容量,单位为 KB。
  • cache:缓存的内存容量,单位为 KB。

swap(交换分区信息字段):

  • si:从磁盘中交换到内存中数据的数量,单位为 KB。
  • so:从内存中交换到磁盘中数据的数量,单位为 KB。

这两个数越大,表明数据需要经常在磁盘和内存之间进行交换,系统性能越差。

io(磁盘读、写信息字段):

  • bi:从块设备读入的数据的总量,单位是块。
  • bo:写到块设备的数据的总量,单位是块。

这两个数越大,代表系统的 I/O 越繁忙。

system(系统信息字段):

  • in:每秒被中断的进程次数。
  • cs:每秒进行的事件切换次数。

这两个数越大,代表系统与接口设备的通信越繁忙。

cpu(CPU信息字段):

  • us:非内核进程消耗 CPU 运算时间的百分比。
  • sy:内核进程消耗 CPU 运算时间的百分比。
  • id:空闲 CPU 的百分比。
  • wa:等待 I/O 所消耗的 CPU 百分比。
  • st:被虚拟机所盗用的 CPU 百分比。

CPU运行队列

Windows和Unix系统都可以监控可运行(意味着没有被I/O阻塞、休眠)的线程数,成为运行队列。Unix系统可通过 vmstat 的 procs字段的 r来查看可运行的线程数。

磁盘使用率

监控磁盘的两个目的:

  1. 如果应用正在做大量的磁盘I/O操作,那么很容易成为瓶颈。
  2. 有助于监控系统是否在进行内存交换。

网络使用率

网络使用率类似磁盘使用率:应用可能没有充分利用网络所有带宽很低,或者写入某网络接口的总数据量超过了它所能处理的量。

Unix系统可以使用 netstat 或者nicstat 来查看网络使用率。

Java监控工具

JDK自带工具列表:

工具命令 描述
jcmd 用来打印Java进程所涉及的基本类、线程、VM信息。
jconsole 提供JVM活动的图形化视图,包括线程的使用、类的使用和GC活动。
jhat 读取内存堆转储,帮助分析。事后使用的工具。
jmap 提供堆转储和其他JVM内存使用信息。
jinfo 查看JVM系统属性,可以动态设置一些系统属性。
jstack 转储Java进程的栈信息。
jstat 提供GC和类装载活动的信息。
jvisualvm 监视JVM的GUI工具,可用来剖析运行的应用,分析JVM堆转储。

这些工具广泛用于一下领域:

  • 基本的VM信息
  • 线程信息
  • 类信息
  • 实时GC分析
  • 堆转储的事后处理
  • JVM性能分析

基本的VM信息

通过上面命令可以或许VM的基本信息:运行时间、使用了哪些JVM标识、JVM系统属性。

运行时间:

1
> jcmd process_id VM.uptime

系统属性(System.getProperties()里的配置):

1
2
3
> jcmd process_id VM.system_properties
## 或者
> jinfo -sysprops process_id

JVM版本:

1
> jcmd process_id VM.version

JVM命令行:

1
2
> jcmd process_id VM.command_line

JVM调优标识:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
## 带上all时,会列出所有调优标识
> jcmd process_id VM.flags [-all]
## 该命令会输出如下标识,最后一列值含义如下
## product:表示所有平台上的默认值都是一致的
## pd product:表示默认值是独立于平台的
## manageable:表示运行时可以动态修改标志的值
## C2 diagnostic:为编译工程师提供诊断输出,帮助理解编译器正以什么方式运行

uintx CMSAbortablePrecleanMinWorkPerIteration = 100 {product}
intx CMSAbortablePrecleanWaitMillis = 100 {manageable}
uintx CMSYoungGenPerWorker = 67108864 {pd product}
intx ValueMapMaxLoopSize = 8 {C1 product}
intx ValueSearchLimit = 1000 {C2 product}
bool UseUnalignedLoadStores = true {ARCH product}
bool UseCompressedOops := true {lp64_product}
bool TraceClassLoading = false {product rw}

## 对于manageable的值,我们可以通过jinfo来开启和关闭,也可以用jinfo来检查单个标识的值
> jinfo -flag PrintGCDetails process_id
## 关闭 PrintGCDetails
> jinfo -flag -PrintGCDetails process_id

线程信息

可以通过 jconsole 和 jvisualvm 直接以GUI的形式查看线程信息。也可以通过以下命令查看线程的栈信息(这个对判断线程是否阻塞很有用):

  • jstack process_id
  • jcmd process_id Thread.print

类信息

可以通过 jconsole 或 jstat 查看应用使用类的个数。 jstat 还提供类编译相关的信息。

实时GC分析

几乎所有的监控工具都能报告一些GC的活动信息。
jconsole 可以以实时图显示堆的使用情况;
jcmd 可以执行GC操作;
jmap 可以打印堆的概况、永久代信息或者创建堆的转储;
jstat 可以为垃圾收集器正在执行的操作生成许多试图;

事后堆转储

堆转储是堆使用情况的快照。jvisiualvm 可以捕获堆转储。也可以使用 jcmd 和 jmap 生成堆转储。
分析堆转储可以使用 jvisiualvm 或者 jhat。也可以使用更为领先的第三方工具-Eclipse Memory
Analyzer Tool。

性能分析工具

Java的众多性能分析工具各有优缺点,需要综合参考使用;
性能分析器开启与应用之间的socket,随后目标应用与性能分析工具交换应用的行为信息。这需要我们注意性能分析工具自身性能的调优。

采样分析器

性能分析有两种模式:数据采样和数据探查。

数据采样时最常用的分析器。由于采样分析器采样频率相对较低,所以失真也较小。

探查分析器可以给出更多的应用信息,比如方法的调用次数,但同样侵入性更强,对应用的影响也更大。
探查分析器应该仅在小代码区域(一些类或者包)中设置使用,以限制对应用的影响。

阻塞方法和线程时间线

NetBean 性能分析器可以报告被阻塞方法和花费时间。可以通过分析阻塞原因来整体提升系统的性能。

Oracle Solaris Studio性能分析工具可以呈现线程的时间线,通过时间线分析可以辨认出阻塞的线程。

本地分析器

本地性能分析工具是指 JVM 自身性能工具,它可以提供JVM和应用代码内部的信息。

Oracle Solaris Studio分析器是一个本地分析器

如果本地分析器显示GC占用了主要的CPU时间,优化垃圾收集器是正确的做法。然而如果显示编译器线程占用了CPU主要时间,则说明通常对性能没什么影响。

Java任务控制

商业版Java提供的JMC(Java Mission Controller)可以监控CPU使用率、堆使用量等信息。

JMC的关键特性就是Java飞行记录器JFR(Java Flight Recorder)。

垃圾收集

概述

分代垃圾收集器

垃圾收集器根据情况将堆划分为不同的区:

  • 老年代(Old Generation)
  • 新生代(Young Generation)
    • Eden
    • Survivor

采用分代机制的原因在于很多对象的生存时间非常短,所有新生的对象都在新生代中,进行垃圾回收时,相较于在整个堆空间进行,在新生代空间进行有两个优势:

1.处理速度更快;
2.在移动对象的时候,比如从Eden移动到Survivor时,可以进行压缩整理;

一般垃圾收集器在进行垃圾收集时,会禁止线程访问对象,赞成时空停顿(stop-the-world)。当新生代填满时,回收新生代空间和不再使用的对象,这称为Minor GC。当对象不断的移动到老年代,导致老年代空间被填满时,垃圾收集器会堆整个堆进行内存回收,这个过程称为FUll GC

通过复杂的计划,有些垃圾收集器可以尽可能做到最小的停顿。比如:CMS 和 C1收集器就被称为低停顿(Low-Pause)收集器。但也由此付出了更多的CPU计算量。所以我们在考虑选用哪种垃圾收集器时,可以根据对应用停顿时间要求和容忍度来进行选择。

GC算法

JVM提供了4种垃圾收集算法:

  1. Serial垃圾收集器

    • 使用单线程清理堆内容。
    • Minor GC和Full GC时所有应用程序会暂停。
    • -XX:+UseSerialGC可以启用Serial收集器。关闭需要指定其他垃圾收集器。
  2. Throughput垃圾收集器

    • 使用多线程清理新生代、老年代空间,JDK 7u4之后默认开启老年代清理使用u多线程。
    • Minor GC和Full GC时会暂停所有应用线程。-XX:+UseParallelGC 、-XX:+UseParallelOldGC 标志启用Throughput垃圾收集器。
  3. CMS收集器

    • Minor GC会暂停所有的应用程序;Full GC 不会暂停应用线程,会使用若干个后台线程定期的堆老年代空间进行扫描,及时回收空间。
    • 更高的CPU消耗。
    • 不会对堆空间进行压缩整理。如果堆变得过度碎片化导致无法找到连续的空间分配对象或无法获得完成任务所需大的CPU资源则会蜕化成Serial收集器行为:暂停所有应用线程,使用单线程回收、整理老年代空间。
    • 通过-XX:UseConcMarkSweepGC、-XX:UseParNewGC标志启用(默认两个都是禁用)
  4. G1垃圾收集器

    • 适用于处理超大堆空间(大于4G)。
    • 新生代区域暂停所有应用线程,使用多线程。
    • 老年代被划分为多个区域,会对空间进行整理。
    • Full GC会消耗额外的CPU周期
    • 通过-XX:+UseG1GC 可以使用G1垃圾收集器(默认关闭)

GC算法选择

  • Serial 收集器适用于应用程序内存使用少于 100MB 的场景;
  • Throughput 收集器和 CMS 收集器之间选择依据可用的空闲的CPU资源。通常情况下,Throughput 的平均响应时间要比 Concurrent 收集器差些;
  • CMS 收集器和 G1 收集器选择:一般情况下,堆空间小于4G,CMS收集器性能比G1收集器好。

GC 调优基础

调整堆的大小

对于堆大小,并非是越大越好,更大的堆空间意味着更大的停顿时间,而且如果堆空间设置超过系统物理内存空间,会导致操作系统使用虚拟内存空间,进一步降低GC的性能。

设置堆的大小应考虑的因素:物理硬件内存大小、系统运行本身需要的内存大小、以及JVM自身及其他应用程序的预留空间;

Linux 64位 堆默认配置:

  • Xms: Min(512MB,物理内存/64)
  • Xmx: Min(32GB,物理内存/4)

针对需要调整堆大小的一个经验法则:Full GC 之后应该释放出30%的空间。可以通过 jconsole 连接应用强制 GC 来观察,或者 GC 日志。

代空间调整

年轻代空间设置参数如下:

1
2
3
4
-XX:NewRatio=N 设置新生代和老年代空间占用比率,默认值2
-XX:NewSize=N 设置新生代空间初始大小,优先级高于 NewRatio
-XX:MaxNewSize=N 设置新生代空间最大大小
-XmnN 将NewSize和MaxNewSize设置为一样大小

根据NewRation我们可以计算初始年轻代空间大小:

初始年轻代空间大小 = 初始堆空间大小 / ( 1 + NewRation )

永久代/元空间的默认大小

64位JVM:初始大小-20.75MB;默认永久代最大值-82MB;默认元空间最大值:没有限制;

-XX:PermSize=N 设置初始永久代空间大小;
-XX:MaxPermSize=N 设置最大永久代空间大小;

-XX:MetaspaceSize=N 设置初始元空间大小
-XX:MaxMetaspaceSize=N 设置最大元空间大小

并发控制

除 Serial 收集器外,其他几乎都是使用多线程来进行垃圾回收的。启动的线程数有 -XX:ParallelGCThreads=N 参数控制。针对下面操作,会影响线程数目:

  • -XX:+UseParallelGC 收集新生代空间
  • -XX:+UseParallelOldGC 收集老年代空间
  • -XX:+UseParNewGC 收集新生代空间
  • -XX:+UseG1GC 收集新生代空间
  • CMS收集器的“时空停顿”阶段
  • G1收集器的“时空停顿”阶段

当N<8时,默认时每个CPU启动一个线程,N > 8 默认启动线程数算法如下:

ParallelGCThreads = 8 + ((N-8) * 5/8 )

其中N代表CPU数。

注意:当机器上同时运行多个JVM时,这时最好限制所有JVM的线程数。针对核心数非常多的机器,运行一个JVM时,也需要适当的调整改参数。