Java性能调优03——Java性能调优工具箱(上):操作系统的工具和分析

本系列均取自《Java性能权威指南》这里只是一个个人备忘笔录

性能分析过程中的一切都要能可视化,从而了解应用内部及应用所在的环境发生了什么。可视化的关键全在于工具,所以性能调优也完全在于工具。

有许多工具可以提供 Java 应用的执行信息,当然全部介绍一遍是不现实的。最重要的工具多数都来自JDK或者开源站点。虽然还有其他开源和商业工具,但为方便起见,本章关注的主要是 JDK 所提供的工具。

操作系统的工具和分析

实际上性能分析的起点与 Java 无关:它是一组操作系统自带的基本监控工具。在基于 Unix 的系统上,有 sar(System Accounting Report)及其组成工具,例如 vmstat、iostat、prstat 等。在 Windows 上,有图形化资源监视器以及像 typeperf 这样的命令行工具。

无论何时运行性能测试,都应该收集操作系统的数据,至少需要收集 CPU、内存和磁盘使用率的信息。如果程序使用网络,还应该收集网络使用率。如果是自动化性能测试,还需要依靠命令行工具(即使是 Windows 系统)。不过,即便可以通过交互方式进行测试,也最好用命令行工具捕获输出,而不是一边盯着 GUI,一边琢磨它的意思。在分析的时候可以再次将这些输出图形化。

CPU使用率

通常 CPU 使用率可以分为两类:用户态时间系统态时间(Windows 上被称作 privileged time)。用户态时间是 CPU 执行应用代码所占时间的百分比,而系统态时间则是 CPU 执行内核代码所占时间的百分比。系统态时间与应用相关,比如应用执行 I/O 操作,系统就会执行内核代码从磁盘读取文件,或者将缓冲数据发送到网络,等等。任何使用底层系统资源的操作,都会导致应用占用更多的系统态时间。

性能调优的目的是,在尽可能短的时间内让 CPU 使用率尽可能地高。这听起来有点不合常理。或许你此时正坐在电脑旁,看着它拼命挣扎,因为 CPU 使用率已经是 100% 了。好,我们先来考虑一下,CPU 使用率到底反映了什么。

首先需要注意的是,CPU 使用率是一段时间内的平均数——5 秒、30 秒,也可能只有 1 秒那么短(不过永远不会比这还要短)。比如,10 分钟内一个程序执行的 CPU 使用率为 50%。如果代码调优之后,CPU 使用率达到了 100%,说明程序的性能翻了倍:程序只需要执行 5 分钟就可以了。如果性能再翻倍,CPU 仍将是 100%,而执行完程序只要 2.5 分钟。CPU 使用率表示程序以多高的效率使用 CPU,所以数字越大,性能越好。

如果在 Linux 桌面系统上运行 vmstat 1(每隔一秒显示一行),可以得到类似如下的几行信息:

procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa
 2  0      0 1797836 1229068 1508276 0    0     0     9 2250 3634 41  3 55  0
 2  0      0 1801772 1229076 1508284 0    0     0     8 2304 3683 43  3 54  0
 1  0      0 1813552 1229084 1508284 0    0     3    22 2354 3896 42  3 55  0
 1  0      0 1819628 1229092 1508292 0    0     0    84 2418 3998 43  2 55  0

为了便于说明问题,这个运行示例程序只有一个活跃线程。不过即便有多个线程,也可以应用如下概念。

每秒内,CPU 被占用 450 毫秒(1秒内有42% 的时间执行us用户代码,3% 的时间执行sy系统代码)。相应地,CPU 空闲 550 毫秒。CPU 空闲可能有以下原因。

1.应用被同步原语阻塞,直至锁释放才能继续执行。

2.应用在等待某些东西,例如数据库调用所返回的响应。

3.应用的确是无所事事。

前面 2 种情况通常都可用来识别某些问题。如果竞争降低,或优化数据库使之发送响应更快,程序运行都能变得更快,平均 CPU 使用率也会上升(当然,得假定没有其他继续阻塞应用的问题)。

第 3 点则常常使人疑惑。如果应用有事情做(而不是因为等待锁或者其他资源而无事可干),CPU 就会分配一些周期执行应用代码。这是一般性原则,并不只针对 Java。比如,包含无限循环的简单脚本。这段脚本执行时,将消耗 100% 的 CPU。以下的 Windows 批处理任务就是这么干的:

ECHO OFF
:BEGIN
ECHO LOOPING
GOTO BEGIN
REM We never get here…
ECHO DONE

