Garbage collection (GC) 是 Java Virtual Machine (JVM) 的必要组成部分,它收集没有使用的 Java 堆内存,以便应用程序可以继续分配新的对象。GC 的效果和性能对于应用程序性能和确定 (determinism) 非常重要。IBM WebSphere Application Server V8 附带的 IBM JVM(在受支持的平台上)提供 4 种 GC 策略算法:
-Xgcpolicy:optthruput
-Xgcpolicy:optavgpause
-Xgcpolicy:gencon
-Xgcpolicy:balanced
每种算法都提供不同的性能和决定质量。此外,WebSphere Application Server
V8 中的默认策略已从 -Xgcpolicy:optthruput
更改为 -Xgcpolicy:gencon
策略。下面我们逐一检查这些策略,看看这个默认策略更改对它们有何影响。
不同应用程序自然有不同的内存使用模式。计算密集型数字处理工作负载使用 Java 堆 (heap) 的方式不同于面向客户的高度事务型接口。要以最佳方式处理这些不同种类的工作负载,则需要使用不同的垃圾收集策略。IBM JVM 支持许多垃圾收集策略,它允许您选择最适合您的应用程序的策略。
最简单的垃圾收集技术可能是:持续分配直到耗尽闲置内存,然后停止应用程序,处理整个堆。尽管这种技术可能会生成一个非常有效的垃圾收集器,但这意味着用户程序必须能容忍收集器带来的暂停。只关注总流量的工作负载可能会从这种策略中受益。
optthruput 策略 (-Xgcpolicy:optthruput
) 采用的就是这种策略(参见图 1)。这个收集器)使用一种平行的 “标记-清扫 (mark-sweep)” 算法。简言之,这意味着收集器首先逐一访问可访问的对象,将它们标记为实时数据。然后,第二轮访问扫除未标记的对象,将未使用的闲置内存留作新分配之用。大部分这种工作都可以并行完成,因此收集器可以使用额外的线程(默认情况下使用的最大线程数为 CPU 的数量)来加快工作速度,减少应用程序的暂停时间。
图 1. 应用程序和收集器 CPU 使用情况:optthruput

“标记-清扫” 算法的问题是可能会导致碎片(fragmentation),如图 2 所示。尽管可能有大量闲置内存,但如果它们只是一些小块,其间夹杂着活动对象,那么可能没有哪个碎块大到足以满足某个特定分配需求。
这个问题的解决方法是压缩(compaction)。理论上,压缩程序会将所有活动对象都移动到堆的一端,留下一块连续的闲置空间。这是一项昂贵的操作,因为可能会移动每个活动对象,每个经过移动的对象的指针都必须更新为新位置。因此,通常只在万不得已时才进行压缩。压缩也可以并行执行,但这会降低活动对象的打包效果,可能会生成几个较小的闲置空间,而不是一整块闲置空间。
图 2. 堆碎片

对于愿意损失部分流量、减少暂停时间的应用程序而言,可以选择另一种策略。optavgpause 策略(-Xgcpolicy:optavgpause
)试图在停止应用程序之前尽可能多完成一些 GC 工作,从而缩短暂停时间(参见图 3)。这种策略也使用 “标记-清扫-压缩 (mark-sweep-compact)” 收集器,但大部分标记和清扫工作可以在应用程序运行时执行。根据程序的分配速度,系统试图预测下次需要执行垃圾收集的时间。达到这个阈值时,就会启动一个并发 GC。当应用程序线程分配对象时,系统偶尔会要求它们在完成分配工作之前执行少量 GC 工作。线程执行的分配工作越多,要求它完成的 GC 工作也越多。与此同时,会有一个或多个背景 GC 线程使用闲置周期完成余下的工作。如果已经完成所有并发工作,或者闲置内存提前耗尽,则将中止应用程序并完成收集工作。这种暂停通常比较短,除非需要进行压缩。由于压缩需要移动和更新活动对象,因此不能并发执行。
图 3. 应用程序和收集器 CPU 使用情况:optavgpause

