关于线程后值得一提的是:除非您的确需要,不要频繁休眠。 清单 8 说明了一个不当行为的例子:
清单 8. 频繁休眠
while(someCondition) {
...more code here...
Thread.sleep(aFewMilliseconds);
...more code here...
}
这么做的应用程序都会在该代码块处创建大量不必要的垃圾 并生成大量上下文切换。您不应当使用频繁休眠, 而应使用 java.util.concurrent 提供的更高级别同步类,如 BlockingQueue、Semaphore、FutureTask 或者 CountDownLatch。 这些同步类提供了一种在等待条件变为真时不消耗 CPU 的途径。 当您调用第三方代码而这些代码没有使用监视器的有些时候不可能达到上面的要求, 在此情况下,您所能采取的佳做法 是尝试在进行池操作时使创建的垃圾量小。
分析并提高启动性能
对于 RCP 应用程序而言,提高其启动性能是一项挑战。 一般而言,启动性能是由磁盘 I/O、类载入和字节码验证综合而成的功能。 当然,如果在您的包内加入过多工作,也可能使其启动缓慢,但是通常情况下 这并不是启动中耗时的一部分。 启动往往会由于许多个小的时间消耗而变得及其缓慢。 通常不会有任何一件事情消耗大量时间,而是每件事都只消耗少量时间, 但当其累积起来后终导致了大量时间的消耗。
RCP 应用程序构建于 OSGi 之上,它是面向 Java 的动态模块系统(Dynamic Module System)。OSGi 提供了一种简单的手段以全局方式钩取类的装载。 有人已经利用这种类装载钩子来创建 Java 类缓存,从而避免频繁访问磁盘并提高 启动速度。这种技术很有前途,不过尚需更多研究以确定其功效。
为了提高启动速度,Eclipse 鼓励的另一项技术是 包按需激活(lazy bundle activation):直到某个包需要时才被装载并激活。 一般在分析启动性能时,我会收集所有激活包的列表以及对应于它们为何激活的堆栈跟踪信息。 接着通览列表,判断我是否认为该包确实应该在启动时被激活。 如果我认为有个包在启动时不需要,我会删除它以提高启动性能(同时看发生了什么中断)。 一旦我知道了删除包后导致何种提高效果, 我联系该代码的开发人员,与之讨论删除或延迟对该包的激活。
要想收集包激活和类装载信息,可使用如 清单 9 所示的调试选项,也可在 org.eclipse.osgi 包的 .options 文件中找到, 或者看看 CVS 的近版本(请参阅 参考资料):
清单 9. 启用 OSGi 调试选项
org.eclipse.osgi/debug=true
org.eclipse.osgi/debug/bundleTime=true
org.eclipse.osgi/debug/monitorbundles=true
org.eclipse.osgi/monitor/activation=true
org.eclipse.osgi/monitor/classes=true
不过,插件开发人员可能自行其是阻挠按需装载。 有个例子,我曾参与一个产品,它有一套堆栈视图。对它定义了一个扩展, 以便于其他人能够贡献自己的堆栈视图。在启动的时候,可能只有一个或者没有视图可见, 但该扩展的作者提前创建了这些视图, 即使它们根本不会展现出来。后来把改扩展改为只显示视图的标题和图标, 直到终端用户真的尝试看该视图时,才激活加入到扩展中的那个包。
另外一个例子,假设您正在创建一个应用程序,它有一个登录对话框。 您的目的是仅激活用于显示登录对话框的包。 我曾经看到有些应用程序,为了显示登录对话框激活了所有包的 70%。
作为一种手段,我建议您开发一个 shell 游戏,它的启动时间可以有所浮动但是总和保持相同。 用户不必为他尚未使用到的特性付出等待时间。 这样做的目的是只为需要付出而不是为所有东西付出时间。 如果某个应用程序在您做了所有提高性能的努力后仍然不够快, 那么不要忘记提高用户在感觉上的性能。
结束语
我特别强调在您的应用程序架构和设计阶段考虑性能。 起码,架构师或者首席开发人员必须知道基本的顺序分析或时间复杂性(比如 Big O), 以便理解应用程序的存储需求或执行时间随应用程序增长如何改变。 后才考虑解决性能问题是被动的 —— 也很低效 —— 因为在游戏后期几乎已不可能再去对架构做大的调整。
不过即使是拥有好的架构的应用程序也会有性能瓶颈, 您需要使用工具和技术了解并处理瓶颈。 现在您了解了如何度量 RCP 应用程序性能,判定是 CPU 还是 I/O 瓶颈导致了速度降低, 使用一些记录技术,保持 UI 线程可响应,用 Job 回避线程误用, 以及提高启动性能。
理解一个富客户机(Rich Client Platform(RCP))平台应用程序的完整内存使用 会是一项脑力劳动。操作系统(OS)会指出应用程序耗费了多少内存,Java™ 平台会指出 您已经耗费了多少堆。操作系统汇报的内存使用情况总是高于可用堆大小。 不幸的是,有时操作系统所报告的数目会远远 大于堆大小。 对于堆分析的一个挑战是判断这片 “黑暗空间” 中藏匿着什么。
一般而言:进程使用的内存 = Java 堆 + 已编译的本地代码 + 字节码 + 其他 / 本地
很不幸,JVMS 根据其发行版本和供应商的不同,指示出的堆大小也不同。 我所运行的一个 Java 应用程序可以给出一些例子:Sun 1.6 JDK 报告堆大小为 32.7MB , 而操作系统报告为 48.6MB 私有字节,有 16MB 未作说明。总的来说这还算不错。 已编译代码和字节码是这 16MB 的一部分。 用 IBM® 1.5 JDK 运行同一应用程序,堆加上类加载器和已编译代码总共 是 39MB,而 OS 报告的大小为 45.8MB。
一般而言,您可以把问题简化为只关注 Java 堆。 这对绝大多数 Java 应用程序而言已经足够了,而且也可以让应用程序做到大程度的改进。 如果还不够,那么您应该使用操作系统工具检查未被 Java 堆覆盖的本地内存。
差异分析(Differential analysis)
处理内存使用问题中为行之有效的一种手段是关注对象数目。 举例而言,如果要在某个邮件应用程序中显示 50 条邮件消息, 那么需要多少个 MailMessage 类的实例? 50,对吗?那么邮件详情或其他邮件域对象呢?如果切换了文件夹,显示新的 50 条邮件消息,又将发生什么情况呢? 您会拥有多少个对象:50 还是 100?
一旦开始进行此类分析,您会对 实例数目大大超过期望数目这一常见情形感到惊讶。注意:在您收集堆转储之前,确保 已经发生了垃圾收集行为,因为您不会想去考虑那些已经死亡的对象。 一般情况下,我会在捕获堆转储前做一个 System.gc() 操作。
我并不想去描述司空见惯的一般性堆分析(请参阅 参考资料)。 相反,我将介绍差异分析(differential analysis),这是用于发现应用程序中内存泄漏的技术。
它的基本思想很简单:
得到一个堆转储。
在应用程序中多次做某件事(假设做 10 次)。
得到另一个堆转储。
比较两个堆转储中应用程序对象的数目。
这样可以构建所需应用程序对象集合。 随着泄漏的发现和处理,将泄漏到脚本的类添加到一个列表。 这样一来,不长时间可以构建经常检查的应用程序对象集合。
单元测试
我所用的另一个技术是写单元测试,解析堆转储并对期望的域对象实例数目做断言。 比如说,您可以启动应用程序,运行一个场景,得到一个对转储,接着做断言。 下面是一个例子:在邮件应用程序中发现一个内存泄漏,当该泄漏被处理后,我希望确定 在以后的代码改变中不会再发生该问题,于是为此构建了一个单元测试。这是一个资源使用 单元测试,如清单 1 所示:
清单 1. JUnit 测试用例,解析堆转储
public void testOpenTenMessages() throws Exception {
Heap heap = Heap.from("openMessages.phd");
assertEquals(10, heap.instancesOf("cbg/mail/ui/message/MessageController"));
assertEquals(10, heap.instancesOf("cbg/mail/ui/message/viewer/AttachmentModel"));
Heap heapAfter = Heap.from("openMessagesClosed.phd");
assertEquals(0, heapAfter.instancesOf("cbg/mail/ui/message/MessageController"));
assertEquals(0, heapAfter.instancesOf("cbg/mail/ui/message/viewer/AttachmentModel"));
}
其工作原理是:打开 10 条邮件消息,创建名为 openMessages.phd 的堆转储。 然后关闭消息并创建第二个堆转储,命名为 openMessagesClosed.phd。
针对这两个堆转储文件,现在对内存中所需域对象数目做断言。 我期望在第一个转储中有 10 条邮件消息(MessageControllers), 在第二个中没有任何邮件消息。
这种自动堆分析是对不同构建之间的变化做跟踪的有力途径。 和标准单元测试一样,您可以仅在发现和处理内存泄漏时才创建此类单元测试。 把应用程序中的资源使用看作应被跟踪的另一个量度信息是有益的。 即便是知道应用程序在运行后分配了多少个对象,也有助于构建的发展。
不幸的是,不同的 JVM(即便是相同 JVM 的不同版本)在堆分析上有着极大的不同。 IBM JVM 改变过几次堆分析格式。Sun 的 JVM 使用另一种格式,并且在每次发布时也做过改动。
回页首
图形设备接口资源的泄漏
在 Windows® 操作系统中,每个颜色、字体、图形上下文(graphics context(GC))、图像、光标或者区域都对应于一个单独的图形设备接口(graphical device interface(GDI))资源。 GDI 是 Windows 的术语,不过每个 OS 都有一个对应物。重要的是整个 OS 所拥有的 GDI 资源数目是有限的。 如果应用程序泄漏或使用了过多的资源,将会影响到系统上所运行的所有应用程序。GDI 泄漏很糟糕。
判断 GDI 资源是否泄漏比较简单。在 Windows OS 中,您可以使用 Task Manager 或 Process Explorer。 添加 GDI 列,观察它是否随时间而增长(参看图 1)。比如说,您可能注意到每当打开一条邮件消息, 与 javaw 进程关联的 GDI 资源数目会增加 50,但是当您关闭邮件消息后, GDI 资源的数目只减少 46。您每阅读一条邮件消息,会泄漏 4 个 GDI 资源。
尽管 Task Manager 能告诉您何时 发生了泄漏, 但它不能帮您发现哪里 发生着泄漏。要做到这点,好的办法是使用 Sleak,一个 SWT 开发工具(请参阅 参考资料)。 您可以启用 SWT 所拥有的调试标记,使它跟踪 GDI 资源的创建位置。 Sleak 让您看到 GDI 资源以及它们是从哪里分配的。