考虑一下,如果这段脚本没有消耗 100%CPU,那意味着什么。意味着,操作系统还有些事可做——它可以打印一行 LOOPING——却选择了空闲。这种情况下,空闲并没有什么好处,如果我们正在进行一些有用(耗时)的计算,那么迫使 CPU 周期性空闲只会使我们得到响应的时间变得更长。

如果在单 CPU 机器上运行上述脚本,多数时候你不会注意到它的运行。不过一旦开启新程序,或者测量其他程序的运行时间,你就能看到影响了。操作系统擅长为争用 CPU 周期的程序分配时间片,但新程序可用的 CPU 变少了,它也就运行得更慢。所以基于这种经验,人们有时会认为,在其他程序可能需要 CPU 周期时预留一些空闲周期,没准是个好主意。

但操作系统无法猜到你接下来想做什么,所以(默认情况下)它会尽可能执行一切而不是让 CPU 空闲。

限制程序所用的 CPU

尽可能利用 CPU 周期运行程序可以使程序性能最大化。不过有时你并不希望如此。比如,你运行 SETI@home1(SETI@home 是一项利用全球联网的计算机共同搜寻地外文明(SETI)的科学实验计划),它将消耗你机器所有可用的 CPU 周期。这在你不干活的时候没事,上网或者写文档的时候也没事,否则就会降低你的生产率。(不妨考虑一下如果你正在玩 CPU 密集型游戏,这样做会发生什么。)

操作系统有许多机制可用来人为限定程序所使用的 CPU——事实上,如果有程序需要使用 CPU,它就会退出空闲周期。进程的优先级也可以改变,所以那些后台任务既不会与你想运行的程序争用 CPU,也不会让 CPU 处于空闲状态。这些技术超出了我们讨论的范围。郑重说一句,SETI@home 可以让你配置优先级,除非你允许,否则它不会真的占用你机器的所有空余周期。

1. Java和单CPU的使用率

再回来讨论 Java 应用——CPU 周期性空闲意味着什么?这依赖于应用的类型。如果应用代码是批处理类型,工作量固定,你应该永远都不会看到 CPU 空闲,因为这意味着没事可做。提高 CPU 使用率,一直都是批处理任务的目的,因为任务会很快完成。如果 CPU 已经达到 100%,你仍然可以寻找优化,使得工作完成的更快(也要尽量保持 100%CPU 使用率)。

如果测试接收请求的服务器应用,就可能出现因无事可做而出现的空闲:例如,Web 服务器已经处理完所有未完成的 HTTP 请求,正在等待下一个请求的时候。这就引入了平均时间。上述 vmstat 的示例来自一个每秒接收一个请求的应用服务器。应用服务器花 450 毫秒处理请求——意思是 CPU 被 100% 占用 450 毫秒,550 毫秒没有占用。这就是所报告的 CPU 被占用 45%。

虽然经常因为 CPU 占用发生的时间粒度很小而难以可视化,但运行负载型应用时 CPU 的行为就是这种爆发式的。如果 CPU 每半秒收到一个请求而平均处理时间为 225 毫秒,也能从宏观层面看到同样的模式。CPU 被占用 225 毫秒,空闲 275 毫秒,再占用 225 毫秒,空闲 275 毫秒:平均来看,被占用 45%,空闲 55%。

如果应用优化之后每个请求只需要 400 毫秒,整体 CPU 使用率就会减少(到 40%)。这是仅有的降低 CPU 使用率有意义的情况——当系统负载量固定并且应用不受外部资源限制的时候。另一方面,优化也使系统可以承担更多负载,最终提高 CPU 使用率。微观来看,这种情况下的优化仍然是使 CPU 使用率在短时间内(执行请求花费 400 毫秒)变为 100%——只是 CPU 峰值持续的时间很短,事实上,大多数工具都不会将其标记为 100%。

2. Java和多CPU的使用率

上面的例子是假定在单个 CPU 上运行的单线程,但概念与一般情况下多 CPU 多线程相同。多线程倾向于以有趣的方式平均使用 CPU后面有这样的例子,展示多个 GC 线程如何使用 CPU。但一般来说,多 CPU 多线程的目的仍然是通过不阻塞线程来提高 CPU 使用率,或者是在线程完成工作等待更多任务时降低 CPU 使用率。

另外在多线程多 CPU 下,需要重点考虑以下 CPU 空闲的情形:即便有事可做,CPU 仍然空闲。这在程序没有更多线程可用的时候可能会出现。典型的情况是,应用以固定尺寸的线程池运行各种任务。每个线