简介
在 Java™ 虚拟机 (JVM) 中,垃圾收集器减轻了应用程序开发人员的内存管理负担。垃圾收集 (GC) 是一个自动化系统,同时处理 Java 对象的内存分配和回收。这有助于降低应用程序开发的复杂性,但代价是可能在应用程序的整个生命周期中表现为不均衡的性能,以及可能影响应用程序响应能力的太长的暂停。
本系列的 第 1 部分 介绍了 IBM WebSphere Application Server V8 中可用的垃圾收集策略,以及配置新的默认分代策略的信息。本文探讨平衡垃圾收集策略,一种 WebSphere Application Server V8 中提供的新的 GC 技术,可通过命令行选项-Xgcpolicy:balanced
使用。平衡收集器旨在均衡暂停时间,减少一些通常与垃圾收集相关联的高成本操作的开销,如图 1 所示。
图 1.平衡收集器的暂停时间目标
垃圾收集成本可归因于许多因素。一些最重要的因素包括:
- 活动数据总大小:随着系统中活动(即可通过一系列对象引用供应用程序访问的)对象的数量和大小增加,跟踪或发现这些活动对象的成本也会增加。
- 堆碎片化:随着对象变得可回收,必须管理关联的内存来满足分配需求。与跟踪此内存,使它可用于分配以及重新定位活动对象以合并空闲内存相关的成本可能很高。
- 分配速率:分配对象的速度,以及它们的大小,将决定空闲内存的消耗率,进而决定垃圾收集的频率。具有高速分配能力的应用程序可能会更频繁地出现破坏性的 GC 暂停。
- 并行性:垃圾收集器可以使用多个线程来执行垃圾收集。随着更多线程并行完成垃圾收集,垃圾收集暂停时间减少。
最近的 Java 应用程序中的一种常见模式是使用堆中的数据存储。示例包括内存型数据库或低延迟缓存,常常可以在 NoSQL 解决方案中看到。相比所管理的大量内存,这些部署通常具有相对较少的可用核心数量。这些应用程序中的响应能力至关重要。较长的 GC 暂停时间可能中断数据网格系统中保持活动的检测信号,导致数据网格中的节点出现故障的错误结论,从而强制节点重启。这会导致糟糕的响应能力,浪费的带宽,因为网格节点要重新启动并重新填充;还增加了仍然活动的节点的压力,因为它们需要尽力处理增加的负载。
现有的垃圾收集方法使用强大的技术来有效解决如今的许多 GC 暂停时间问题。但是,不断增加的堆大小和数据模式的变更已开始暴露这些传统模式中的不足。
传统的垃圾收集
存在多种现有的垃圾收集解决方案。较流行的两种是:
整堆收集:在此方法中,JVM 通常会等到堆使用完成,然后对整个堆执行垃圾收集。主要成本与活动的应用程序集的大小成正比。其他成本可能还包括其他全局操作,比如为了整理堆碎片而执行的整堆压缩。
分代收集:“对象夭折” 的假设表明,在典型的应用程序中,大部分分配的对象拥有极短的寿命,可在分配后不久即收集。分代收集器通过在堆中指定用于对象分配并由特殊的收集器收集的区域(通常称为新空间)来利用此假设。这有望实现最佳的时间和精力投资回报,减少与整堆收集相关的总暂停时间。最终,当对象存活的时间长到足够离开新空间时,堆的剩余部分(称为旧空间)会填满,并必须使用整堆收集器来收集。
图 2. 新空间收集区域和大小与整堆(全局)收集对比
有其他许多技术可帮助减轻垃圾收集中的暂停时间压力,包括:
- 并行性:在多核系统上使用多个 GC 线程,以更快完成 GC 操作,减少暂停时间。
- 并发性:通过一组专用的 GC 帮助器线程来以 Java 线程形式执行 GC 操作,创建 Java 线程来帮助完成工作。
- 增量收集:通过将工作分散到多个更短的暂停时间内,减少平均 GC 暂停时间,最终完成整个 GC。此方法通常以更短的平均暂停时间来换取较长的总暂停时间,因为增量化操作所需的处理工作也具有开销。
本系列第 1 部分 中已经提到,有许多强大的 GC 技术可供应用程序开发人员使用,具体取决于特定部署所需的性能特征。
平衡垃圾收集器的目标
平衡收集器的主要目标是将全局垃圾收集的成本分担在许多 GC 暂停上,减少整堆收集时间的影响。与此同时,每次暂停应该尝试执行一次独立的收集,将空闲内存返回到应用程序供直接重用。
为了实现此目的,平衡收集器使用一种动态方法来选择要收集的堆区域,以最大化时间和精力的投资回报。这类似于 gencon 策略方法,但更加灵活,因为它会在每次暂停期间考虑对堆的所有部分进行收集,而不是仅考虑静态定义的新空间。
图 3. 动态选择堆区域进行收集的平衡能力
通过删除对要收集的堆区域的限制,平衡收集器能够动态适应广泛的对象使用模式。例如,在 3 分钟后具有高对象死亡率的应用程序、每次事务要分配 100MB 的应用程序,或者在某些对象生命周期内容易产生碎片的应用程序,都可通过在合适的时刻专注于堆的合适区域来解决。
平衡收集器会基于所生成的工作量,在 GC 操作之间均匀分配暂停时间。这可能受到对象分配速率、对象存活率和堆内的碎片级别的影响。请注意,这种暂停时间的平均化只是一种尽力而为,而不是一种实时保证。暂停时间无法保证在某个最大值之内,该技术也无法提供利用率保证。
平衡收集器基于 IBM JDK 内现有的 GC 技术的功能,包括 optavgpause、gencon 和 metronome。本文的其余部分将介绍平衡收集器所采取的一般垃圾收集方法,用于实现这些目标的一些重要的技术,应该使用平衡收集器的场景,以及关于调节收集器以实现最佳结果的建议。
平衡收集器工作原理
本节介绍平衡收集器的操作,首先解释如何组织堆,然后介绍平衡收集器用于收集堆和将空闲内存返回给应用程序的技术。
堆的组织
平衡收集器架构的一个基本方面(也是实现其减少较长收集时间影响的目标的关键)是,它是一种基于区域的垃圾收集器。区域是 Java 对象堆的一个具有明确界限的部分,它对相关内存的使用进行分类并将相关对象分组到一起。在 JVM 启动期间,垃圾收集器将堆内存分解为相等大小的区域,这些区域界线在 JVM 的生命周期内保持静态。
区域是基本的垃圾收集和分配操作单元。例如,当堆被展开或收缩时,分配或释放的内存将与区域数量相对应。尽管 Java 堆是一个连续的内存地址范围,但该范围内的任何区域都可根据需要分配或释放。这使平衡收集器可比其他垃圾收集器更加动态和积极地收缩堆,这通常需要分配的堆是连续的。
还需要注意,GC 操作(比如跟踪活动对象或压缩)在一组区域上操作。因为堆分区为定义良好的区域,所以当平衡收集器在不同时间分析可用的堆数据时,收集操作可能在不同的区域集上操作。
单个区域中的对象具有某些共同特征,比如具有类似的年龄(图 4)。具体来讲,新分配的对象(自上一个 GC 周期后分配的对象)都集中在一个所谓的 eden 区域中。eden 区域很值得注意,因为它们始终包含在下一个收集周期中。
区域具有最大对象大小限制。对象始终分配在单个区域界限内。无法放在单个区域内的数组使用一种称为 arraylet 的不连续格式来表示,稍后将介绍该格式。除了 arraylet,对象从不允许跨越区域。
区域大小始终为 2 的幂次(例如 512KB、1MB、2MB、4MB 等)。区域大小在启动时选择,基于最大堆大小。收集器选择 2 的最小幂次,这将产生不到 2048 个区域,最小的区域大小为 512KB。除了较小的堆(小于 512MB),JVM 可以拥有 1024 到 2047 个区域。
图 4. 对象堆中存在的区域结构和特征
收集堆
就像 gencon 收集器一样,平衡收集器利用了最新收集的对象可能快速变成垃圾的观察结果。它通过一个完全中断的周期来查找新创建的对象,意味着在 GC 处理的过程中,所有 Java 线程都会暂停执行。一种更加全局化的操作支持此方法,以帮助处理一组 eden 区域(始终会收集)外的对象。
有 3 种不同类型的垃圾收集周期(图 5):
- 部分垃圾收集 (PGC) 收集一个称为收集组的区域子集。PGC 是在使用平衡收集器时最常见的 GC 周期。它用于从具有高死亡率的区域收集垃圾,收集 eden 集外的区域并对其进行碎片整理。
- 全局标记阶段 (GMP) 增量式地跟踪堆中的所有活动对象。GMP 是常见的操作,但通常没有 PGC 常见。GMP 周期不负责将空闲内存返回到堆或压缩碎片化的堆区域(这是 PGC 的职责)。GMP 的作用主要是为 PGC 提供支持,细化用于确定堆的哪些区域最适合不同的收集操作的信息。
- 全局垃圾收集 (GGC) 以完全中断的方式标记、擦除并压缩整个堆。GGC 通常仅用在应用程序通过调用
System.gc()
来显式调用收集器时。它也可以在内存严重不足时执行,作为在抛出OutOfMemoryError
之前释放内存的最后补救办法。平衡收集器的一个主要目标是避免全局垃圾收集,除非显式调用,这些全局垃圾收集应被视为在调节平衡收集器时发生的问题。
认识到 PGC 是独立的完全中断性操作之后,GMP 周期可跨越多个增量,在 Java 应用程序运行期间部分并发执行。每个 PGC 周期的目标是回收内存,它会选择堆中要收集的区域,执行收集,将最终的空闲内存返回到应用程序做分配之用。图 5 显示了一个典型的时间线,描述了平衡收集器的操作模式。
图 5. 典型的平衡收集器行为的时间线表示
理解部分垃圾收集的作用
每个部分垃圾收集 (PGC) 负责确保有足够的空闲内存使应用程序继续运行。为此,每个 PGC 选择将一些区域包含在收集组中。可通过 3 个主要因素来决定一个区域是否属于收集组(图 6):
- Eden 区域(包含自上一次 PGC 之后分配的对象)始终包含在内,这主要是因为它们通常具有较高的死亡率。这还使 GC 能够分析和记录与这些对象的引用图和活跃度统计相关的元数据。
- GMP 阶段发现的区域,它们非常零散,且在分配、压缩和合并时有助于将空闲内存返回给应用程序。这称为碎片整理。要创建空闲区域来用作 eden 区域,离不开持续碎片整理。
- eden 区域集外的区域,它们应该具有高死亡率。GC 以对象年龄的函数的形式,收集对象平均死亡率的统计数据。基于这些统计数据,每个 PGC 动态选择一些区域,它期望在这些区域中找到对于发现活动对象所需的工作量而言足够的垃圾(进而释放足够的空闲内存)。
图 6. 对象堆中按区域选择的收集组
一个 PGC 通常采用一个类似于 gencon 策略所使用的收集器的复制收集器。这种复制方法要求 PGC 保留一定量的空闲区域,作为从收集组撤离的活动对象的目的地。与 gencon 不同,平衡收集器不为存活的对象预先分配内存。相反,它估计预期的存活率并使用堆中一个足够大的空闲区域子集,以便成功复制来自收集组的所有活动对象。
在具有较高的分配易变性和死亡率的应用程序工作负载中,预计的存活对象区域大小可能比实际可用的区域更大。在这种情况下,PGC 将从一种复制机制切换到一种原地跟踪并压缩的方法。压缩周期使用滑动压缩,这可以保证成功地完成操作,而无需任何空闲的堆内存,不同于首选的复制方法。
除了在 PGC 之间切换收集模式,平衡收集器还能够在 PGC 期间切换模式。如果评估证明存活对象内存需求不够用,复制收集器会填充剩余的空闲空间,然后动态过渡到原地方法。在过渡之前复制的任何对象仍然被复制,没有成功复制的对象将在原地收集。
在所有情况下,PGC 内的所有操作都是完全中断性的 (STW)。这意味着在 GC 周期完成之前所有 Java 线程都会暂停执行。
理解全局标记阶段的作用
PGC 可以恢复较高比例的垃圾,而通过战略性收集组决策实现相对较低的暂停时间。PGC 没有堆生命迹象的全局知识,它用于做决定的数据将逐渐变得越来越不可靠。全局标记阶段 (GMP) 负责刷新整个堆的视图,使 PGC 能够制定关于收集组选择和跟上应用程序的堆使用率所需的工作的更精明决策。
GMP 在 PGC 无法及时处理拒绝进入系统的活动数据时被激活。PGC 没有发现的垃圾会缓慢积累,堆将逐渐填满。当 PGC 的效率恶化时,GMP 周期就会启动。GMP 通过并行 STW 增量和并发处理的结合来在堆中标记所有活动对象。
为了确定何时应该发起 GMP 周期,GC 预测堆被消耗的速率、全局活动对象集的大小,以及跟踪活动对象集的成本。基于此信息,GC 计划初始启动点、完成 GMP 所需的增量数,以及每个增量需要完成的工作量。此计划旨在:
- 最小化堆应用程序的影响。
- 在堆完全耗尽空闲内存之前完成 GMP。(更准确来讲,要完成该任务,需要有足够的空闲空间供 PGC 用于有效完成增量。)
GMP 增量大概在 PGC 周期中间安排。此外,如果在 PGC 周期结束时存在可用的处理器时间(比如空闲的核心),将在应用程序执行期间分配线程来帮助并发地完成 GMP 增量。如果并发线程能够在其计划的时刻之前完成下一个 GMP 增量,则不会执行额外的工作。
幕后:帮助引发这一切的关键机制
现在您已理解了平衡收集器所使用的核心基础架构和方法,您应该知道支持该收集器并帮助它实现其目标的两种重要机制:记忆集合和 arraylet。
记忆集合
PGC 要能够准确发现所有活动的对象,收集器必须扫描所有对象 root(比如线程堆栈、永久类加载器、JNI 引用)。此外,必须发现从收集组外部的对象对收集组内的对象的引用。这可以通过扫描未包含在收集组内的所有区域中的所有对象来完成,但这种方法可能效率很低。相反,不同区域中的对象之间的引用在一个称为 “记忆集合” 的数据结构中得到跟踪和记录。记忆集合用于按区域跟踪所有传入的引用(图 7)。
图 7. 记忆集合的基本结构
引用在程序执行期间通过一个写屏障(write barrier)来创建和发现。此过程由 JVM 处理,对于 Java 应用程序不可见,无需更改 Java 代码。
堆外内存保留给记忆集合存储,通常不超过当前堆大小的 4%。在每个区域所跟踪的传入引用的数量上也存在限制。如果达到了区域的全局限制 (4%) 或局部限制,那么堆区域的记忆集合的任何数据添加都将导致该区域被标记为 “popular”。这会使该区域无法被 PGC 收集。它可能再次变为下一个 GMP 周期后的收集候选者,这会从记忆集合删除过期信息并更新它。
Arraylet
大部分对象都可轻松包含在 512KB 的最小区域大小内。但是,一些大型数组可能需要比单个区域中的可用内存更多的内存。为了支持这种大型数组,平衡收集器为大型数组使用了一种 arraylet 表示形式。
图 8 表明大型数组对象显示为一个 spine,它是中央的对象和惟一可被堆上的其他对象引用的实体以及一系列 arraylet 叶,其中包含实际的数组元素:
图 8. arraylet 的基本结构
arraylet 叶不会直接由其他堆对象引用,可按任何顺序分散在堆中。每个叶子是一个完整区域,允许对元素位置执行简单的计算,需要一种额外的媒介才能到达任何元素。如图 8 所示,由于 spine 内部的碎片而导致的内存使用开销已得到优化,方法是将最终叶子的任何尾随数据包含在 spine 中。
因为数组表示被 JVM 隐藏,所以 arraylet 的形状所带来的明显复杂性对 Java 应用程序不可见。无需任何代码修改或 arraylet 知识。
使用 arraylet 有许多优势。由于堆会逐渐变成碎片,所以其他收集器策略可能会强制运行全局垃圾收集和碎片整理(压缩)阶段,以便恢复足够的连续内存来分配大型数组。通过消除在连续内存内分配大型数组的需求,平衡垃圾收集器更容易满足这样的分配需求,而无需计划外的垃圾收集,而且无需全局碎片整理也能够满足此需求。此外,平衡收集器从不需要在分配了 arraylet 叶之后移动它。重新定位数组的成本仅限于重新定位 spine 的成本,所以大型数组不会导致更长的碎片整理时间。
图 9. 在碎片化的堆中将数组分配为 arraylet
arraylet 表示形式仅用于非常大的数组。小数组在平衡收集器中具有与其他 IBM 垃圾收集器(比如 gencon 收集器)相同的表示形式。对于小数组,没有额外的空间开销。但是,由于 JIT 编译的代码在大部分情况下都需要同时包含小型和大型数组的逻辑,所以 arraylet 的使用可能导致更多的已编译代码。
使用 arraylet 的最明显后果可以在 Java 本地接口 (JNI) 代码中看到。JNI 提供了一些 API,包括 GetPrimitiveArrayCritical
、ReleasePrimitiveArrayCritical
、GetStringCritical
和ReleaseStringCritical
,只要可能,它们就会提供对数组数据的直接访问。平衡收集器提供了对连续数组的直接访问,但需要为不连续的 arraylet 将数组数据复制到连续的堆外内存块中,因为 arraylet 的表示与这些 API 所需的连续表示不一致。
如果您认为这会影响您的应用程序,还有其他一些可能的解决方案。首先,确定是否复制数组数据。前面提到的 JNI API 包含一个 isCopy
返回参数。如果 API 将此参数设置为JNI_TRUE
,那么数组将是不连续的,将会复制数据。检查您的 JNI 代码,确定相关的本地函数是否可使用 Java 重写,或者更改为使用不同的 API,例如Get<Type>ArrayRegion
。如果数据未修改,确保 ReleasePrimitiveArrayCritical
的任何调用方使用了JNI_ABORT
模式,因为这样会消除将数据复制回 Java 堆的需要。最后,较大的区域大小(由增长的堆大小控制)可以减少或消除 arraylet。
调节平衡收集器
平衡收集器使用一种与 gencon 收集器(如 第 1 部分 中所述)类似的收集方法,所以许多相同的技术也适用于它。但是,我们有必要列出需要考虑的一些区别:
- eden 空间的平衡表示法类似于 gencon 新空间,但不相同。eden 空间包含收集流程总会涉及到的新创建的对象,但它们在收集之后就会成为一般堆的一部分。相反,在转移到旧空间之前,对象可在多次收集中保留在 gencon 新空间中。
- 尽管存在全局跟踪阶段的平衡表示法,但平衡收集器的一个目标是通过增量收集 eden 空间外的堆并对它执行碎片整理,完全避免定期全局收集(尤其是全局压缩)。这与 gencon 相反,后者仅关注新空间,最终会导致在旧空间耗尽时执行全局收集。
- 长期活动的对象(比如类和字符串常量)从不会分配到 gencon 的新空间中(它们直接分配给旧空间)。平衡收集器将这些对象分配到 eden 空间中,随后在收集周期中收集它们。
平衡收集器的基本调节选项与 gencon 相同,只是 eden 空间取代了可调节的新空间。这里回顾一下这些选项:
-Xmn<size>
设置 eden 空间的建议大小,有效地设置-Xmns
和-Xmnx
。-Xmns<size>
将 eden 空间的建议初始值设置为指定的值。-Xmnx<size>
将 eden 空间的建议最大值设置为指定的值。
请注意这些是建议的选项,只要系统能够适应任何限制,平衡收集器就会采用这些选项。例如,如果对于建议的 eden 大小没有足够的堆内存,平衡收集器会将 eden 空间减小为可获得的大小。默认的建议 eden 大小为当前堆大小的 1/4。
调节 den 空间的主要目标应该是包含分配给系统内一组事务的对象。因为大部分系统都会不断拥有许多事物(原因在于多线程处理),所以 eden 空间应该能够在任何时刻容纳所有这些事务。从调节的角度讲,这意味着常规负载下从系统中的 eden 空间存活的数据量应该比 eden 空间本身的实际大小小很多。一般来说,要实现最优性能,在每次收集中从 eden 空间存活的数据量应该保持到大约 20% 或更少。一些系统可能能够容忍超出这些边界的参数,具体取决于堆的总大小或系统中可用 GC 线程的数量。关键在于使用这些指南作为部署应用程序的起点。一般而言,用于 gencon 策略的任何 -Xmn
设置也都适用于平衡收集器。
尽管可以使用 -Xmn
自由调节 eden 空间的大小,但在这么做时需要记住一些要点:
- 保持 eden 较小:通过将 eden 空间缩小到最小值(基于事务大小和周转时间),您的系统可能比它需要的更频繁地暂停,从而降低了性能。另外,如果太频繁地调节 eden 大小,任何行为更改(例如,从突然出现故障的系统接管更多工作,以满足高可用性需求)将超出 eden 空间范围,导致欠佳的性能。
- 保持 eden 较大:通过将 eden 空间调大,可以减少收集暂停次数(潜在地提高性能),但会减少可用于一般堆的内存量。如果一般堆相对于总体活动集合而言太小,它可能强制平衡收集器在每个 PGC 周期中增量式地收集堆的较大部分并执行碎片整理,以保持正常运行,导致较长的 GC 暂停时间和欠佳的性能。
在所有情况下都需要权衡,而且调节过程是系统的和迭代式的:
- 根据需要调节堆最大值 (
-Xmx
) 和初始 (-Xms
) 内存。 - 在正常负载压力下运行应用程序。
- 收集冗长的 GC 日志(
-verbose:gc
、-Xverbosegclog:<filename>
)以供分析 - 如果有必要,使用合适的工具确定对 eden 大小的调节 (
-Xmn
)。 - 返回执行第 2 步,直到获得满意的性能。
尽管可以手动检查详细的 GC 日志,但数据量常常非常大。Garbage Collection and Memory Visualizer (GCMV) 工具使用详细的 GC 日志,可视化结果,并提供数据分析(图 10)。GCMV 报告正确的信息、错误的信息、需要关注的信息,以及最重要地,提供建议来帮助改进性能。
图 10. Garbage Collection and Memory Visualizer (GCMV)
如果希望亲自尝试,以及在一些情况下通过 GCMV 从日志中挖掘可能还不可用的信息,那么直接检查详细的 GC 日志是一种可接受的问题诊断和调节途径。清单 1 中给出了一节详细 GC 日志的示例,它描述了一个典型的 PGC 周期。
清单 1. 一节详细 GC 日志的示例
<exclusive-start id="137" timestamp="2011-06-22T16:18:32.453" intervalms="3421.733"> <response-info timems="0.146" idlems="0.104" threads="4" lastid="0000000000D97A00" lastname="XYZ Thread Pool : 34" /> </exclusive-start> <allocation-taxation id="138" taxation-threshold="671088640" timestamp="2011-06-22T16:18:32.454" intervalms="3421.689" /> <cycle-start id="139" type="partial gc" contextid="0" timestamp="2011-06-22T16:18:32.454" intervalms="3421.707" /> <gc-start id="140" type="partial gc" contextid="139" timestamp="2011-06-22T16:18:32.454"> <mem-info id="141" free="8749318144" total="10628366336" percent="82"> <mem type="eden" free="0" total="671088640" percent="0" /> <numa common="10958264" local="1726060224" non-local="0" non-local-percent="0" /> <remembered-set count="352640" freebytes="422080000" totalbytes="424901120" percent="99" regionsoverflowed="0" /> </mem-info> </gc-start> <allocation-stats totalBytes="665373480" > <allocated-bytes non-tlh="2591104" tlh="662782376" arrayletleaf="0"/> <largest-consumer threadName="WXYConnection[192.168.1.1,port=1234]" threadId="0000000000C6ED00" bytes="148341176" /> </allocation-stats> <gc-op id="142" type="copy forward" timems="71.024" contextid="139" timestamp="2011-06-22T16:18:32.527"> <memory-copied type="eden" objects="171444" bytes="103905272" bytesdiscarded="5289504" /> <memory-copied type="other" objects="75450" bytes="96864448" bytesdiscarded="4600472" /> <memory-cardclean objects="88738" bytes="5422432" /> <remembered-set-cleared processed="315048" cleared="53760" durationms="3.108" /> <finalization candidates="45390" enqueued="45125" /> <references type="soft" candidates="2" cleared="0" enqueued="0" dynamicThreshold="28" maxThreshold="32" /> <references type="weak" candidates="1" cleared="0" enqueued="0" /> </gc-op> <gc-op id="143" type="classunload" timems="0.021" contextid="139" timestamp="2011-06-22T16:18:32.527"> <classunload-info classloadercandidates="178" classloadersunloaded="0" classesunloaded="0" quiescems="0.000" setupms="0.018" scanms="0.000" postms="0.001" /> </gc-op> <gc-end id="144" type="partial gc" contextid="139" durationms="72.804" timestamp="2011-06-22T16:18:32.527"> <mem-info id="145" free="9311354880" total="10628366336" percent="87"> <numa common="10958264" local="1151395432" non-local="0" non-local-percent="0" /> <pending-finalizers system="45125" default="0" reference="0" classloader="0" /> <remembered-set count="383264" freebytes="421835008" totalbytes="424901120" percent="99" regionsoverflowed="0" /> </mem-info> </gc-end> <cycle-end id="146" type="partial gc" contextid="139" timestamp="2011-06-22T16:18:32.530" /> <exclusive-end id="147" timestamp="2011-06-22T16:18:32.531" durationms="77.064" />
使用平衡收集器的时机
平衡收集器是较旧的策略(包括 gencon)的一种合适的替代品,只要环境和应用程序需要能够适合相关利弊。一般而言,在以下情况下建议使用平衡收集器:
- 应用程序在 64 位平台上运行并部署了一个大于 4GB 的堆。平衡收集器是在大型堆上减少大型全局 GC 暂停时间的最佳选择。由于与平衡收集器中使用的技术相关的开销,小型堆可能不提供理想的部署场景。
- 应用程序可使用 gencon 获得出色的结果,但仍然会遇到偶尔过长的全局 GC 暂停时间,包括较长的全局压缩时间。平衡收集器提供的平均暂停时间可能比 gencon 稍微长一些,但通过专注于新创建的对象而保留了 gencon 的优点,并且最终将通过增量收集和压缩全局堆来避免全局收集成本。请注意,在其他 GC 策略中,由于大对象(尤其是数组)的分配,全局收集和压缩也可能过于频繁地发生。
- 应用程序愿意接受性能的细微降级。前面已经提到,平衡收集器的技术方法比 gencon 或其他 GC 策略复杂得多,因此,暂停时间和 Java 应用程序开销方面的 GC 成本都要高些。尽管此开销常常不会超过 10%,它仍然代表着不小的得失,尤其是在与 gencon 对比时。
Java 应用程序还可以利用许多性能或行为收益,但这不是转向使用平衡收集器的必要原因:
- 应用程序可以大量使用类卸载,无论是通过反射还是其他工具。gencon 依靠全局收集来执行类卸载和字符串常量垃圾收集,与此不同,平衡收集器能够收集类加载器和字符串常量,只要它们位于给定的收集组中,包括夭折的、新创建的对象。这是平衡收集器的一项优势,因为它能够迅速收集这些对象和与它们关联的本地内存结构,减少总体内存压力和开销。
- 应用程序部署在大型硬件(大量内存,大量核心)上,平衡收集器可以更好地利用它们。平衡收集器也将识别 NUMA 系统,部署 GC 线程和 Java 线程,以及分配堆中的对象,采用可以利用内存速度上的区别的方式。此外,有了大量的核心,平衡收集器可通过部署帮助器线程来加速收集过程,从而利用并发性(GC 工作在 Java 线程运行期间继续运行)。
- 应用程序使用非常大的数组,平衡收集器可使用不连续的表示形式来存储这样的数组。平衡收集器能够在垃圾收集期间更有效地处理这些数组,也可以避免通过压缩来分配大型数组。
最后,在评估平衡收集器时,还有许多因素需要考虑,这些因素可能意味着它不适合部署:
- 平衡收集器不是一种实时的垃圾收集器,如果需要实时结果,您应该使用 IBM WebSphere Real Time for AIX 或 IBM WebSphere Real Time for Linux。尽管平衡收集器在尽力缓和 GC 暂停时间,但它最终无法保证最大的暂停时间或暂停将完全一致。应用程序生成的工作将表明 GC 暂停的频率和持续时间。
- 因为基于堆在内部的组织方式,平衡收集器会使用更多的堆内存,所以它不是很适合已经很满(超过 90% 的占用率)的堆。由于全局收集过程的增量性质,以及区域中存在微型碎片的可能性,对非常满的堆使用平衡收集器可能不会实现良好的暂停时间行为,因为它会尽力在相对较短的时间内处理整个堆,实际上使它充当着 optthruput 或 optavgpause 等全局收集器。
- 因为平衡收集器为非常大的数组使用了一种不连续的表示法,所以 JNI 对这些数组的访问可能比其他收集器更慢。如果应用程序广泛使用 JNI 和大型数组,您也许能够充分增加堆大小来使数组变得连续,进而改进性能。您也许还能够修改本地代码来减少它对大型数组的依赖性。如果这些可能性都不存在,平衡收集器可能不适合您的部署。
结束语
本文介绍了平衡 GC 技术,它可通过 IBM WebSphere Application Server V8 Java 虚拟机中的 -Xgcpolicy:balanced
选项来实现。该技术旨在通过增量化整堆收集过程并将它合并到一种分代收集机制中,减少全局收集和压缩所引起的非常长的暂停时间。尽管该技术具有一些不足,比如较长的平均暂停时间,但事实证明它在各种需要相对一致的暂停时间的部署场景中非常有用。由于技术上的进步,比如增量类卸载,根据具体需求,平衡收集器可能还适合其他部署。