JVM性能优化内幕-Java 性能工具箱
性能分析讨论的基础是所有过程和信息的可见性,即要了解应用程序内部以及承载应用程序运行的环境中正在发生什么。可见性的关键在于使用工具,所以性能优化是围绕工具展开的。
在第 2 章中,我们探讨了采用数据驱动的方法进行性能优化的重要性:你必须测量应用程序的性能并了解测量结果的意义。性能分析同样必须由数据驱动:为了优化应用程序,你必须掌握有关应用程序运行的数据。如何获取和理解这些数据是本章的主题。
数以百计的工具可以提供 Java 应用程序的运行信息,但是研究所有的工具是不切实际的。最重要的工具大都是 Java 开发工具包(JDK)自带的。尽管还有其他的开源工具和商用工具,但为方便起见,本章主要关注 JDK 工具。
操作系统工具和分析
性能分析的起点和 Java 无关,而和操作系统自带的一组基本监控工具有关。在 Unix系统中,有 sar(System AccountingReport)及其组成工具,例如 vmstat、iostat 和prstat 等。在 Windows 系统中,有图形化的资源监视器和类似 typeperf 的命令行工具。
无论性能测试何时进行,都需要从操作系统收集数据。至少要收集 CPU、内存和磁盘使用率的相关信息。如果应用程序使用了网络,也需要收集网络使用率信息。如果是自动化的性能测试,就需要使用命令行工具(甚至在 Windows 上也一样)。即使测试以交互式运行,最好也用命令行工具捕获输出,而不是盯着 GUI 图表猜测它的意思。在之后进行分析时,随时可以将捕获的输出以图表的方式展现出来。
CPU 使用率
我们首先来看 CPU 监控,看看从中可以得到关于 Java 应用程序的哪些信息。CPU 使用率通常分为两类:用户时间和系统时间(Windows 上叫作特权时间)。用户时间指的是 CPU执行应用程序代码的时间所占的百分比,系统时间则是执行内核代码的时间所占的百分比。系统时间和应用程序相关。如果应用程序执行 I/O 操作,那么系统将执行内核代码,从磁盘读取文件,或者将缓冲的数据发送给网络。任何使用底层系统资源的操作都会让应用程序使用更多的系统时间。
性能优化的目标是,在尽可能短的时间内,让 CPU 使用率尽可能高。这听起来有点违反常理,因为你肯定有过这样的经历:CPU 使用率到了 100% 之后,你只能坐在计算机屏幕前看着它挣扎。接下来让我们看看 CPU 使用率到底意味着什么。
首先需要牢记的是,CPU 使用率是一段时间内的平均值,也许是 5 秒、30 秒,甚至可能是1 秒(不会比它还短了)。假设在执行一个应用程序的 10 分钟内,CPU 使用率平均是50%,这意味着 CPU 有一半时间是空闲的。如果优化这个应用程序以避免出现空闲时间(在没有其他瓶颈的情况下),那么这样做能让它的性能翻倍,即以 100% 的 CPU 使用率在 5 分钟内结束运行。
如果我们优化这个应用程序的算法并让其性能再次翻倍,那么它会以 100% 的 CPU 使用率在 2.5 分钟内结束运行。CPU 使用率反映了应用程序使用 CPU 的效率,所以这个数字越大,性能就越好。如果我在 Linux 桌面系统上运行 vmstat 1,会得到如下的几行输出(每秒一行):
这个例子中运行的应用程序只有一个活跃线程,这使得例子更易于理解,不过即使有多个线程,概念仍然适用。
在每秒内,CPU 忙碌 450 毫秒(42% 的时间执行用户代码,3% 的时间执行系统代码)。相应地,CPU 空闲 550 毫秒。CPU 空闲可能是由于以下原因。应用程序阻塞在同步原语上,直到锁释放后才能继续执行。
应用程序在等待某些东西,比如数据库调用返回的响应。应用程序没有事情可做。
前两种情况往往表明问题可以解决。如果可以减少锁的竞争或者优化数据库以使响应返回得更快,应用程序就会运行得更快,平均 CPU 使用率也会上升(当然,此处假设没有其他类似问题会阻塞应用程序)。
第 3 种情况常常使人困惑。如果应用程序有事可做(而且没有等待锁或其他资源),那么CPU 必然会分配一些周期执行应用程序的代码。这是一个通用原则,并不局限于 Java。例如你写了一个包含无限循环的简单脚本,脚本执行时会消耗 100% 的 CPU 资源。下面的Windows 批处理任务就是这么做的:
试想一下,脚本没有消耗 100% 的 CPU 资源意味着什么。这意味着操作系统还可以做其他的事——比如可以打印另一行 LOOPING——但是它选择空闲。空闲在这种情况下并没有什么帮助,如果正在进行有用却很耗时的计算,那么强制 CPU 周期性地空闲会让我们得到答案的时间变得更长。
如果你在单 CPU 机器上或者容器中运行这个脚本,那么多数时候你不会注意到它在运行。但是,如果尝试启动一个新的应用程序,或者测试另一个应用程序的性能,你就会注意到影响了。操作系统会为竞争 CPU 周期的应用程序分配时间片,但是新应用程序的可用 CPU 周期较少,所以就会运行得很慢。有经验的人有时认为,留下一些空闲的 CPU 周期是件好事,以备不时之需。
但是操作系统不会猜测你下一步要做什么,它默认会执行任何可以执行的应用程序,而不是让 CPU 空闲。限制程序使用 CPU尽可能地利用 CPU 周期运行程序,可以最大化程序的性能。不过,有时你可能并不想这么做。比如,运行 SETI@home 时,它会消耗机器上所有可用的 CPU 周期。在你不工作或者仅仅浏览网页和编写文档时还好,否则这会降低你的生产率。(我们还没考虑你在玩 CPU 密集型游戏时会发生什么!)
有许多与操作系统相关的机制可以人为地限制程序使用 CPU——实际上,就是强制CPU 留下空闲周期,以防有程序需要使用。也可以更改进程的优先级,这样一来,后台任务就不会和你想运行的程序争夺 CPU 周期,也不会留下空闲的 CPU 周期。这些技术不在我们现在的讨论范围内。(顺便说一句,SETI@home 允许你对这些进行配置,它不会真的占用机器上所有的空闲周期,除非你告诉它这么做。)
01. Java 和单 CPU 的使用率
我们再回到 Java 应用程序的讨论,上述例子中的 CPU 周期性空闲意味着什么?这取决于应用程序的类型。如果应用程序属于批处理类型,那么其要完成的工作量是固定的。这种情况下,你不会看到 CPU 空闲,因为 CPU 空闲意味着无事可做。对批处理任务而言,榨干 CPU 的最后一点处理能力是其孜孜以求的目标。用得越多,任务完成的速度越快。即便 CPU 使用率已经达到 100%,你依然可以探索进一步优化的可能,从而让工作更快完成(同时尽量保持 100% 的 CPU 使用率)。
如果应用程序是服务器处理类型,即应用程序需要从某处接收请求,那么 CPU 会因为没有工作可做而处于空闲状态。例如,Web 服务器已经处理了所有的 HTTP 请求,正在等待下一个请求。这时就要引入平均时间。前面 vmstat 样例的输出是从服务器获取的,服务器每秒接收一个请求。应用程序服务器处理请求的时间为 450毫秒——在这 450 毫秒内,CPU100% 忙碌,而剩下 550 毫秒空闲。这会报告为CPU 是 45% 忙碌的。
尽管 CPU 忙碌时间常常因为粒度太小而无法可视化,但运行有负载的应用程序时,CPU 就是这样以短时突发的方式运行的。如果 CPU 每半秒接收一个请求且请求的平均处理时间是 225 毫秒,那么也可以在宏观层面看到同样的模式。CPU 会忙碌 225毫秒,空闲 275 毫秒,再次忙碌 225 毫秒,再次空闲 275 毫秒:平均来说,45% 的时间忙碌,55% 的时间空闲。
如果应用程序优化之后每个请求只需要 400 毫秒,那么总体的 CPU 使用率会降低至40%。这是唯一降低 CPU 使用率有意义的情况——流入系统的负载量固定且应用程序不受限于外部资源。同时,这些优化可以让系统承担更多的负载,最终提高 CPU使用率。在微观层面,这种优化仍然只是在短时间(执行请求的 400 毫秒)内让CPU 使用率达到 100%——只是 CPU 峰值的维持时间太短,不足以让大多数工具显示为 100% 使用率。02. Java 和多 CPU 的使用率上述例子假设在单 CPU 机器上运行单个线程,一般情况下,这与多 CPU 多线程是相通的。多线程会以有趣的方式使平均 CPU 使用率偏斜——第 5 章就有这样的例子,它展示了多个 GC 线程对 CPU 使用率的影响。但总体来说,在多 CPU 机器上运行多线程的目标仍然是通过确保单个线程不被阻塞而提升 CPU 使用率,或者是在线程已经完成工作,正在等待更多的工作时,降低 CPU 使用率(在很长的时间间隔内)。
在多 CPU 多线程的情况下,还有一个关于 CPU 何时空闲的重要补充:CPU 在有工作可做时也可能空闲。如果没有处理那项工作的可用线程,这种情况就会发生。典型的情况是,应用程序以固定大小的线程池运行各种任务。线程任务以队列形式放置,当有线程空闲而且队列中有任务时,该线程会取出任务并执行。然而,每个线程一次只能执行一个任务,如果该任务被阻塞(例如,等待数据库的响应),那么该线程在这期间无法执行新的任务。此时,有任务要执行(有工作要完成)但是没有可用的线程来执行它们,结果就是 CPU 空闲。
在这个具体的例子中,应该增加线程池的大小。但是,不要仅仅因为有空闲的 CPU可用,就认为应该增加线程池的大小以完成更多的工作。程序无法获得 CPU 周期,还有我们之前提到过的两个原因——锁或外部资源的瓶颈。在确定行动方案之前,了解程序为什么没有获得 CPU 很重要。(有关这一主题的更多详细信息,请参见第9 章。)
查看 CPU 使用率是了解程序性能的第一步,但这仅仅是看看代码是否使用了所有可用的 CPU 资源,或者看看是否出现了同步问题或资源问题。
CPU 运行队列
Windows 系统和 Unix系统都可以监控运行(意味着它们没有被 I/O 阻塞或者休眠)的线程数。Unix系统称之为运行队列,许多工具的输出包含运行队列的长度。3.1.1 节中的vmstat 的输出就包含这一数值:每行第一个数字就是运行队列的长度。Windows 系统称之为处理器队列,可以用 typeperf 命令查看(还有其他方式):
这个输出和之前的输出的重要区别是:在 Unix系统中,运行队列的长度(vmstat 示例输出中的 1 和 2)指的是,正在运行的线程的数量加上一旦 CPU 可用就可以运行的线程的数量。在示例中,总是有至少 1 个线程试图运行,即以单线程执行应用程序。因此,运行队列的长度至少是 1。记住,运行队列表示的是机器上所有的进程信息,所以有时其他线程(完全不同的进程)也试图运行,这就是为什么示例输出中的运行队列长度有时是 2。
在 Windows 系统中,处理器队列长度不包含正在运行的线程的数量。因此在 typeperf 的样例输出中,处理器队列的长度是 0,即便机器上运行的是同一个单线程应用程序,而且其中的线程始终在运行。
如果要运行的线程数量多于可用的 CPU,性能就会开始下降。总体来说,为确保性能,你需要让 Windows 系统的处理器队列长度等于 0,或者让 Unix系统的运行队列长度小于等于CPU 数。这不是一条硬性规定,系统进程和其他进程会定期地暂时提高这个值,这不会对性能产生重大影响。但是如果运行队列在相当长的时间内过长,那就说明机器已经过载,你需要想办法减少机器当前的工作量(通过将任务移至其他机器或者优化代码)。
快速小结
查看应用程序性能时,首先应该考察的就是 CPU 时间。优化代码的目的是提高 CPU 使用率(在较短时间内),而不是降低。在着手深入优化应用程序之前,应该先弄清楚 CPU 使用率为什么低。
本文就是愿天堂没有BUG给大家分享的内容,大家有收获的话可以分享下,想学习更多的话可以到微信公众号里找我,我等你哦。