即使做了很好的前期规划,应用程序仍可能出现重大的性能问题。 这篇由两个部分构成的文章给出了一些帮助您分析这些问题的技术, 重点关注的是基于 Eclipse 的富客户机 Windows 应用程序。 这是第 1 部分,我将向您展示如何度量基于 Eclipse 的 RCP 应用程序性能, 判断速度降低的原因是由于 CPU 还是 I/O 瓶颈, 保持 UI 线程空闲以保持响应性。 我还会给您提供一些避免线程错误以及提高应用程序启动性能的技巧。 第 2 部分将讨论一些跟踪内存问题的方法。 这些技术中的大部分也适用于 Eclipse 之外的应用程序。
关键概念
在您探究性能问题时,第一步是要判定出问题的任务究竟是受限于 CPU(CPU-bound) 还是受限于I/O(I/O-bound)。
CPU-bound 意味着 CPU 是完成工作的瓶颈,因此一个更快的 CPU 会 更快地完成动作。举例而言,如果您的 CPU 是 100MHz,用 50 秒 对 100,000 个电子邮件排序,那么 您可以期望一个 1GHz 的 CPU 能在 5 秒内完成排序。
但提高 CPU 频率并不一定会使任务运行更快。I/O-bound 任务是指那些以 I/O 为瓶颈的任务。 一个很好的例子是从磁盘读取大型文件或从 Web 站点下载文件。 一般而言,CPU 速度对 I/O-bound 任务没有影响,因为 处理文件读取的是 I/O 子系统。典型情形是,源设备不能保持足够高的传输速率以 使得 CPU 保持繁忙。CPU 在其等待数据时没有事情做,干脆休眠了。
不要猜测
不要费心去猜测为什么应用程序速度缓慢。 您的猜测或许是错误的,不要猜测,要分析。
所以有多个因素可能导致速度降低:CPU 繁忙、应用程序做的 I/O 过多、 等待 I/O 完成或上述情况的某种组合。凭空猜测其原因没有意义,使用工具判断则更为有效。 下一节介绍了一些工具,可以帮助判定任务是受限于 CPU 还是 I/O。
用于 Windows 的监视工具
对应用程序做监视的成本
开发人员经常会问对应用程序做分析会不会改变应用程序的行为。 这是一个好现象,不过他们应该问的是分析会对应用程序有何种程度 的影响。 总的来说,某种技术对应用程序的触及越深入,影响会越大。 记录比取样的开支更大,所以也比几个固定位置的日志消息有更大的开销。
举个例子,绝大多数剖析器都会带来很大的开销,使方法调用的时间不再有效,只有相对时间有效。根据您选择的是对调用堆栈做取样还是记录每个方法调用,分析所耗费的时间会有所不同。 记录使一个小而快的方法被频繁调用成为热点,因为随着方法不断被调用, 记录的开销不断增加。 而另一方面,堆栈取样不会导致热点,因为它在调用堆栈时所耗费得时间非常少。
根据您所做的分析类型,您可能会不关心分析应用程序行为所导致 的开销。比如说,您可能会愿意为准确捕获某方法被调用的次数而付出较高代价, 因为那对于理解应用程序顺序分析至关重要。
图 1. Perfmon Add Counters 对话框
我一般会添加如下计数器,选中 Process 作为 Performance 对象:
% User Time:该进程正在处理的工作量。
Handle count:该进程打开的句柄数。 其中,句柄数代表了某个应用程序打开的文件或套接字数目。
IO Data Bytes/sec:该进程正在操作的磁盘、网络或设备 I/O 量。
Private Bytes:与该进程相关的不能被共享的内存量 —— 应用程序大小的粗略估算。(该值对应于 Task Manager 中的 VM 大小。)
Thread Count:与该进程相关的线程数目。
另一方面,如果我要 “实时” 观察重复问题, 我会使用 Sysinternals Process Explorer。它的优势是能够关注一个进程而不是整台机器。 在考察一个特定问题时,您通常希望只观察的涉及到的那个应用程序。
在 Process Explorer 内双击您要监视的应用程序,打 开该进程的 Properties 对话框(参见 图 2):
图 2. Javaw 进程的属性
图 2 中的 javaw 进程来自于 JEdit。在本例中,我从磁盘打开了一个 14MB 的文本文件。 从下往上观察图 2 的三个图表,您可以发现:
I/O Bytes History 图表中的大型峰值表示磁盘 I/O 要读取那个 14MB 文件。 在线上停留片刻,会显示已读取 14MB。
Private Bytes 跳升至 33MB。Java™ 堆(heap)会为 14MB 文本文件要求 28MB 空间, 这主要是因为 Java 语言使用 16 位 Unicode 字符。Swing 和 JEdit 为管理编辑还需要另外 5MB 空间。
CPU Usage History 的大型峰值说明文件读取到内存后的执行了处理动作。 在此例中, JEdit 在更新显示,对文件做语法高亮等处理。
如果操作缓慢是由于 I/O 所致,您需要判断是哪部分应用程序导致了 I/O 问题。如果 操作缓慢是由于 CPU 所致,需要使用分析器。
设置分析器
为 RCP 应用程序所做的分析器设置与为其他很多类型的应用程序所做的不同, 因为 RCP 应用程序一般由一个可执行程序或 shell 脚本启动,而不是直接启动 Java 运行库。 问题还可能更复杂,因为 RCP 启动器为 Java 处理程序创建命令行参数并启动它。 这种更高级别的间接性会在您尝试分析或精细控制 JVM 调用参数时造成困难。 为了不依赖于应用程序启动器而启动 Java 运行库, 我经常提取 Java 命令行并直接启动它。下面是一种方法:
正常地启动应用程序。
运行后,启动 Process Explorer 并找到 javaw 或 Java 进程。打开进程属性 并从详细信息(参见 图 3)里复制命令行参数。
将复制内容粘贴到一个批处理文件并按照需求做修改。(按这种方式,您可以创建一个核心 批处理文件,配备几个变量用来添加或删除 VM 参数、类路径入口等等。)
图 3. 用于 Java 进程的命令行参数
确定 UI 线程中的长时间运行动作
绝大多数现代操作系统都有一个单独的 UI 线程。 同样的,Standard Widget Toolkit(SWT)也是如此。您必须小心 不要让这个单独的线程执行长时间运行操作,比如大量的磁盘 I/O、网络调用或 其他那些大工作量操作。