很久以前,人们就注意到,创建的大多数对象只被使用一小段时间。这是编程技术和应用程序类型所导致的结果。许多常用 Java 惯用语都会创建一些将迅速弃用的帮助程序 (helper) 对象,比如 StringBuffer/StringBuilder
对象和 Iterator 对象。可以分配这些对象来完成某个特定任务,任务完成后就很少会再用到这些对象。在更大的范围内,实际上事务型应用程序也常常创建一些 “一次性使用、用完作废” 的对象组。一旦返回数据库查询的响应之后,就不再需要回复、中间状态和查询本身。
这种发现导致了分代(generational)垃圾收集器的开发。其背后的理念是:将堆分割为多个不同区域,以不同的速度收集这些区域。新对象被分配到一个称为托儿所(nursery)(或新空间)的区域中。由于这个区域中的大多数对象很快都将变为垃圾,所以收集该区域最有利于恢复内存。如果某个对象可能会存活一段时间,则会将它移动到另一个称为保留区 (tenure)(或旧空间)的区域中。这些对象不太可能变为垃圾,因此收集器很少检查它们。对于适当的工作负载,进行垃圾收集的结果是:由于检查的内存更少,收集更快更有效;而且,经过检查的对象被回收的比例更高一些。收集更快意味着暂停时间更短,因此应用程序响应性也更好。
IBM 的 gencon 策略(-Xgcpolicy:gencon
)在上述并发策略之上提供了一个分代 GC( “gen-”)。保留区空间如上所述收集,而
托儿所空间使用了一个复制 (copying) 收集器。这种算法的工作方式是将托儿所区域进一步细分为分配 (allocate) 和幸存者 (survivor) 空间(参见图 4)。新对象被放置到分配空间中,直到耗尽其闲置空间。然后,应用程序会停止,分配空间中的所有活动对象都将复制到幸存者空间中。然后这两个空间交换角色:分配空间变为幸存者空间,幸存者空间变为分配空间,应用程序恢复运行。如果某个对象在几轮复制之后得以幸存,则会将它移动到保留区空间中。
图 4. gencon 应用

理论上,这意味着托儿所空间的一半(即幸存者空间)在任何时点上都未使用。实际上,预留为幸存者空间的内存量会根据在每次收集中幸存下来的对象的百分比进行实时调整。如果大多数新对象都被收集(这是预期的情况),那么分配空间和幸存者空间之间的分界线就会倾斜,此时需要增加垃圾收集之前可以分配的数据量。
这种风格的收集器有一个重大好处:通过在每次收集时移动活动对象,托儿所区域在每次收集时都被隐式压缩。这会导致闲置空间块变得尽可能的大,但也可能会将关系密切的对象(例如 String
及其 char[]
数据)移动到临近的内存位置。这有助于改进系统内存缓存的性能特征,从而提高应用程序本身的性能。
托儿所垃圾收集的成本与幸存的数据量有关(参见图 5)。由于预期的情况是多数对象都将是垃圾,因此一次托儿所收集通常导致很短的暂停。尽管应该能够快速收集多数对象,但有些对象无法收集。这意味着随着时间的推移,保留区区域中将塞满了长期存活的对象,最终导致需要对整个堆进行一次垃圾收集。上述并发收集器使用的大部分技术在这里仍然适用。保留区区域的标记将根据需要并发运行,而分配和收集是在托儿所区域中进行的。在 gencon 策略下,保留区区域的清扫不是并发执行的,而是作为保留区主收集的一部分进行的。
图 5. 应用程序和收集器 CPU 使用情况:gencon

