Garbage-First Garbage Collector
Garbage-First(G1)垃圾收集器的目标是具有大量内存的多处理器计算机。它试图以高概率满足垃圾收集暂停时间目标,同时在几乎不需要配置的情况下实现高吞吐量。 G1旨在使用当前目标应用程序和环境提供延迟和吞吐量之间的最佳平衡,其功能包括:
-
堆大小最多为10 GB或更大,超过50%的Java堆占用实时数据。
-
对象分配和促销的比率可能会随着时间的推移而显着变化。
-
堆中存在大量碎片。
-
可预测的暂停时间目标目标,不超过几百毫秒,避免长时间的垃圾收集暂停。
G1取代了Concurrent Mark-Sweep(CMS)收集器。它也是默认的收集器。
G1收集器实现了高性能,并尝试以下面几节中描述的几种方式实现暂停时间目标。
启动 G1
Garbage-First 垃圾收集器是默认收集器,因此通常您不必执行任何其他操作。
您可以通过在命令行上提供-XX:+UseG1GC
来明确启用它。
基本概念
G1是世代的,增量的,并行的,大部分是并发的,停止世界的,以及疏散的垃圾收集器,它监视每个世界上停顿的暂停时间目标。
与其他 Collector 类似,G1将堆分成(虚拟)年轻和老一代。空间回收工作集中在年轻一代,这是最有效的,老一代偶尔进行空间回收
有些操作总是在世界各地暂停时执行以提高吞吐量。其他需要花费更多时间停止应用程序的操作,例如全局标记等整堆操作,是与应用程序并行执行的。
为了使空间回收缩短世界各地的停顿时间,G1逐步并行地逐步进行空间回收。
G1通过跟踪先前应用程序行为和垃圾收集暂停的信息来建立相关成本的模型,从而实现可预测性。它使用此信息来确定暂停中完成的工作的大小。例如,G1首先在最有效的区域中回收空间(即大部分区域充满垃圾,因此名称)。
G1主要通过疏散回收空间:在选定的存储区域内找到的活动对象被复制到新的存储区域,在此过程中压缩它们。疏散完成后,活动对象先前占用的空间将被重用,以供应用程序分配。
Garbage-First收集器不是实时收集器。它试图在较长时间内以高概率满足设定的暂停时间目标,但对于给定的暂停并不总是绝对确定。
堆布局
G1将堆分区为一组大小相等的堆区域,每个区域都是一个连续的虚拟内存区域,如图9-1所示。
区域是内存分配和内存回收的单位。在任何给定时间,这些区域中的每一个都可以是空的(浅灰色),或者分配给特定的一代,无论年轻还是年老。随着内存请求的进入,内存管理器会分发免费区域。内存管理器将它们分配给一代,然后将它们作为可以自行分配的可用空间返回给应用程序。
- Figure 9-1 G1 Garbage Collector Heap Layout
年轻一代包含伊甸园区域(红色)和幸存区域(红色与“S”)。
这些区域提供与其他收集器中的相应连续空间相同的功能,不同之处在于G1中这些区域通常以不连续的模式布置在存储器中。旧区域(淡蓝色)构成了老一代。对于跨越多个区域的物体,老一代区域可能是巨大的(浅蓝色和“H”)。
应用程序总是分配给年轻一代,即伊甸园区域,除了直接分配为属于旧一代的大量对象。
G1垃圾收集暂停可以回收整个年轻一代的空间,任何收集的任何其他一组老一代区域都会暂停。在暂停期间,G1将此集合中的对象复制到堆中的一个或多个不同区域。对象的目标区域取决于该对象的源区域:整个年轻代被复制到幸存者或旧区域,并且使用老化将旧对象中的对象复制到其他不同的旧区域。
垃圾收集周期
在高水平上,G1收集器在两个阶段之间交替。
仅限年轻阶段包含垃圾收集,逐渐填充当前可用内存和旧代中的对象。
空间回收阶段是除了处理年轻一代之外,G1逐步回收老一代的空间。 然后循环重新开始,只有一个年轻阶段。
图9-2给出了有关此循环的概述,并提供了可能发生的垃圾收集暂停序列的示例:
- 图9-2垃圾收集周期概述
以下列表详细描述了G1垃圾回收周期的阶段,暂停和阶段之间的转换:
仅限年轻阶段:此阶段从一些仅限新生代的集合开始,这些集合将对象推广到旧一代。只有年轻阶段和空间回收阶段之间的过渡开始于旧一代占用率达到某个阈值,即启动堆占用阈值。此时,G1计划初始标记仅限年轻人的集合,而不是常规的仅限年轻集合。
初始标记:除了执行常规的仅限年轻的集合之外,此类集合还会启动标记过程。并发标记确定旧一代区域中当前可到达(实时)的所有对象,以便在下一个空间回收阶段保留。虽然标记尚未完全完成,但可能会出现常规的年轻 collect。
标记完成时有两个特殊的世界停顿:备注和清理。
备注:此暂停最终确定标记本身,并执行全局引用处理和类卸载。在备注和清理之间G1同时计算活跃度信息的摘要,这些摘要将在清理暂停中最终确定并用于更新内部数据结构。
清理:此暂停还会回收完全空的区域,并确定是否实际遵循空间回收阶段。如果接下来是一个空间回收阶段,那么仅限年轻的阶段将完成一个仅限年轻人的收藏。
空间回收阶段:此阶段由多个混合集合组成,除了年轻代区域外,还可以撤离旧一代区域的活动对象。当G1确定撤离更多的旧一代区域不会产生足够的自由空间值得努力时,空间回收阶段结束。
在空间回收之后,收集周期将以另一个仅限年轻的阶段重新开始。作为备份,如果应用程序在收集活动信息时内存不足,则G1会像其他收集器一样执行就地停止全堆压缩(Full GC)。
Garbage-First Internals
本节介绍Garbage-First(G1)垃圾收集器的一些重要细节。
确定启动堆占用率
启动堆占用百分比(IHOP)是触发初始标记集合的阈值,它被定义为旧生成大小的百分比。
G1默认情况下通过观察标记所花费的时间以及在标记周期期间通常在旧一代中分配多少内存来自动确定最佳IHOP。
此功能称为自适应IHOP。如果此功能处于活动状态,则选项-XX:InitiatingHeapOccupancyPercent
将初始值确定为当前旧代的大小的百分比,只要没有足够的观察值来对启动堆占用阈值进行良好预测即可。
使用选项-XX:-G1UseAdaptiveIHOP
关闭G1的此行为。在这种情况下,-XX:InitiatingHeapOccupancyPercent
的值始终确定此阈值。
在内部,Adaptive IHOP尝试设置Initiating Heap Occupancy,以便当旧一代占用率为当前最大旧生成大小减去-XX:G1HeapReservePercent
的值作为额外缓冲区时,空间回收阶段的第一个混合垃圾收集开始。
标记
G1标记使用称为Snapshot-At-Beginning(SATB)的算法。它会在初始标记暂停时获取堆的虚拟快照,此时标记开始时生效的所有对象都被认为是剩余标记的实时对象。这意味着在标记期间变为死亡(无法访问)的对象仍然被认为是用于空间回收的目的(有一些例外)。与其他收集器相比,这可能会导致错误地保留一些额外的内存。但是,SATB可能会在备注暂停期间提供更好的延迟。在该标记期间过于保守地考虑的活动对象将在下一个标记期间被回收。有关标记问题的详细信息,请参阅Garbage-First垃圾收集器调整主题。
在非常紧的堆情况下的行为
当应用程序保持如此大的内存以便疏散无法找到足够的空间来复制时,就会发生疏散失败。疏散失败意味着G1试图通过保留已经在新位置移动的任何对象来完成当前的垃圾收集,而不是复制任何尚未移动的对象,只调整对象之间的引用。疏散失败可能会产生一些额外的开销,但通常应该与其他年轻的收集一样快。在此疏散失败的垃圾收集之后,G1将正常恢复应用程序而不采取任何其他措施。 G1假定疏散失败发生在垃圾收集结束时;也就是说,大多数对象已经被移动,并且剩下足够的空间来继续运行应用程序,直到标记完成并开始空间回收。
如果这个假设不成立,那么G1最终将安排一个完整的GC。此类集合执行整个堆的就地压缩。这可能会很慢。
有关分配失败或Full GC的问题的详细信息,请参阅垃圾优先垃圾收集器调整,然后再发出内存不足信号。
Humongous Objects
Humongous对象是大于或等于半个区域大小的对象。除非使用-XX:G1HeapRegionSize
选项进行设置,否则当前区域大小按照符合人体工程学的方式确定,如G1 GC的人体工程学默认值部分所述。
这些巨大的物体有时会以特殊方式处理:
每个巨大的对象都被分配为老一代的一系列连续区域。对象本身的起始始终位于该序列中第一个区域的开头。序列的最后一个区域中的任何剩余空间都将丢失以进行分配,直到整个对象被回收为止。 通常,只有在清理暂停期间标记结束时才会回收大量对象,或者如果它们无法访问则可以在完整GC期间回收。但是,对于基本类型的数组,例如bool,各种整数和浮点值,有一个特殊的规定。如果在任何类型的垃圾收集暂停时没有被许多对象引用,G1会机会性地尝试回收大量对象。
默认情况下启用此行为,但您可以使用选项-XX:G1EagerReclaimHumongousObjects
禁用它。
大量物体的分配可能导致垃圾收集暂停过早发生。 G1检查每个巨大物体分配时的初始堆占用阈值,并且如果当前占用率超过该阈值,则可以立即强制初始标记年轻收集。 即使在Full GC期间,这些巨大的物体也不会移动。这可能导致过早缓慢的Full GC或意外的内存不足情况,并且由于区域空间的碎片而留下大量可用空间。
仅年轻阶段生成大小
在仅限年轻阶段,收集的区域集合(收集集)仅由年轻一代区域组成。
G1始终是年轻一代的尺寸,仅限年轻代。
这样,G1可以满足使用设置的暂停时间目标:-XX:MaxGCPauseTimeMillis
和-XX:PauseTimeIntervalMillis
基于实际暂停时间的长期观察。
它考虑了年轻一代相似规模的撤离需要多长时间。这包括诸如在收集期间必须复制多少对象以及这些对象之间的互连方式等信息。
如果没有另外约束,则G1自适应地调整-XX:G1NewSizePercent
和-XX:G1MaxNewSizePercent
确定满足暂停时间的值之间的年轻代大小。
有关如何修复长暂停的详细信息,请参阅垃圾优先垃圾收集器调整。
空间回收阶段生成大小
在空间回收阶段,G1尝试在单个垃圾收集暂停中最大化在旧一代中回收的空间量。年轻代的大小设置为允许的最小值,通常由-XX:G1NewSizePercent确定,并且添加任何用于回收空间的旧代区域,直到G1确定添加更多区域将超过暂停时间目标。在特定的垃圾收集暂停中,G1按照其回收效率,最高的第一个以及获得最终收集集的剩余可用时间的顺序添加旧的生成区域。
每个垃圾收集所需的旧代区域的数量在下端被要收集的潜在候选旧生成区域(收集集合候选区域)的数量限制,除以由-XX:G1MixedGCCountTarget
确定的空间回收阶段的长度。
集合集候选区域都是旧阶段区域,其占用率低于阶段开始时的-XX:G1MixedGCLiveThresholdPercent
。
当收集集候选区域中可回收的剩余空间量小于-XX:G1HeapWastePercent
设置的百分比时,该阶段结束。
有关将使用多少旧生成区域G1以及如何避免长混合收集暂停的详细信息,请参阅垃圾优先垃圾收集器调整。
比较
这是G1和其他Collector 之间主要差异的总结:
并行GC只能作为一个整体来压缩和回收旧一代的空间。
G1逐步将这项工作分配到多个更短的集合中。这大大缩短了暂停时间,可能会降低吞吐量。
与CMS类似,G1同时执行旧一代空间回收的一部分。
但是,CMS无法对旧一代堆进行碎片整理,最终会遇到长整型GC。
G1可能表现出比其他收集器更高的开销,由于其并发性质而影响吞吐量。
由于它的工作原理,G1有一些独特的机制来提高垃圾收集效率:
G1可以在任何收集过程中回收旧一代的一些完全空旷的大面积区域。这可以避免许多其他不必要的垃圾收集,从而不需要太多努力就可以释放大量空间。 G1可以选择同时尝试在Java堆上重复删除重复的字符串。
始终启用从旧一代回收空的大对象。您可以使用选项禁用此功能-XX:-G1EagerReclaimHumongousObjects
。
默认情况下禁用字符串重复数据删除。您可以使用选项-XX:+G1EnableStringDeduplication
启用它
G1的一般建议
一般建议使用G1及其默认设置,最终为其提供不同的暂停时间目标,并根据需要使用-Xmx设置最大Java堆大小。
G1默认值与其他任何收集器的平衡方式不同。
G1在默认配置中的目标既不是最大吞吐量也不是最低延迟,而是在高吞吐量下提供相对较小的统一暂停。但是,G1的递增回收堆中的空间和暂停时间控制的机制在应用程序线程和空间回收效率中都会产生一些开销。
如果您更喜欢高吞吐量,请使用-XX:MaxGCPauseMillis
放宽暂停时间目标或提供更大的堆。
如果延迟是主要要求,则修改暂停时间目标。避免使用-Xmn,-XX:NewRatio等选项将年轻代大小限制为特定值,因为年轻代大小是G1允许其满足暂停时间的主要手段。将年轻代大小设置为单个值会覆盖并实际禁用暂停时间控制。
从其Collector转移到G1
通常,当从其他收集器(特别是并发标记扫描收集器)移动到G1时,首先删除所有影响垃圾收集的选项,并仅使用-Xmx和可选的-Xms设置暂停时间目标和总体堆大小。
许多选项对其他收集器以某种特定方式响应是有用的,根本没有效果,甚至降低吞吐量和满足暂停时间目标的可能性。
一个例子可能是设置年轻一代的尺寸,完全阻止G1调整年轻一代的尺寸以达到暂停时间的目标。
提高G1性能
G1旨在提供良好的整体性能,而无需指定其他选项。
但是,有些情况下,默认启发式或默认配置会提供次优结果。本节提供了有关这些病例的诊断和改进的一些指导原则。本指南仅描述G1在给定设置应用程序时提供的用于改善所选指标中的垃圾收集器性能的可能性。在逐个案例的基础上,应用程序级优化可能比尝试调整VM以更好地执行更有效,例如,通过完全避免长寿命较短的对象的某些问题情况。
出于诊断目的,G1提供全面的日志记录。一个好的开始是使用 -Xlog:gc*=debug
选项,然后根据需要优化输出。
该日志提供有关垃圾收集活动暂停期间和之外的详细概述。
这包括收集的类型和在暂停的特定阶段花费的时间的细分。
以下小节将探讨一些常见的性能问题。
观察完整的垃圾收集
完整堆垃圾收集(Full GC)通常非常耗时。通过在日志中找到“暂停完整(分配失败)”字样,可以检测到旧代中堆占用率过高导致的完整GC。完整的GC通常在垃圾收集之前,遇到由空间耗尽标签指示的疏散失败。
发生Full GC的原因是应用程序分配了太多无法快速回收的对象。
通常并发标记无法及时完成以开始空间回收阶段。许多巨大物体的分配可能会加剧进入Full GC的可能性。由于这些对象在G1中的分配方式,它们可能会占用比预期更多的内存。
目标应该是确保并发标记按时完成。这可以通过降低旧一代的分配率,或者给予并发标记更多的时间来完成。
G1为您提供了几种更好地处理这种情况的选择:
如果Java堆上有大量的大量对象,那么 gc + heap = info logging
会显示巨大区域旁边的数字。
每次垃圾收集后,最好的选择是尝试减少对象的数量。您可以通过使用-XX:G1HeapRegionSize
选项增加区域大小来实现此目的。当前选定的堆区域大小将打印在日志的开头。
增加Java堆的大小。这通常会增加标记必须完成的时间。
通过显式设置-XX:ConcGCThreads
来增加并发标记线程的数量。
强制G1开始提前标记。 G1根据先前的应用程序行为自动确定初始堆占用百分比(IHOP)阈值。
如果应用程序行为发生更改,则这些预测可能是错误的。
有两种选择:通过修改-XX:G1ReservePercent
来增加自适应IHOP计算中使用的缓冲区,从而降低何时开始空间回收的目标占用率;
或者,通过使用-XX:-G1UseAdaptiveIHOP
和-XX:InitiatingHeapOccupancyPercent
手动设置IHOP来禁用IHOP的自适应计算。
除完全GC的分配失败之外的其他原因通常表明应用程序或某些外部工具导致完整堆集合。
如果原因是System.gc(),并且无法修改应用程序源,则可以通过使用-XX:+ExplicitGCInvokesConcurrent
来缓解Full GC的影响,或者让VM完全忽略它们,方法是设置-XX:+DisableExplicitGC
。
外部工具可能仍会强制使用Full GC;它们只能通过不请求它们来删除。
Humongous对象碎片
在所有Java堆内存耗尽之前,可能会发生完整GC,因为必须为它们找到一组连续的区域。
在这种情况下,可能的选项是通过使用选项-XX:G1HeapRegionSize
来增加堆区域大小,以减少大量对象的数量,或增加堆的大小。
在极端情况下,即使可用内存另有说明,G1也可能没有足够的连续空间来分配对象。
如果Full GC无法回收足够的连续空间,则会导致VM退出。
因此,除了如前所述减少大量对象分配或增加堆之外,没有其他选择。
RS时代
为了使G1能够撤离单个旧生成区域,G1跟踪跨区域引用的位置,即从一个区域指向另一个区域的引用。指向给定区域的一组跨区域引用称为该区域的记忆集。移动区域内容时,必须更新记住的集合。区域记忆集的维护大部分是并发的。出于性能目的,当应用程序在两个对象之间安装新的跨区域引用时,G1不会立即更新区域的记忆集。记住的设置更新请求被延迟和批处理以提高效率。
G1需要完整的记忆集来进行垃圾收集,因此垃圾收集的更新RS阶段处理任何未完成的记忆集更新请求。 Scan RS阶段在记忆集中搜索对象引用,移动区域内容,然后将这些对象引用更新为新位置。根据应用,这两个阶段可能需要很长时间。
使用选项-XX:G1HeapRegionSize
调整堆区域的大小会影响跨区域引用的数量以及记住的集合的大小。处理区域的记忆集可能是垃圾收集工作的重要部分,因此这对可实现的最大暂停时间有直接影响。较大的区域往往具有较少的跨区域参考,因此处理它们的相对工作量减少,但同时,较大的区域可能意味着每个区域更多的活动对象撤离,增加了其他阶段的时间。
G1尝试计划记住的集更新的并发处理,以便更新RS阶段大约占用允许的最大暂停时间的-XX:G1RSetUpdatingPauseTimePercent
百分比。通过减小该值,G1通常会同时执行更多记忆集更新工作。
虚假高更新RS时间与分配大对象的应用程序相结合可能是由于尝试通过批处理来减少并发记忆集更新工作的优化引起的。如果创建此类批处理的应用程序恰好在垃圾收集之前发生,那么垃圾收集必须在暂停的更新RS时间部分处理所有这些工作。使用-XX:-ReduceInitialCardMarks
来禁用此行为并可能避免这些情况。
扫描RS时间也取决于G1执行的压缩量,以保持记忆的设置存储大小较低。记忆集存储在内存中越紧凑,在垃圾回收期间检索存储值所花费的时间就越多。
G1会自动执行此压缩,称为记忆集粗化,同时根据该区域记忆集的当前大小更新记住的集。
特别是在最高压缩级别,检索实际数据可能非常慢。
选项-XX:G1SummarizeRSetStatsPeriod
与gc + remset =trace level
日志记录一起显示是否发生此粗化。
如果是这样,那么前GC摘要部分中的<X>
粗糙线中的X显示高值。可以显着增加-XX:G1RSetRegionEntries
选项以减少这些粗化的数量。
避免在生产环境中使用此详细的记忆集记录,因为收集此数据可能会花费大量时间。
调整吞吐量
G1的默认策略试图在吞吐量和延迟之间保持平衡;但是,有些情况下需要更高的吞吐量。除了如前面部分所述减少总暂停时间之外,可以减少暂停的频率。
主要思想是使用-XX:MaxGCPauseMillis
增加最大暂停时间。生成大小调整启发式将自动调整年轻代的大小,这直接决定了暂停的频率。如果这不会导致预期的行为,特别是在空间回收阶段,使用-XX:G1NewSizePercent
增加最小年轻代大小将强制G1执行此操作。
在某些情况下,-XX:G1MaxNewSizePercent
(允许的最大年轻代大小)可能会通过限制年轻代的大小来限制吞吐量。
这可以通过查看gc + heap = info logging的区域摘要输出来诊断。在这种情况下,伊甸园地区和幸存者地区的综合百分比接近-XX:G1MaxNewSize占地区总数的百分比。在这种情况下,请考虑增加-XX:G1MaxNewSizePercent
。
增加吞吐量的另一个选择是尝试减少并发工作量,特别是并发记忆集更新通常需要大量CPU资源。
增加-XX:G1RSetUpdatingPauseTimePercent
将工作从并发操作移动到垃圾收集暂停。
在最坏的情况下,可以通过设置来禁用并发记忆集更新:-XX:-G1UseAdaptiveConcRefinement
-XX:G1ConcRefinementGreenZone = 2G
-XX:G1ConcRefinementThreads = 0
。
这通常会禁用此机制,并将所有记住的集更新工作移动到下一个垃圾收集暂停中。
使用-XX:+UseLargePages
启用大页面也可以提高吞吐量。有关如何设置大页面的信息,请参阅操作系统文档。
您可以通过禁用它来最小化堆大小调整工作;将选项-Xms和-Xmx设置为相同的值。
此外,您可以使用-XX:+AlwaysPreTouch
将操作系统工作移动到具有物理内存的虚拟内存到VM启动时间。为了使暂停时间更加一致,这两种措施都是特别需要的。
调整堆大小
与其他收集器一样,G1的目的是调整堆的大小,以便在垃圾收集中花费的时间低于-XX:GCTimeRatio
选项确定的比率。调整此选项可使G1满足您的要求。
可调默认值
本节介绍默认值以及本主题中介绍的有关命令行选项的一些其他信息。
参考资料
https://docs.oracle.com/javase/9/gctuning/garbage-first-garbage-collector.htm
https://docs.oracle.com/javase/9/gctuning/garbage-first-garbage-collector-tuning.htm