这是由五部分组成的有关 AIX 上的 Java 性能优化的系列中的第三篇文章。强烈建议您在进一步继续之前阅读本系列中的第 1 部分(如果您还没有这样做的话)。
本文集中于涉及各种类型的内存结构(Java 堆、本机堆、堆栈)的优化,并研究用于优化系统以进行大小调整的方法。
您应该查看第一部分,以了解适用于大多数情况的一般技巧。我们还提供了对于内存瓶颈检测和研究非常有用的工具的快速参考。下一部分将描述各种类型的应用程序以及如何优化它们。此讨论将利用您的应用程序知识来决定哪些技巧最适合您。第三部分将描述各种技巧。本文在结束时将讨论一下本系列中的下一篇文章。
本文讨论如何使您的应用程序扩展到更多数量的线程或具有更大的堆,或者同时实现这两个目的。您可能还希望通过迫使应用程序以受约束的方式使用资源,从而使其更加“文明”。在系统由多个应用程序共享的环境中,这是特别重要的。
当 AIX Java 应用程序尝试扩展时,存在若干可能会产生影响的资源瓶颈。Java 堆只是其中之一,并且在大多数情况下,您只需切换到更大的堆大小即可向上扩展。但是还存在另外三个内存方面,它们对于确定 Java 应用程序的内存占用空间和可伸缩性具有重要的影响。
除 Java 堆以外的第一个重要内存方面是本机堆(native heap),文章“Getting more memory in AIX for your Java applications” 描述了如何监视本机堆和调整其大小。Java 堆由垃圾收集器(Garbage Collector)管理,但是垃圾收集器不会对本机堆执行任何操作。因此,如果您看到本机堆稳定地增加,可能是由于不匹配的 JNI 分配(举例而言)所导致的。
第二个方面是用 -Xss
指定的本机堆栈(native stack)。这是针对每个线程分配的,并且不基于具体的使用情况,因此如果计划运行一百个线程,则要在指定 -Xss2m
前慎重考虑;它将消耗 200 MB 的本机内存,每个线程消耗 2 MB。在运行更多数量的线程时,这尤其成为一个限制因素,建议的解决办法是使用较小的值而不是使用缺省值。与 JVM 配套的 SDK 指南中提供了有关此主题的更多信息。
最后,第三个方面是通过 -Xoss
控制的 Java 堆栈(Java stack)。使用 -Xoss
指定的值是上限,因此指定一个较大的值的效果不如使用 -Xss
那么显著。请注意,由于 JIT 编译,您需要调整 -Xss
以满足大多数需要,并且 -Xoss
通常可以保留不变。例如,需要调整 -Xoss
的一个重要场合是在用完了 JNI 引用的时候。
我们不会集中于上述任何内存方面,因为其调整通常是在调试中出现的,而不是在性能优化中出现的。但是如果您陷入了资源不足的情况,现在可以检查这额外的三个方面。当您将系统利用到极限时,尤其必须了解这些方面。
谈到极限,您应该确保 ulimit
设置不会在大小调整期间成为瓶颈。在理想的情况下,我们建议将某些 ulimit
值设置为无限大,但是需要将此行为与某个进程消耗完所有本地资源的风险进行评估。对于性能优化工作,您可以首先将 ulimit
值设置为无限大,一旦达到所需的目标,您应该将这些设置设定为有限值。
您可以使用 ulimit -a
命令检查当前的 ulimit
设置,并且应该作为将启动 Java 的用户帐户至少运行以下三个命令:
ulimit -m unlimited ulimit -d unlimited ulimit -f unlimited |
在某些情况下,您可能不允许执行上述操作,因为运行 Java 的用户帐户可能没有分配足够的硬限制:有关必需的 ulimit
设置,请参阅位于 IBM developer kits for AIX, Java technology edition 的 JVM 配套 SDK 指南。
请注意,与 GC 相关的问题通常会作为 CPU 资源紧张的问题出现,因此如果遇到与 GC 相关的问题,您还应该阅读本系列的第 2 部分。此外,如果 RMI 触发了 GC,第 4 部分将对此进行讨论。
本部分的其余内容将简单介绍一些常用的工具和如何检测特定于 Java 的问题。有关更多详细信息,请参见 AIX 5L Performance Tools Handbook 和 Understanding IBM eServer pSeries Performance and Sizing。
vmstat
已在本系列的第 2 部分进行了介绍。要从 vmstat
输出中最需要了解的信息是是否发生了分页,以及分页是否是由于堆大小大于可用的物理内存而导致的。在大多数情况下,由于更大的堆而获得的任何可伸缩性好处,都抵不过由于堆分页而导致的严重性能降低,因此应该避免使用较大的堆。
svmon
是监视 Java 进程(尤其是本机堆)时最有用的工具。文章“When segments collide”(http://www.ibm.com/developerworks/eserver/library/es-segcollide.html)提供了如何使用 svmon -P <pid> -m
来监视 AIX 上的 Java 进程本机堆的示例。但是还存在另一种形式 svmon -P <pid> -m -r
,此命令对于确定本机堆碎片非常有效。 -r
开关打印正在使用的地址范围,因此它提供了每个段的当前使用情况的更准确视图。例如,请考虑下面经过部分编辑的输出:
Pid Command Inuse Pin Pgsp Virtual 64-bit Mthrd LPage 10556 java 681613 2316 2461 501080 N Y N Vsid Esid Type Description LPage Inuse Pin Pgsp Virtual 22ac4 9 mmap mapped to sid b1475 - 0 0 - - 21047 8 mmap mapped to sid 30fe5 - 0 0 - - 126a2 a mmap mapped to sid 91072 - 0 0 - - 7908c 7 mmap mapped to sid 6bced - 0 0 - - b2ad6 b mmap mapped to sid b1035 - 0 0 - - b1475 - work - 65536 0 282 65536 30fe5 - work - 65536 0 285 65536 91072 - work - 65536 0 54 65536 6bced - work - 65536 0 261 65536 b1035 - work - 45054 0 0 45054 Addr Range: 0..45055 e0f9f 5 work shmat/mmap - 48284 0 3 48284 19100 3 work shmat/mmap - 46997 0 463 47210 c965a 4 work shmat/mmap - 46835 0 281 46953 7910c 6 work shmat/mmap - 37070 0 0 37070 Addr Range: 0..50453 e801d d work shared library text - 9172 0 0 9220 Addr Range: 0..30861 a0fb7 f work shared library data - 105 0 1 106 Addr Range: 0..2521 21127 2 work process private - 50 2 1 51 Addr Range: 65300..65535 a8535 1 pers code,/dev/q109waslv:81938 - 11 0 - - Addr Range: 0..11 |
如果阅读过文章“Getting more memory in AIX for your Java applications”,您应该能够判断出上述配置是使用 LDR_CNTRL=MAXDATA=0x40000000。“Inuse”列中显示 4K 页的值,因此一个段(大小为 256 MB)在此列中的最大值将为 65536。正如上述输出所示,这个特定的应用程序在使用大量的本机堆;没有为段 3-5 打印范围的原因在于,尽管“Inuse”计数不是 65536,但是该段已经完全分配了。此外,如果没有使用 -r
,可以认为既然段 6 的“Inuse”值为 37070,则该段只有 56% 的使用率。但是可以判断出,段 6 正在使用的实际地址范围是 0 到 50453,或换句话说该段差不多达到 77% 的使用率。这会对应用程序的大小调整具有重要的影响。
要注意的另一个有趣部分在于,通过 svmon
无法看到 Java 堆碎片。段 7 至 A 看起来已完全利用(对应 SID 的“Inuse”值为 65536),而段 B 正在使用前 45056 个页面。这只是告诉了您该堆的大小约为 1100 MB,但是还存在找到此信息的更容易方法(即查看 Java 的命令行参数!)。
Fine-tuning Java Garbage Collection Performance 介绍了 Java 堆的优化。在 1.4 以前的 Java 版本上,您可能需要调整某些环境设置才能使用超出 1 GB 的堆,“Getting more memory in AIX for your Java applications”对此进行了解释。
如果您了解 verbosegc 输出的各项意义,以及如何使用 svmon
,对于 AIX 上的任何 Java 应用程序的大多数性能监视工作应该足够了。
存在若干有关 GC 优化的文章可以使用,因此基于您希望了解此主题的深度,您可以使用大量的信息。Diagnostics 页还指向一个总结 IBM Java 的 GC 是如何工作的文档,对于开发人员来说这应该特别有用。下面几个部分将坚持使用基于特征的优化方式,但是如果您希望更进一步研究任何特定的技巧,应该参考“IBM Garbage Collection and Memory Allocation techniques”一文,您可以从 http://www-106.ibm.com/developerworks/java/jdk/diagnosis/ 访问它。
下面我们将看一下典型应用程序的不同特征。您应该定位到与您的应用程序类似的行为(无论是设计上的还是观察到的),并应用对应的技巧。除非明确指明,否则术语“堆”是指 Java 堆。
对于大多数应用程序,使用固定与可变堆的决定是很容易做出的。任何具有或多或少的有限堆要求的应用程序都可以使用 技巧 MEM001,而任何会定期猛增堆使用量的应用程序使用技巧 MEM002 效果会更好。但是,如果应用程序逐渐地增长,或者如果您在考虑由于可变堆大小而导致的性能影响,则技巧 MEM001 也许仍然是可行的。位于 IBM developer kits - diagnosis documentation 的诊断指南包括一个有关如何调整 Java 堆大小的不错的内容。正如“Fine-tuning Java Garbage Collection Performance”所提到的,要遵循的规则是:分配应用程序需要的堆大小,但是决不多分配。但是在决定使用技巧 MEM001 之前,请参阅有关“堆回收”的部分。
如果应用程序堆需要迅速增长,则技巧 MEM003 将会非常有用。能从快速内存增长中获益的应用程序类型是在工作日的特定时间具有内存需求高峰的应用程序。如果优化得当,堆扩展次数将会减少,因为每次扩展会将堆增长更大的量。
另一方面,如果希望控制堆增长速度,请参见技巧 MEM004。当您看到堆扩展相当大并且希望控制扩展增量时,这是非常有用的。此类情况非常罕见,因为 Java 通常会基于定义良好的规则来扩展堆,除非您首先使用技巧 MEM003,否则堆不会快速增长。不过在需要时使用技巧 MEM004 还是有用的。
如果应用程序堆只应该增长而决不会收缩,可以使用 技巧 MEM005 来强制实现此目的。GC 周期不断监视堆使用情况,并在看到分配的堆大于当前需求时收缩堆。毕竟,这就是使用可变大小的堆的本意。但是有时,您可能注意到在堆收缩后,由于堆需求的增长而紧跟着进行堆扩展。在此情况下,您可以要求 JVM 不要收缩堆。
如果应用程序生成大量的临时对象,如果可以设置足够小的堆,则技巧 MEM001 将会很有帮助。其基本思路在于,如果堆增长到 200 MB 后触发一个 200 毫秒的 GC 周期,而不是增长到 1 GB 后触发一个 1500 毫秒的 GC 周期,则使用较小的堆效果会好得多,因为应用程序无论如何都不需要较大的内存占用空间。这当然是假设最大的堆大小始终满足用于长期分配所需的堆空间量。此技巧与 第 2 部分的技巧 CPU012 相同,但现在是集中于应用程序的内存占用空间,而不只是集中于应用程序的性能。
即使分配相当迅速,固定大小的堆通常也工作得很好。但是如果这些临时对象的大小通常相当大,则堆会很快变得零碎,从而导致假性 OOM。不适合使用技巧 MEM001 的另一个场景是固定 (Pinned) 对象正在导致碎片的情况。如果必须将对象固定在堆中,则将它们分配在堆中尽可能低的位置是有帮助的。但是除非非做不可,否则 Java 不会收集垃圾,因此只要有堆可用,就不会触发任何 GC 周期,并且这会转化为以导致碎片的方式分配固定对象。
较新版本的 Java 能够执行更好的碎片管理,并且可以使用新的开关来调整固定集群的大小(请参见 -Xk 和 –Xp)。但是如果您遇到堆碎片,技巧 MEM002 也许能满足您的所有需要。在许多情况下,堆扩展和收缩能够比压缩周期更好地消除堆中的空隙。堆碎片会严重影响应用程序的可伸缩性,技巧 MEM002 是在这些情况下使用的理想调整。
下面是基于 verbosegc 输出的简单指示信息。有关更多详细信息,请参阅“Fine-tuning Java Garbage Collection Performance”。
- 如果您在使用技巧 MEM002,并且在应用程序稳定时观察到太多的 GC,请参见技巧 MEM007。您可能还希望尝试一下技巧 MEM001,并确定它是否有帮助。
- 如果观察到堆扩展或收缩得太频繁,可以使用技巧 MEM001 来同时消除收缩和扩展。技巧 MEM005 将消除任何堆收缩。
- 如果 GC 周期中的标记时间太高,您应该尝试 MEM006。这也可能是由于“标记堆栈溢出 (Mark Stack Overflow)”所导致的。
- 如果 GC 周期不是由于“分配故障 (Allocation Failure)”所导致的,可以使用技巧 MEM007。不过分布式 GC 调用不会受到此设置的影响。
- 如果 verbosegc 中指示了过多的压缩,这可能与某个大小调整不足的堆有关。
如果应用程序的本机代码发出多个小请求,则使用技巧 MEM008 也许会获得性能提升。但是本机堆调整的最重要部分是确保每一次本机堆分配都与相应的回收相匹配。可以使用 svmon
监视本机堆,并且您可能希望设置 IBM_JAVA_MMAP_JAVA_HEAP=true,以便更清楚地区分 Java 堆和本机堆。除了确保应用程序不会在运行时耗尽本机堆以外,通常没有多少针对本机堆的性能优化余地。
下文将把 Java 的命令行参数(在 class/jar 文件名称之前指定)称为“开关”。例如,命令行 java -mx2g hello
具有单个开关 -mx2g
。
通过同时为初始 (-Xms
) 和最大 (-Xmx
) Java 堆大小指定相同的值,从而创建固定大小的 Java 堆。指定的值应该足够高到不会导致 OOM,同时要足够低到不会显著增加 GC 周期时间。
请注意:指定固定大小的堆意味着您将不能使用 -Xminf/-Xmaxf/-Xmine/-Xmaxe
来微调 GC 特征。固定大小的堆在许多情况下还容易导致碎片。
通过为 -Xms
和 -Xmx
指定不同的值,或者仅指定 -Xmx
,从而创建可变大小的 Java 堆。指定的值应该足够高以避免 OOM,但是应该进行调整以避免过多的堆收缩或扩展。
请注意:如果优化不当,使用可变大小的堆会产生严重的性能影响。
使用高于缺省值 (1 MB) 的 -Xmine
值。这将允许 Java 堆的最小扩展变得更迅速。例如, -Xmine5m
将允许堆一次增长 5 MB(或更多,最多可达 -Xmaxe
)。
请注意:-Xmine
只是在进行堆扩展时发挥作用的若干因素之一。有关堆扩展的更多信息,请参见 Diagnostics Guides。
使用与缺省值 (0) 不同的 -Xmaxe
值。这会强制将 Java 堆的最大扩展保持在指定的限制之内。例如,-Xmaxe2m
将强制堆在一个周期中的增长不能超过 2MB。
请注意: -Xmaxe
只是在进行堆扩展时发挥作用的若干因素之一。有关堆扩展的更多信息,请参见位于 IBM developer kits - diagnosis documentation 的 Diagnostics Guides。
要禁用堆收缩,可以使用 -Xmaxf1
,此开关将最大空闲堆百分比设置为 100%。
请注意:这将强制堆只能增长,如果堆具有固定大小,则此开关不起作用。
技巧 MEM006:使用并发标记 (Concurrent Mark)
使用开关 -Xgcpolicy:optavgpause
以启用并发标记。
请注意:对于 CPU 密集型应用程序,此设置可能会影响性能。
使用开关 -Xdisableexplicitgc
禁用任何 System.gc()
调用以避免触发 GC。
请注意:确保您的应用程序功能不会受到此开关的影响。
设置环境变量:
export MALLOCTYPE=buckets |
以切换到基于 Bucket 的本机堆模型。
请注意:Java 堆分配不受此开关的影响。而且,如果使用不当,此开关会影响应用程序的性能。
本文向您介绍了如何使用 AIX 工具进行 Java 性能监视,并提供了可用来优化应用程序的内存使用的常用调整列表。本系列中的下一篇文章将讨论“AIX 上的 Java 应用程序的网络和磁盘 I/O 调整”。
学习
- 本系列文章的其他部分:
- AIX 5L 性能优化系列文章:这个系列文章十分系统地向您从 CPU、内存、硬盘和网络方面介绍了如何对 AIX 5L 进行优化。
- IBM developer kits for AIX, Java technology edition
- IBM developer kits for AIX, Java technology edition
- IBM developer kits - diagnosis documentation
- AIX 性能 PMR 数据收集工具
- AIX 5L Performance Tools Handbook