WebSphere Application
Server V8 中添加了一个新的垃圾收集策略。这个策略名为 balanced(-Xgcpolicy:balanced
),它扩展了拥有多个不同的堆区域这个概念,将堆划分为大量区域,每个区域都可以单独处理。本系列第 2 部分将详细介绍基于区域的垃圾收集的基础知识,特别是将深入讨论 balanced 策略。
要调优任何应用程序的堆大小,第一步是使用默认堆设置运行应用程序,这允许您测量开箱即用性能。此时,如果堆闲置空间总是低于 40%,或者 GC 暂停高于总运行时间的 10%,就应该考虑增加堆大小。最小堆大小和最大堆大小可以分别通过 -Xms<value>
和 -Xmx<value>
修改。
用于垃圾收集的标记和清扫阶段的 GC 暂停时间基于堆上的活动对象的数量。当您增加统一工作负载上的堆大小时,标记和清扫阶段将继续花费大致相同的时间完成操作。因此,通过增加堆大小,可以增加 GC 暂停之间的间隔,从而为应用程序提供更多的执行时间。
如果 GC 由于碎片问题而执行压缩,那么增加堆大小可能有助于缓解压缩导致的长时暂停问题。压缩阶段可能会极大地增加 GC 暂停时间,因此,如果压缩阶段经常出现,那么调优堆设置就能改进应用程序性能。
使用可变大小堆允许 GC 仅对堆使用应用程序必需的 OS 资源。随着应用程序程序堆需求的变化,GC 可以通过扩大和收缩堆做出反应。GC 只能收缩从堆末尾开始的连续内存块,因此收缩堆可能需要进行压缩。实际的收缩和扩大阶段很快就能完成,不会明显增加 GC 暂停时间。通过将最大堆大小设置为略大于常规操作所需的大小,应用程序能够通过扩大堆来处理额外的工作负载。
堆需求不变的应用程序可以通过使用固定堆大小改进 GC 暂停时间。
调优分代垃圾收集时,最简单的方法是将托儿所空间视为非分代垃圾收集使用的 Java 堆区域之外的新 Java 堆区域。这样,非分代垃圾收集使用的 Java 堆就变成了保留区堆。
这种方法是一种保守方法:预期的情况是保留区堆的占用率将由于托儿所空间的引入而降低,但它提供了一个安全的起点,特别是从非分代策略迁移时。当可以监控全局(完全)收集之后的保留区堆的占用率时,就可以按照 前面 描述的方法来调整堆大小:
-Xmn<size>
设置托儿所区域的初始和最大大小,有效地设置-Xmns
和-Xmnx
。-Xmns<size>
将托儿所区域的初始大小设置为指定的值。-Xmnx<size>
将托儿所区域的最大大小设置为指定的值。
托儿所堆大小应该是固定的,因此只需要这些选项中的一个:-Xmn
。因此,您只需理解如何正确设置托儿所堆大小。
要正确设置托儿所堆大小,首先需要考虑托儿所收集使用的机制,然后考虑随之出现的二级特征:
- 托儿所收集的工作方式是将数据从分配空间复制到幸存者空间。复制数据是一个比较昂贵耗时的任务。因此,托儿所收集所花费的时间由需要复制的数据量决定。这不是说要复制的对象的数量与托儿所空间自身的大小没有影响,而是说与复制实际数据的成本相比,这些因素造成的影响相对较小。因此,托儿所收集所花费的时间与需要复制的数据量成正比。
- 在任何给定的收集中,只有有限和固定的数据量是 “实时的”。一旦应用程序完成启动并完全填充其缓存后,托儿所堆中需要复制的 “实时” 数据量就由该时点需要完成的工作量来确定。在处理事务的系统中,需要复制的实时数据量等于某个实时事务集。例如,如果您使用支持 50 个并发事务发生的 50 个 WebContainer 线程来配置您的应用服务器,那么实时数据量就是与那 50 个事务关联的数据量。
这意味着,托儿所收集所需的时间由收集时发生的并发事务的数量的关联数据的大小决定,而不是由托儿所空间的大小决定。这还意味着,随着托儿所空间的大小增大,托儿所收集之间的间隔时间会随之增大,但收集所需的时间不会增加。事实上,随着托儿所空间增大,垃圾收集所需的总时间会随之降低。
图 6 显示,如果托儿所空间的大小低于事务集的关联实时数据的大小,因此托儿所收集之间的时间间隔低于一个事务,则必须多次复制这些数据。
图 6. 数据复制的平均次数与托儿所收集之间的时间间隔

