本系列的 第 1 部分 详述了最新版本的 IBM Java 运行时环境(JRE)中不同的 GC 策略:
-
optthruput:针对吞吐量进行优化。这是默认策略。 -
optavgpause:针对平均 GC 停顿时间进行优化。 -
gencon:使用分代并发收集样式。 -
subpool:加速多处理器系统的对象分配。此策略只适用于 IBM pSeries® 和 zSeries® 平台,本文不打算深入讨论。
本文将介绍如何收集选择特定策略所需的信息。默认策略在大多数情况下都运行良好,无需更改。然而,不同的策略有不同的特征,对于一些应用程序来说,其他策略可能更适合。在评估不同 GC 策略之前,必须决定对特定应用程序来说非常重要的性能特征。
选择 GC 策略时,考虑垃圾收集会通过多种方式影响性能非常重要,包括积极和消极影响。所有 GC 策略(甚至包括并发策略)都会使应用程序线程停顿一段时间。这些停顿会导致应用程序瞬间无响应,并减少应用程序的可用工作时间。不过,正确的 GC 策略能够为应用程序交付重要的性能优势。
垃圾收集可以改进对象局部性和加速新对象分配,从而增强应用程序性能。通过紧凑排列(compact)或执行复制收集在堆中重新排列对象的垃圾收集器可以将堆中相互访问的对象移动到一起,这能够对应用程序性能产生巨大影响。将同一时刻访问的对象分组到一起的垃圾收集器能够对应用程序性能产生积极影响。紧凑排列和复制收集器还可以通过保持内存完整性来提高新对象的分配速率。这消除了在可用内存中查找足够大的内存片段来保存新分配对象的需要。
优化性能的一个明显方法是选择能够最小化垃圾收集停顿时间带来的消极影响的 GC 策略。第二种不是很明显的方法是选择能够最大化垃圾收集的益处的策略。例如,如果要创建很多短期对象,则选择 gencon 策略来利用它的快速分配能力,这样可以获得最佳性能。
在获得应用程序的最佳性能之前,需要考虑想要什么类型的性能特征。应用程序的性能可以用两个明显的属性来描述:吞吐量 和响应时间。吞吐量表示系统处理的数据量,而响应时间是应用程序处理请求(从收到请求到完成处理)的时间。例如,一个 Web 应用程序可能每秒处理 1,000 个请求(吞吐量),每个请求需要 2 秒的处理时间(响应时间)。
对于一些应用程序来说,只有一种性能特征是重要的。交互式应用程序(如文本编辑器)不需要处理大量数据,但是它需要及时响应用户的按键。另一方面,长时间批量处理应用程序不需要快速响应,但是有效处理大量数据的能力很重要。然而,对于大多数应用程序来说,理想的性能是在响应时间低并且 吞吐量高的情况下实现的。
应用程序响应请求的快慢取决于传入请求时正在发生的其他事情。例如,如果正在进行垃圾收集或传入队列中有大量的请求,那么处理请求的时间可能稍长些。在确定给定应用程序的重要性能特征时,同时考虑平均响应时间和最大响应时间是很重要的。例如,如果一个交互式应用程序通常能够在几毫秒内响应用户请求(低的平均响应时间),但是它偶尔会停顿十秒钟(高的最大响应时间),那么用户可能 不会满意这个应用程序的性能。
务必注意 GC 停顿时间与应用程序响应时间不同。短的停顿时间不能够保证快的应用程序响应速度,长的停顿时间也不一定会导致长的应用程序响应时间。在负载非常轻的系统中,停顿时间是响应时间的重要部分,但是随着系统负载越来越重,停顿时间也就变得越来越不重要。但是,很少收集堆时的停顿时间比经常收集时更重要。
通过检查 verbose GC 日志不可能计算出应用程序响应时间。平均响应时间与应用程序吞吐量密切相关,而与 GC 停顿时间的关系不大。最大响应时间总是比最长 GC 停顿时间长,除非应用程序负载极低。但是,在大多数应用程序中,GC 停顿对最大停顿时间的影响很小,您将在我们的 第一个案例 中见到。
Verbose GC 日志能够洞察垃圾收集器的操作,并为策略和参数选择提供提示。IBM Developer Kit 的最新版本提供了详细的 XML 格式的日志。这些日志非常详细地展示了垃圾收集器所做过的事情。verbose GC 日志包括堆的大小,gencon 策略中的堆划分信息、停顿时间、对象结束和清除的引用。可以使用以下两个命令行选项之一来支持 verbose GC:-verbose:gc 或 -Xverbosegclog:文件名
。
有多种原因使您想要支持 verbose GC 和检查日志。您的应用程序可能经历了长时间的停顿或长的无响应时间,您想要从导致停顿的原因中确定或排除垃圾收集。另外,如果您将应用程序性能调整到最大,在 verbose GC 中可能会有与能够提高性能的更改相关的线索。
清单 1 显示了 optthruput 策略中收集的 verbose GC 部分示例:
清单 1. 一个收集的 verbose GC 示例
<af type="tenured" id="1" timestamp="Sun Mar 12 19:12:55 2006" intervalms="0.000">
<minimum requested_bytes="16" />
<time exclusiveaccessms="0.025" />
<tenured freebytes="23592960" totalbytes="471859200" percent="5" >
<soa freebytes="0" totalbytes="448266240" percent="0" />
<loa freebytes="23592960" totalbytes="23592960" percent="100" />
</tenured>
<gc type="global" id="3" totalid="3" intervalms="11620.259">
<refs_cleared soft="0" weak="72" phantom="0" />
<finalization objectsqueued="9" />
<timesms mark="74.734" sweep="7.611" compact="0.000" total="82.420" />
<tenured freebytes="409273392" totalbytes="471859200" percent="86" >
<soa freebytes="385680432" totalbytes="448266240" percent="86" />
<loa freebytes="23592960" totalbytes="23592960" percent="100" />
</tenured>
</gc>
<tenured freebytes="409272720" totalbytes="471859200" percent="86" >
<soa freebytes="385679760" totalbytes="448266240" percent="86" />
<loa freebytes="23592960" totalbytes="23592960" percent="100" />
</tenured>
<time totalms="83.227" />
</af>
|
开始的 <af> 元素展示了在此案例中触发收集的事件:一个分配失败。其他可能性包括针对并发收集的 <con> 或针对由 System.gc() 强制执行的收集的 <sys>。并发收集只发生在 optavgpause 或 gencon 策略中。不建议强制执行垃圾收集,因此如果 verbose GC 中存在 <sys> 元素,则应考虑重写应用程序以避免影响到垃圾收集例程,或者使用 -Xdisableexplicitgc 命令行参数禁用对垃圾收集的显式调用。
最有趣的元素可能是描述堆占用率的 <tenured> 元素的三个副本:
<tenured freebytes="23592960" totalbytes="471859200" percent="5" >
<soa freebytes="0" totalbytes="448266240" percent="0" />
<loa freebytes="23592960" totalbytes="23592960" percent="100" />
</tenured>
|
这些元素有三个副本,用于显示三个重要时间点上堆的状态。第一个副本显示收集之前堆的状态。第二个副本嵌套在 <gc> 元素中,表示收集后的堆。这个副本显示收集时应用程序中的活动数据量。最后一个副本显示满足触发分配的请求之后可用堆的数量。这个数量与收集后可用的自由空间数量的差额可能大于实际请求量。内存管理器以块的形式将内存分配给线程,以最小化堆锁定争用。
如果在收集了堆以后,占用率依然很高,那么最大的堆可能太小。可以使用 -Xmx 命令行选项将其扩大。
嵌套的 <loa> 和 <soa> 元素分别描述大和小对象区域使用的堆。大对象区域是堆中保留给大对象分配的一小片区域,而小对象区域就是 “正常的” 堆。所有对象最初都分配给小对象区域,但是如果小区域已经分配完了,则将大于 64KB 的对象分配给大对象区域。如果应用程序不需要大对象区域(即,如果应用程序不分配任何大对象),内存管理例程会快速将大对象区域缩减为空,这样,整个堆都可以供 “正常的” 分配使用。
下面这行代码展示了触发分配失败的分配请求大小:
<minimum requested_bytes="16" /> |
如果小对象区域中的可用自由空间量远远大于分配请求大小,但是还无法满足请求,则表明堆中都是碎片。考虑以很小的堆开始或使用 gencon 策略。小的初始堆通过鼓励收集器尽早执行紧凑排列来降低碎片率。当堆很小时,紧凑排列成本很低。gencon 策略使用一个复制收集器有效地紧凑排列会给每个收集带来负作用的婴儿(nursery)区域,从而避免碎片的产生。
使用 gencon 策略收集的日志也为婴儿 区域留有一行。婴儿区域是堆中保存近期分配的所有对象的区域。
<nursery freebytes="0" totalbytes="33554432" percent="0" /> |
如果应用程序的内存需求相对稳定,则可以考虑将堆大小的最大值和最小值设置为相同值。这可以避免垃圾收集器对堆进行不必要的收缩,从而改善应用程序性能。使用日志中的堆占用率来决定堆的大小。固定大小的堆不得小于应用程序的最大内存使用量,以避免出现内存不足的错误。如果堆和应用程序的内存使用量很接近,那么垃圾收集器必须非常频繁地收集堆,这会导致性能损失。
没有魔法来实现理想的占用率,但是有一条不错的经验法则,那就是将堆大小设置为系统最大内存需求的 1.5 倍。一般而言,堆越大,应用程序的性能就越好,但是对于某些系统或某些类型的工作负载来说也会存在例外。也可以添加命令行选项 -Xmaxf=1 来禁止收缩堆。正如上面讨论的一样,以一个非常小的堆开始,然后允许垃圾收集器对堆进行扩展,这样会减少碎片并提高性能。因此,必须进行一定量的尝试和面对一些错误,才能决定默认设置、固定大小的堆、禁用收缩和小的初始堆等哪个最适合您的应用程序。
当 gencon 策略的主要假设(最近分配的大多数对象不会在多个垃圾收集中存活下来)成立时,它的工作效果最好。如果在收集之后婴儿区域仍然很满,那么可能有很多对象在收集中存活了下来。考虑调整婴儿区域的大小,以便在婴儿区域中的活动对象较少时执行收集,或者考虑另一个 GC 策略。
弱引用、软引用和虚引用允许灵活的缓存,能够改进应用程序的内存特性。然而,像很多东西一样,最好适当地使用它们。如果收集器每次收集都要清理数千个引用,那么停顿时间会受到影响。考虑您的应用程序是否真的需要大量引用。
<refs_cleared soft="4" weak="28" phantom="0" /> |
最好适当地使用引用,收尾器与之不同,除了清理本地资源,它在编写得很好的应用程序中基本没用。如果在 verbose GC 日志中看到等待收尾的对象,那么请尝试重写应用程序以消除收尾器的使用。
<finalization objectsqueued="5" /> |
GC 日志中的时间信息也很有趣。对于整体收集,每一个标志、扫描(sweep)和紧凑排列阶段的停顿时间都是以 <gc> 元素中的嵌套元素的形式提供的:
<timesms mark="74.734" sweep="7.611" compact="0.000" total="82.420" /> |
紧凑排列事件很少发生,因此紧凑排列时间几乎总是 0。如果发生了许多紧凑排列事件,则需要考虑扩大堆的最大值以使堆中有更多自由空间,收缩堆的最小值以降低早期紧凑排列的成本,或者采用 gencon 策略。扫描时间应该比标记时间短得多。如果扫描时间很长,则应考虑缩小堆的大小或者采用 gencon 策略。
每个收集元素的最后一个元素是 <time> 元素,该元素记录垃圾收集所用的时间:
<time totalms="83.227" > |
但是,这与应用程序性能和应用程序响应时间的关系没有您最初想象的那么大。应用程序负载、垃圾收集并发、位置和分配效率都可能影响应用程序性能。第一个案例 说明典型应用程序中的停顿时间、吞吐量和响应时间是如何相关的。
在这个案例中,我们用典型的三层客户机-服务器应用程序来研究垃圾收集策略的选择如何影响应用程序性能。
将工作推入应用程序的活动线程数量每隔两分钟增加一次。每个线程都有相关联的数据;因此,随着线程数量的增加,应用程序的内存消耗也会增加。应用程序在有四个处理器的计算机上运行,活动线程的数量在 1 到 8 之间变化。如果线程数多于处理器数,那么线程必须争用处理器时间,有些线程会一直处于等待状态,这样,应用程序的效率就降低了。这反映了应用程序的低吞吐量,高的平均响应时间和相当高的最大响应时间。图 1、2、3 和 4 展示了应用程序在 optthruput 和 optavgpause 策略下运行时的停顿时间、吞吐量和响应时间图。图的中间点表示系统超载的界限。
图 1 显示每次垃圾收集时应用程序的停顿时间,使用 verbose GC 日志的 <timesms mark="74.734" sweep="7.611" compact="0.000" total="82.420" /> 元素中给定的时间。与预期的一样,optthruput 停顿时间(蓝线)比 optavgpause 停顿时间(绿线)长。optthruput 停顿时间随着堆中的活数据量的增加而变长,而 optavgpause 停顿时间保持不变。
图 1. 停顿时间
图 2 显示了应用程序的吞吐量。与预期的一样,针对停顿时间的优化和针对吞吐量的优化各有利弊:optthruput 策略(蓝线)中的吞吐量比 optavgpause 策略(绿线)中的吞吐量稍高,无论应用程序的负载是多少。因此,如果吞吐量是最重要的考虑因素,则应该使用 optthruput 策略,而不是 optavgpause 策略。对任何应用程序来说,optthruput 策略的吞吐量都比 optavgpause 策略更好,原因在本系列文章的第 2 部分中讲述过(参见 参考资源,获取链接)。
图 2. 应用程序吞吐量
在此案例中,平均响应时间与吞吐量的变化趋势相同,optthruput 策略中的平均响应时间总是更好,正如图 3 所示。蓝线表示 optthruput 策略,绿线表示 optavgpause 策略。因此,如果平均响应时间是最重要的考虑因素,那么应该使用 optthruput 策略,而不是 optavgpause 策略,即使 optavgpause 策略中的停顿时间更小。
图 3. 平均响应时间
最后,图 4 显示了两种策略的最大响应时间。蓝线表示 optthruput 策略,绿线表示 optavgpause 策略。在系统欠载时,optavgpause 策略的最大响应时间更好。而在系统超载时,optthruput 策略会提供较快的响应时间。注意:当系统欠载时,最大响应时间接近于停顿时间。垃圾收集停顿时间是任何线程遇到的最显著的延迟。在此环境下,optavgpause 策略可以提供最好的最大响应时间。另一方面,当系统超载时,线程需要等待其他线程,就像等待垃圾收集一样,并且两种策略的最大响应时间都会急剧增加。因为 optthruput 策略中的吞吐量更高,系统中未在最前面的线程会为前面的线程让路并快速释放控制权,因此最大响应时间较好。
如果系统超载很严重,或者由于堆太小而引起频繁收集,那么 optthruput 策略可能不会提供较好的最大响应时间。如果垃圾收集运行频繁,那么未在队列前面的线程可能必须等待多次垃圾收集才能到达队列头部。这时使用 optthruput 策略可能会产生相当长的最大停顿时间,使用 optavgpause 策略可能会获得较短的最大停顿时间。
图 4. 最大响应时间
表 1 和 2 总结了两种策略的在轻负载和重负载情况下的停顿时间、响应时间和吞吐量:
表 1. 轻负载
| optavgpause | optthruput | |
|---|---|---|
| 停顿时间 | 12 ms | 101 ms |
| 最大响应时间 | 35 ms | 100 ms |
| 平均响应时间 | 0.069 ms | 0.063 ms |
| 吞吐量 | 22,300 个事务/秒 | 24,600 个事务/秒 |
表 2. 重负载
| optavgpause | optthruput | |
|---|---|---|
| 停顿时间 | 14 ms | 155 ms |
| 最大响应时间 | 724 ms | 593 ms |
| 平均响应时间 | 0.13 ms | 0.11 ms |
| 吞吐量 | 28,000 个事务/秒 | 31,900 个事务/秒 |
正如表中所示,如果吞吐量是最重要的考虑因素,那么应该使用 optthruput 策略。如果响应时间是最重要的考虑因素,选择就不是很明显了。当应用程序负载很轻时,使用 optavgpause 策略将垃圾收集停顿时间降到最低之后,可以获得最佳响应时间。然而,如果线程的数量多于处理器的数量,应用程序将会超载,并且使用 optthruput 策略会获得最佳响应时间。(当其他应用程序将有不同的超载条件。)
如果系统中存在正在排队等候的任务,那么系统的响应时间可能会受到排队时间的控制,而不是垃圾收集停顿时间。很多其他因素,包括 I/O 停顿时间、数据库等待时间、Web 服务响应时间、网络延迟和任何其他外部交互都可能影响响应时间。基于这个原因,停顿时间不一定能够很好地反映期望的响应时间。停顿时间将影响响应时间的程度也是由堆的大小和垃圾收集的频率决定的。在较小的堆中,当必须频繁收集垃圾时,停顿时间对应用程序响应时间的影响比在较大堆中不频繁收集时的影响更大。
此案例中未展示 gencon 策略,因为在此工作负载下,该策略提供的停顿时间比 optavgpause 停顿时间大,而吞吐量比 optthruput 吞吐量更小。因此,对于此工作负载,无论性能标准是什么,gencon 都不是一个好的选择。但是,对于其他工作负载,gencon 策略能够很好地结合低停顿时间和高吞吐量。对于一些事务型工作负载,gencon 可以提供比 optavgpause 策略更好的停顿时间和比 optthruput 策略更好的吞吐量。
第二个案例讨论了一个更复杂的应用程序,它具有更复杂的性能标准。该应用程序包含一个应用服务器和位于该服务器上的包含多个部分的 J2EE 应用程序。它可以从不同的机器上驱动。这个应用程序的重要性能需求是吞吐量,但是也存在最小响应时间标准。只有在给定时间内完成的事务才能包括在吞吐量统计中,因此性能指标是吞吐量和平均响应时间指标的有效组合。
图 5 显示了第二个应用程序的停顿时间图。绿线表示 gencon 策略,蓝线表示 optthruput 策略:
图 5. 停顿时间
应用程序运行几分钟后,一些长期对象将能够在足够多的收集中幸存下来,从而被提升到长存区域。这意味着它们不再需要在每个婴儿区域收集过程中进行处理,并且能够在每个婴儿区域收集中幸存下来的数据量更小。因为需要处理的对象更少,所以停顿时间也会降低。
当长存区域将空间消耗完,并且不需要对整个堆进行并发收集时,gencon 停顿时间会达到最大值。这个收集比正常的收集更慢,但是这类收集很少发生。应用程序运行时间为 10 或 15 分钟。长存的堆无需经常收集,通常对它进行并发收集。
图 6 展示了两种策略的性能分数。与 optthruput 策略(蓝线)相比,gencon policy(绿线)具有 4% 的优势。
图 6. 吞吐量
表 3 总结了此应用程序的停顿时间和吞吐量结果。在这两种衡量标准上,gencon 策略优于 optthruput 策略。这有两个原因。很明显,第一个原因是性能指标要同时考虑响应时间和吞吐量,gencon 策略结合了好的吞吐量和好的响应时间,这使此策略可能成为胜利者。第二个原因很难量化。在这个应用程序中,对象创建和灭亡的模式受益于 gencon 策略没有碎片和稀疏填充婴儿区域的迅速收集。其他具有类似性能标准和不同对象创建模式的应用程序可能使用 optthruput 策略会更好。
表 3. 第二个案例的响应时间和吞吐量
| optthruput | gencon | |
|---|---|---|
| 停顿时间 | 630 ms | 424 ms |
| 吞吐量(符合响应时间标准) | 10,100 个事务/秒 | 10,500 个事务/秒 |
优化应用程序性能的第一步是决定对您很重要的性能特征。您可能选择针对吞吐量进行调优,或响应时间,或二者的某种组合。在定义了目标之后,就可以开始度量应用程序性能了,使用不同策略进行实验,考虑 verbose GC 日志的提示。
正如上述两个案例所示,每个应用程序都是不同的,您需要进行一些不同的尝试才能找到最适合您的特定应用程序和系统堆大小、可选参数和垃圾收集策略组合。
我们希望本文和前一篇文章能够帮助您更好地理解 IBM SDK 中垃圾收集的工作原理。后续文章将讨论 Java 技术的 IBM 实现的其他方面,包括类共享和调试、监控,以及配置特性。
学习
- “垃圾收集策略,第 1 部分”(Mattias Persson,developerWorks,2006 年 5 月):本系列的第一部分介绍了不同的 GC 策略并探讨了它们的一般特性。
-
Diagnostics Guide:获取 verbose GC 的更多详细信息和在 Java 技术的 IBM 实现中调整垃圾收集集参数的说明(PDF 格式)。
-
developerWorks Java 技术专区:浏览所有 Java 内容。
- “优化 Java 垃圾收集的性能”(Sumit Chawla,developerWorks,2003 年 1 月):学习如何使用 Java 虚拟机的 IBM 实现检测和解决垃圾收集问题。
获得产品和技术
-
Java SDK:从此页下载 SDK for AIX, Linux, and z/OS,以及其他针对 Java 技术的 IBM Developer Kit。
-
针对 Eclipse 的 IBM 开发软件包:使用这个开箱即用的 Java 开发环境开发、测试和运行 Java 应用程序。
-
WebSphere Everyplace Micro Environment:这是一个经过 J2ME 规范测试和认证的随时可投入生产的运行时环境。
-
针对 Apache Harmony 的 IBM 开发软件包:该执行环境设计用于运行 Apache Harmony 项目代码。
讨论
- 参与论坛讨论。
-
IBM SDKs and Runtimes:该讨论论坛由系列负责人 Chris Bailey 主持,访问该论坛,了解关于 Java 平台 IBM Developer Kit 的相关问题。

