最大化 AIX 上的 Java 性能,第 5 部分: 参考资料和结论
2008-11-10 08:26:47 来源:WEB开发网引言
这是由五个部分组成的有关 Java 性能的系列的结束部分。本系列中的第一篇文章奠定了性能优化的基础,第 2、3 和 4 篇文章考虑了会影响系统可伸缩性和吞吐量的各种瓶颈。本文介绍两个先前未涉及到的重要主题,并提供案例研究和参考资料。
一个常见问题 (FAQ) 是如何将特定于 Sun 的命令行开关转换为特定于 IBM 的开关。此外,任何严格的性能优化工作,例如基准测试,都不能忽略系统范围的优化。我们将在下一个部分中简要讨论这些主题。
随后是几个案例研究,尝试说明如何应用本系列中描述的工具和技巧来解决实际中的问题。重点在于了解和学习如何使用本系列中提到的工具和技术。
最后通过回顾有用的参考资料结束本文和本系列。
其他重要注意事项
本部分讨论如何将 Sun Java 配置转换为 IBM Java 配置,以及针对 AIX 应用程序的系统范围的优化。这些主题的范围相当大,我们只是略微触及皮毛。
转换 Sun Java 开关
如果您有一个为 Sun Java 而优化过的应用程序,并且正在尝试将应用程序迁移到 AIX(或迁移到任何运行 IBM Java 的平台),您可能已经完成了最艰巨的工作。了解应用程序特征只是成功的一半。基于对使用 Sun Java 进行优化工作的了解,您可以使用第 2 部分和第 3 部分 中阐述的优化技巧。
但是,我们经常收到有关如何将特定 Sun Java 命令行开关转换为等效的 IBM Java 命令行开关的询问。这些开关几乎始终对应于垃圾收集,因为经过充分优化的 GC 对任何基于 Java 的应用程序的性能都非常重要。由于 JVM 体系结构方面的区别,Sun 和 IBM 开关之间的映射非常困难。IBM Java 没有包含分代的垃圾收集器 (Generational Garbage Collector),并且不支持任何以 -XX 开头的命令行开关。IBM Java 的“完全独立”体系结构也不基于 Sun HotSpot 体系结构。最容易并且在大多数情况下最快的方法是在 IBM 平台上运行应用程序时丢弃所有特定于 Sun 的设置,并根据需要进行微调。但是如果您对某些 Sun 开关如何映射到 IBM 开关感到好奇,请继续阅读。
下表尝试将 Sun Java GC 命令行开关转换为等效的 IBM 开关。此映射基于特定于 Sun 的文章“Tuning Garbage Collection with the 1.4.2 Java™ Virtual Machine”中描述的 Sun 开关的功能。此表只用于定位 IBM Java 的等效(或近似)开关这个非常特定的目的。这并不意味着取代优化实践,因为即使是堆大小要求也会差异相当大。有关 IBM Java 的一般 GC 优化技巧,以及有关 IBM Java 的这些和其他 GC 开关的信息,请参见“Fine-tuning Java garbage collection performance”。此表的创建并不涉及任何性能相关的测试,而是完全基于上面引用的参考资料中的 Sun 开关用法的文档记录。
Sun 开关 | 等效的 IBM 开关 | 说明 |
-Xms、-Xmx | -Xms、-Xmx | 这些参数及其含义保持不变。您仍然需要执行堆大小调整。 |
-XX:SurvivorRatio、-XX:NewSize、 -XX:MaxNewSize、-XX:NewRatio | 无 | 这些开关可简单地删除,因为它们用于 GC,而 GC 不适用于 IBM Java。 |
-XX:MinHeapFreeRatio、-XX:MaxHeapFreeRatio | -Xminf、-Xmaxf | 堆扩展/收缩受其他因素而不只是受这些开关的控制。 |
-Xverbose:gc、-XX:+PrintGCDetails | -Xverbose:gc | IBM Java 的 verbosegc 跟踪格式与 Sun GC 区别相当大。可以根据需要启用更详细的跟踪,但是在大多数情况下,缺省的 verbosegc 跟踪就足够了。 |
-XX:+UseParallelGC、-Xincgc、-XX:+AggressiveHeap | 无 | 这些是 Sun 支持的各种类型的垃圾收集器。它们不适用于 IBM Java。 |
-XX:+UseConcMarkSweepGC -XX:+UseParNewGC | -Xgcpolicy:optavgpause | 并发低暂停 (Concurrent Low-pause) 收集器的作用接近于 IBM 并发标记(但在设计上是不必要的)。 |
-XX:+CMSParallelRemarkEnabled | 无 | 不适用于 IBM Java。 |
-XX:ParallelGCThreads | -Xgcthreads | 至少对于 IBM Java,更改此设置是不可取的。 |
-Dsun.rmi.dgc.client.gcInterval、-Dsun.rmi.dgc.server.gcInterval | -Dsun.rmi.dgc.client.gcInterval、-Dsun.rmi.dgc.server.gcInterval | 请参见第 4 部分中的技巧 NIO003。 |
正如上表所示,大多数情况下,把 Sun 平台上的开关转换到 IBM 平台的方式就是丢弃这些开关。这使得针对 IBM Java 的GC 优化工作相当简单,同时仍然提供卓越的性能。
系统范围的优化
使用诸如 schedo 和 vmo 等 AIX 工具具有系统范围的影响,因此对这些工具的全面介绍超出了本系列的范围。有关这些工具的更多信息,请参见参考资料部分。
但是对于大多数多层应用程序(尤其是基准测试),系统范围的优化是不可避免的。存在若干可用于 AIX 性能优化的优秀资源可供您参考。要了解通常需要的优化类型,您可以查看实际的已发布基准。如果查看最近针对 AIX 上的 IBM Java 的 SpecJBB 2000 结果,比方说 http://www.spec.org/osg/jbb2000/results/res2003q3/jbb2000-20030624-00194.html,则操作系统优化如下所示:
操作系统优化 SPINLOOPTIME=2000 vmo -r -o lgpg_regions=256 -o lgpg_size=16777216 setsched -S rr -P 40 -p $$ schedtune -t 400 -F 1 vmtune -S 1 |
警告:切勿在没有充分了解后果的情况下应用这些设置,因为不正确使用这些设置实际上会恶化系统性能。
那么上述设置有何作用呢?参阅 AIX 文档,您很快就可以更好地了解这其中每个设置是做什么用的。让我们依次研究一下其中每个设置。
SPINLOOPTIME 控制系统在让步于另一个进程之前重试某个繁忙锁的次数。由于缺省值为 40,更高的值将告诉系统应该尝试更长的时间以待锁释放。在多处理器系统上,这样会导致更好的性能,因为忙锁重试的开销要比进程上下文切换的开销低。
vmo 行设置大型页的大小和数量。查看一下 Java 命令行开关,您将看到正在使用 -Xlp。该开关启用 Java 中的大型页支持,白皮书”AIX Support for Large Page“对该开关进行了更详细的描述。如果您有内存密集型应用程序,可以试验大型页以确定它是否有帮助。与 Java 配套的 SDK 指南中提供了有关此主题的更多信息。
setsched 行实际上是一个脚本而不是 AIX 命令。它调用 thread_setsched 内核服务以选择固定优先级的循环调度,并对该 Java 进程使用优先级 40。
schedtune 命令也是 AIX 5.2 以上版本中的一个脚本,它将传递的参数映射到新的 schedo 命令。上面这一行命令是将固定优先级线程的时间片更改为 400 个计时单元。它还强制固定优先级的线程驻留在全局运行队列中。
最后,vmtune 命令将该调用转换为等效的 vmo 调用,并且上面的行还启用了共享内存段的固定。
因此可以看到系统被切换到大型页和固定优先级的调度程序,以达到 SpecJBB 2000 基准的记录数据。您能否在自己的应用程序中使用这些相同的设置呢?也许不能,但是您现在能够研究这些命令和调度策略,并进行试验以适合自己的应用程序特征。这是性能优化中的下一步。
案例研究
在本部分中,我们将查看一些示例,这些示例取自 Java Service 团队处理过的具体问题。这些示例能够让您清楚了解如何进行性能优化,以及如何使用各种工具来收集可用于性能优化工作的信息。请注意,本部分的案例不是基于问题在实际工作中的出现频率来选择的。重点在于了解如何使用本系列中讨论的各种工具和技术来定位和纠正性能问题。
案例 1 糟糕的应用程序响应时间
报告的问题是基于 Java 的应用程序的响应时间不可接受。使用 topas,然后使用 vmstat,结果发现 Java 是消耗最多 CPU 的应用程序。使用 tprof,出现的函数中具有 GC 相关的术语,因此这指示可能存在 Java 堆大小调整方面的问题。
观察 GC 日志确认了堆扩展得太频繁。这与接二连三的多次分配故障和非常满的堆相结合,导致了 Java 应用程序花大量的时间尝试分配空闲块,未找到空闲块,扩展堆,然后仅满足当前分配请求。
这可以通过为 -Xmine 指定更大的值来修复,从而迫使堆增长得更快(请参见第 3 部分中的技巧 MEM003)。其结果是单次扩展即可避免多次潜在的分配故障。
第一步是使用 AIX 工具来确认这是与 Java 相关的问题。在 AIX 工具指示这是与 GC 相关的问题这个事实的指导下,第二步可以是直接集中于 GC 日志。第三步是使用可用的优化参数来中断应用程序陷入的异常周期,从而允许恢复多余的 CPU 时间。
案例 2JVMPI 让您绝处逢生
另一个有趣的场景是作为性能问题提出的,其中 CPU 在大多数时间都处于繁忙状态。通过查看 verbosegc,我们看到有一个 GC 周期被调用的太过频繁,从而导致应用程序的大多数时间只是花在执行 GC 上。
verbosegc 跟踪表明,大多数 GC 活动都是由于超大型对象的多次分配所导致的,该对象的大小约为 10 MB 或更大。这些活动导致了堆碎片,使得 GC 周期更长,从而影响了性能。但是通过查看 verbosegc 周期,客户无法确定这些对象是什么。
确定问题根源的最容易方法是使用 HeapRoots 工具来分析堆转储 (heapdump)。但是该场景还存在另一个难解之处:那些大型对象并没有存活到 GC 周期之后。因此堆转储没有显示任何具有此类大小的对象。
对于定位和纠正应用程序源代码中的问题来说,这是分析(profiling)如何能够成为有用助手的经典示例。Java 虚拟机分析接口 (Java Virtual Machine Profile Interface) 使得此问题变得微不足道。对于这个特定示例,我们使用了 Using JVMPI to Identify Large Memory Allocations 上描述的方法的一种变化形式,并且能够快速确定执行此分配的代码。
案例 3无限增长
作为本文的最后一个案例研究,我们将讨论一个其表现似乎为简单的大小调整问题的场景。曾经尝试将该应用程序扩展到 1000 个用户,并且应用程序将会耗尽 Java 堆空间。基于用户数量来计算堆需求后,将堆大小从 1 GB 增加到了 1.5 GB。
但是这样触发不是由于 Java 堆带来的 OOM 错误。Java 堆显示了足够的空闲空间,但应用程序日志表明发生了 OOM。通过使用 svmon,发现仅有大约 4 个段(或 1 GB)用于本机堆,第四个段似乎差不多是空的(请参见“Getting more memory in AIX for your Java applications”中的“Balancing Memory”)。
为了进一步深入研究,我们添加了命令行开关 -verbose:jni。作为此开关的结果而打印的额外消息表明全局 JNI 引用池被耗尽了,发生这种事情是非常少见的。全局 JNI 引用池足够大,以确保大多数正常应用程序决不会耗尽它(甚至是接近耗尽它)。
一般情况下,我们尝试通过增加 JNI 引用数量来绕过该问题(如果指定更高的 -Xoss 值,则会呈比例地增加上限)。但这只是延迟了不可避免的问题,大量的固定 JNI 引用导致的严重 Java 堆碎片并没有任何好转。
进一步研究应用程序的设计揭示了真正的根源:应用程序创建了无限数量的线程。随着测试的继续,线程将等待终结器,而且由于终结器是不可预测的,因此存在大量的此类线程在等待释放它们的 JNI 引用。在此情况下,唯一可行的解决方案是更改应用程序代码以纠正这两个问题。一旦应用程序将无限数量的线程替换为线程池,并且尽可能替换终结器,大小调整工作就胜利完成了。
此案例表明,有时候问题的原因是一些深层次的因素。Java 堆耗尽的问题最终证明是设计问题。Java 和本机堆的平衡通常是性能优化的关键部分,但在此例中是不足够的。手边拥有如此多的工具和技术可以让您了解更全面的情况,使您能够做出应该优化什么对象的精明决策。
结束语
本文将结束整个系列。希望本系列成为最大化 AIX 上的 Java 应用程序性能的宝贵指南。
作者要感谢 Ashok Ambati、Rajesh Jeyapaul、Sharad Ballal、Roger Leuckie 和 Mark Bluemel 为这些文章提供的帮助和建议。特别要感谢 John Tesch,他的有关 AIX Java 性能的信息集合是本系列的主要灵感之源。
更多精彩
赞助商链接