随着托儿所空间大小和托儿所收集之间的时间间隔增加,需要复制的数据量通常会随之减少,垃圾收集的开销也会随之降低。
IBM 垃圾收集器或 JVM 没有对托儿所堆大小进行直接限制;事实上,托儿所堆大小有时被设置为 10 GB 甚至 100 GB。但是,操作系统在 Java 进程使用的虚拟内存、进程地址空间以及足够的物理内存(RAM)的可用性方面有一些限制。一个 32 位进程在每个平台上的操作系统限制如图 7 所示。
图 7. 按操作系统列示的 32 位地址空间

对 64 位进程的限制要严格得多。由于可寻址内存的范围从数百到数十亿 GB,可用物理内存 (RAM) 的限制变得更加重要。
如上所述,最简单的方法是将托儿所空间视为一个额外的内存空间。但是,托儿所堆和保留区堆实际上都被分配为单个连续内存段,它们的大小可通过 -Xmx
设置进行控制。如果只使用 -Xmx
设置,则 -Xmx
值的 25% 用于最大托儿所堆大小,托儿所堆大小允许在那 25% 之内进行伸缩。下面提供了 Java 堆布局,如图 8 所示。
图 8. 默认堆布局

但是,您应该将托儿所堆大小固定为一个较大的值,以最小化垃圾收集花费的时间,增强保留区堆,使其根据占用率重置自身大小,从而提高弹性。因此,首选的 Java 堆布局如图 9 所示。
图 9. 推荐的堆布局

要实现这个布局,托儿所空间和保留区空间的最小和最大堆大小的值应该设置如下:各个托儿所空间大小的最大和最小值都相等,而各个保留区空间大小的最小和最大值各不相同。
例如,如果您想拥有一个 256MB 的托儿所堆大小,而保留区堆大小介于 756MB 和 1024MB 之间,则这些值应该为:
-Xmns256M
-Xmnx256M
-Xmos756M
-Xmox1024M
由于 WebSphere Application Server V8 中的默认 GC 策略已经从 optthruput 变为 gencon,因此需要调整以前选择的调优参数。主要问题是更改堆大小来补偿托儿所空间。以前在 optthruput 策略中,以 1G 堆大小(即 -Xmx1G
)运行正常的程序在使用 768M 保留区空间和 256M 托儿所空间运行时可能会出现问题。前面介绍的技术将有助于您选择新的堆参数。
还有一些不太明显的情况,gencon 可能会表现出不同的行为。
由于类通常是长期存活的对象,因此可以将它们直接分配到保留区空间。因此,类卸载只能是保留区收集的一部分。如果应用程序非常依赖短期存活的类加载器,且托儿所收集能够及时处理其他已分配对象,那么保留区收集可能不会频繁发生。这意味着,类和类加载器的数量将持续增长,这可能会增加本机内存上的压力,而且会在需要进行保留区收集时导致收集时间过长,这是因为有太多的类卸载工作需要完成。
如果出现这个问题,有两种解决方法。第一种方法是在出现大量类加载器时鼓励进行额外的保留区收集。命令行选项 -Xgc:classUnloadingKickoffThreshold=<number>
告知系统,每当新创建的类加载器达到 <number>
时,就会启动一次并发保留区收集。因此,如果指定 -Xgc:classUnloadingKickoffThreshold=100
,那么托儿所收集会仔细观察,每当自上次保留区收集以来新创建的类加载器数量达到 100 时,就会启动一次并发保留区收集。第二种方法是改为使用另一种 GC 策略。
类似的问题可能会出现在引用对象(例如 java.lang.ref.Reference
的子类)和使用 finalize()
方法的对象上。如果这两种对象存活的时间足够长,在变得无法访问之前被移动到保留区空间中,那么可能会经过很长时间以后才会运行保留区收集,“发现” 这个对象已经死亡。如果这些对象占用着大型或稀有本机资源,就有可能导致出现问题。我们将这种对象戏称为 “冰山” 对象:表面上它们只占用很小的 Java 堆大小,但下面隐藏着巨大的本机资源,垃圾收集器看不到。就像面对真正的冰山一样,最好的策略是尽可能远离它们。即使使用其他 GC 策略,也无法保证能够探测到可终结的对象,并及时运行它的终止程序 (finalizer)。如果它们占用稀有资源,尽可能地手动释放它们可能是最佳策略。
默认策略应该能为多数工作负载提供足够的性能,但它可能不是某个特殊应用程序的理想选择。
类似 “批作业” 的应用程序初始化状态并加载要处理的数据。这些对象中的大部分将在作业期间存活,只有少数几个额外对象是在作业运行时创建的。 这种工作负载适合 optthruput 模式,因为预期的情况是:在任务完成之前,几乎没有垃圾。另一种类似情况是,如果作业很快完成或只分配很少的对象,那么只要堆大小适当,作业运行就不需要垃圾收集。在上述这些情况下,optthruput 收集器的最小开销会让您做出最佳选择。
相比之下,事务型应用程序不断创建并启用对象组。在这个上下文中,术语 “事务” 主要使用字面意义(比如数据库更新或电子商务采购)或者采用更宽泛的意义,比如一项独立工作。举例来说,服务一个 Web 页可以视为一个事务。客户机提交一个 URL,服务器计算页面内容并将其发送给客户机。一旦客户机接收到页面,服务器就可以丢弃已计算的数据。
为了进一步阐述这个定义,我们来看一个标准用户界面。用户单击 Save 按钮之后,系统会打开一个文件对话框,以便用户导航文件系统,为文档选择一个位置。一旦用户关闭对话框,就不再需要所有中间状态。实质上,有些批作业甚至也是事务型的。假设一个任务正在为一些大型图像文件创建缩略图,那么该作业看起来似乎是一个大型批作业,但在内部,作业分别处理每个图像,每个处理都形成一个 “事务”。对于这类工作负载,gencon 模式应该能提供好处。
optavgpause 模式介于二者之间。这种模式适用的应用程序的特点是:拥有大量长期存活的数据,这些数据随着程序运行缓慢更改。对于工作负载而言,这种模式比较少见。通常,长期存活的数据要么从不更改,要么频繁更改。也就是说,如果系统拥有缓慢演变的数据,且这些数据创建的中间对象也不多,那么这种系统就适合使用这个策略。由于前面讨论的某种原因而不能在 gencon 策略下有效运行的程序可能会受益于 optavgpause 的并发性。
本文简要描述了 WebSphere Application Server V8 中的 Java Virtual Machine 中提供的垃圾收集策略。尽管默认设置应该适用于多数情况,但是,为了获得最佳性能,可能需要进行一些调优。通过匹配工作负载类型及其使用的 GC 策略并选择适当的堆参数,应该能够减少垃圾收集对应用程序的影响。
本系列第 2 部分将介绍一种基于区域的新的垃圾收集策略 balanced,该策略旨在在 64 位大型多核系统上部署时提高可伸缩性。下一篇文章会涉及到这种新技术背后的动机、它提供的性能改进以及关于调优这个新选项的提示和技巧。
学习
- Wikipedia: 垃圾收集定义
-
Java 技术,IBM 风格: 垃圾收集策略,第 1 部分
-
Java 技术,IBM 风格: 垃圾收集策略,第 2 部分
-
IBM developerWorks WebSphere
获得产品和技术
-
下载 IBM SDKs for Java
-
IBM Monitoring and Diagnostic Tools for Java - Health Center
-
IBM Monitoring and Diagnostic Tools for Java - Garbage Collection and Memory Vizualiser
- IBM developerWorks 工具包:下载关键 WebSphere 最新的产品工具包。
讨论
- 加入 developerWorks 中文社区,developerWorks 社区是一个面向全球 IT 专业人员,可以提供博客、书签、wiki、群组、联系、共享和协作等社区功能的专业社交网络社区。
- 加入 IBM 软件下载与技术交流群组,参与在线交流。

Chris Bailey 是位于英国的 Hursley Park Development Lab 的 IBM Java Technology Center 团队成员。作为 IBM Java 服务和支持组织的技术架构师,他负责支持 IBM SDK for Java 用户交付成功的应用程序部署。Chris 还参与收集和评估新需求,交付新调试功能和工具,改进文档并提高 IBM SDK for Java 的质量。