g1垃圾回收器的优缺点
这是有关G1垃圾收集器的两部分系列文章中的第二篇。 您可以在InfoQ 2013年7月15日找到第1部分: G1:由一个垃圾收集器来全部统治 。
在我们了解如何调整“垃圾优先”垃圾收集器(G1 GC)之前,我们必须首先了解定义G1的关键概念。 在本文中,我将首先介绍该概念,然后讨论如何针对该概念调整G1(在适当情况下)。
回想一下上一篇文章:记忆集(简称RSets)是按区域的条目,可帮助G1 GC跟踪指向堆区域的外部引用。 因此,现在,G1无需扫描整个堆中对区域的引用,而只需扫描其RSet。
让我们来看一个例子。 在上面的图1中,我们显示了三个区域(以灰色表示):区域1,区域2和区域3以及它们代表一组卡的相关RSet(以粉红色表示)。 区域1和区域3恰好都在引用区域2中的对象。因此,区域2的RSet跟踪对区域2的两个引用,即“拥有区域”。
有两个概念可以帮助维护RSets:
屏障代码在写入后进入(因此称为“写入后屏障”),并有助于跟踪跨区域更新。 更新日志缓冲区负责记录包含更新的参考字段的卡。 一旦这些缓冲区已满,它们就会退役。 并发优化线程处理这些完整缓冲区。
请注意,并发优化线程通过并发更新RSets(当应用程序也正在运行时)来帮助维护RSets。 并发优化线程的部署是分层的,其中最初仅部署少量线程,然后根据要处理的已填充更新缓冲区的数量最终添加更多线程。 并发优化线程的最大数量可以由-XX:G1ConcRefinementThreads甚至-XX:ParallelGCThreads控制 。 如果并发优化线程无法跟上已填充缓冲区的数量,则更改器线程将拥有并处理缓冲区的处理-通常应避免这样做。
好,回到RSets-每个区域有一个RSet。 RSets的粒度分为三个级别-稀疏,精细和粗糙。 Per-Region-Table(PRT)是包含RSet粒度级别的抽象。 稀疏PRT是包含卡索引的哈希表。 G1 GC在内部维护这些卡。 卡可以包含从跨越与卡相关联的地址的区域到拥有区域的引用。 细粒度的PRT是一个开放的哈希表,其中每个条目代表一个引用拥有区域的区域。 该区域内的卡索引保存在位图中。 当达到细粒度PRT的最大容量时,在粗粒度位图中设置相应的粗粒度位,并且从细粒度PRT中删除相应的条目。 每个区域的粗略位图只有一位。 粗粒度图中的设置位意味着关联的区域可能包含对拥有区域的引用。
收集集(CSet)是在垃圾收集期间要收集的一组区域。 对于年轻集合,CSet仅包括年轻区域,对于混合集合,CSet包括年轻和旧区域。
如果CSet包含许多带有粗糙的RSets的区域(请注意,“ RSets的粗化”定义为RSets通过不同粒度级别的过渡),那么您将看到RSets扫描时间的增加。 这些扫描时间在GC暂停中在GC日志中表示为“ Scan RS(ms)”。 如果相对于整体GC暂停时间而言,“扫描RS”时间似乎较长,或者对于您的应用程序而言,它们看起来较高,那么在使用诊断选项-XX:+ G1SummarizeRSetStats时,请在GC日志输出中查找文本字符串“ Did xyz thinnings” (您还可以通过设置-XX:G1SummarizeRSetStatsPeriod = period来指定报告频率周期(以GC的数量为单位))。
如果您回想起前一篇文章,GC暂停输出中的“ Update RS(ms)”显示了更新RSets所花费的时间,“ Processed Buffers”显示了GC暂停期间更新缓冲区过程的计数。 如果您在GC日志中发现了问题,请使用上述选项进一步“深入研究”问题。
这些选项还可以帮助确定更新日志缓冲区和并发优化线程的潜在问题。
-XX:+ G1SummarizeRSetStats的样本输出,其周期设置为一个-XX:G1SummarizeRSetStatsPeriod = 1:
并发RS 处理的784125卡
在4870个完成的缓冲区中 :
并发RS线程 4870 (100.0%) 。
mutator线程为 0 ( 0.0%) 。
并行RS线程时间(s)
0.64 0.30 0.26 0.18 0.17 0.16 0.17 0.15 0.15 0.12 0.13 0.08 0.13 0.13 0.12 0.13 0.12 0.11 0.12 0.11 0.12 0.13 0.11
并发采样线程时间(秒)
0.00
总堆区域rem集大小= 199140K。 最大值= 661K。
静态结构= 660K,free_lists = 15052K。
1009422114代表占用卡。
最大大小区域=
313:(O)[0x000000054e400000,0x000000054e800000,0x000000054e800000],大小= 662K,已占用= 1214K。
做了2759次粗化。
上面的输出显示已处理卡和已完成缓冲区的数量 。 它还显示并发优化线程完成了100%的工作,而更改程序线程则不执行任何工作 (正如我们所说的,这是一个好兆头!)。 然后,它列出了工作中涉及的每个线程的并发优化线程时间。
棕色部分显示自HotSpot VM启动以来的累积统计信息。 累积的统计信息包括RSet的总大小和RSet的最大大小,占用卡的总数以及区域的最大大小信息。 它还显示自VM启动以来完成的粗化总数。
在这一点上,我认为可以引入另一个选项标志-XX:G1RSetUpdatingPauseTimePercent = 10是安全的。 该标志设置了目标百分比百分比(默认为暂停时间目标的10%),G1 GC在GC撤离暂停期间更新RSets时应花费该百分比。 您可以增加或减少百分比值,以便在世界停止(STW)GC暂停期间花更多(或更少)的时间(分别)来更新RSets,并让并发优化线程相应地处理更新缓冲区。
请记住,通过降低百分比值,您会将工作推向了并发优化线程。 因此,您会发现并发工作有所增加。
G1 GC在疏散暂停和备注暂停(多阶段并行标记的一部分)期间处理参考。
在撤离暂停期间,将在对象扫描和复制阶段发现参考对象,并在此之后对其进行处理。 在GC日志中,您可以看到称为“其他”的顺序工作组下的基准处理(Ref proc)时间-
[Other: 0.2 ms]
[Choose CSet: 0.0 ms][Ref Proc: 0.2 ms]
[Ref Enq: 0.0 ms]
[Free CSet: 0.0 ms]
注意:带有无效参照物的参照物将添加到待处理列表中,该时间在GC日志中显示为参照物入库时间 (Ref Enq)。
在注释暂停期间,发现发生在并发标记的较早阶段。 (注意:两者都是多阶段并发标记周期的一部分。有关更多信息,请参阅上一篇文章。)标记阶段处理发现的引用的处理。 在GC日志中,您可以看到GC备注部分中显示的参考处理(GC ref-proc)时间-
0.094: [GC remark 0.094: [GC ref-proc, 0.0000033 secs], 0.0004374 secs]
[Times: user=0.00 sys=0.00, real=0.00 secs]
如果在参考处理期间看到大量时间,请通过在命令行-XX:+ ParallelRefProcEnabled上启用以下选项来打开并行参考处理。
在您的GC旅行期间,您可能遇到过一些术语“撤离失败”,“到空间用尽”,“到空间溢出”或诸如“促销失败”之类的东西。 这些术语均指同一事物,并且G1 GC中的概念也相似。 如果没有更多的空闲区域可以升级到老一代或复制到幸存者空间,并且由于堆已经达到最大值,则堆无法扩展,则会发生疏散失败。
对于G1 GC,疏散失败非常昂贵-
那么当您在G1 GC日志中遇到疏散失败时该怎么办? --
为了帮助解释疏散失败的原因,我想介绍一个非常有用的选项: -XX:+ PrintAdaptiveSizePolicy 。 此选项将提供许多符合人体工程学的细节,这些细节有意保留在-XX:+ PrintGCDetails选项之外。
让我们看看启用了-XX:+ PrintAdaptiveSizePolicy的日志片段:
6062.121: [GC pause (G1 Evacuation Pause) (mixed ) 6062.121: [G1Ergonomics (CSet Construction) start choosing CSet, _pending_cards: 129059, predicted base time: 52.34 ms, remaining time: 147.66 ms, target pause time: 200.00 ms]
6062.121: [G1Ergonomics (CSet Construction) add young regions to CSet , eden: 912 regions, survivors: 112 regions , predicted young region time: 256.16 ms]
6062.122: [G1Ergonomics (CSet Construction) finish adding old regions to CSet, reason: old CSet region num reached min , old: 149 regions, min: 149 regions] 6062.122: [G1Ergonomics (CSet Construction) finish choosing CSet, eden: 912 regions, survivors: 112 regions, old: 149 regions, predicted pause time: 344.87 ms, target pause time: 200.00 ms]
6062.281: [G1Ergonomics (Heap Sizing) attempt heap expansion , reason: region allocation request failed , allocation request: 2097152 bytes ]
6062.281: [G1Ergonomics (Heap Sizing) expand the heap, requested expansion amount: 2097152 bytes, attempted expansion amount: 4194304 bytes]
6062.281: [G1Ergonomics (Heap Sizing) did not expand the heap, reason: heap expansion operation failed]
6062.902: [G1Ergonomics (Heap Sizing) attempt heap expansion, reason: recent GC overhead higher than threshold after GC, recent GC overhead: 20.30 %, threshold: 10.00 %, uncommitted: 0 bytes, calculated expansion amount: 0 bytes (20.00 %)]
6062.902: [G1Ergonomics (Concurrent Cycles) do not request concurrent cycle initiation, reason: still doing mixed collections , occupancy: 9596567552 bytes, allocation request: 0 bytes, threshold: 5798205810 bytes (45.00 %) , source: end of GC]
6062.902: [G1Ergonomics (Mixed GCs) continue mixed GCs, reason: candidate old regions available, candidate old regions: 1038 regions, reclaimable: 2612574984 bytes (20.28 %), threshold: 10.00 %]
( to-space exhausted ), 0.7805160 secs]
上面的代码段包含了很多信息-首先,让我从上面用于GC日志的命令行选项集中突出一些内容:-server -Xms12g -Xmx12g -XX:+ UseG1GC -XX:NewSize = 4g -XX:MaxNewSize = 5g
红色开关表示用户已将苗圃限制在4G至5G范围内,从而限制了G1 GC的适应性。 如果G1需要将苗圃降低到一个较小的值,则不能; 如果G1需要增加育儿空间,超出其分布范围,那就不能!
从此疏散暂停结束时打印的堆利用率信息可以明显看出这一点:
[Eden: 3648.0M(3648.0M)->0.0B(3696.0M) Survivors: 448.0M->400.0M Heap:11.3G (12.0G)->9537.9M(12.0G)]
循环之后,G1必须遵守最小育儿空间(-XX:NewSize = 4g)的4096M,其中根据G1的计算,伊甸园空间应为3696M,幸存者空间应为400M。 但是,Java堆中的后收集数据已经是9537.9M。 因此,G1用完了“太空”! 接下来的两个疏散暂停还会导致疏散失败,并带有以下堆信息:
下一个混合疏散暂停1:
[Eden: 2736.0M(3696.0M)->0.0B(4096.0M) Survivors: 400.0M->0.0B Heap: 12.0G(12.0G)->12.0G(12.0G)]
下一个混合疏散暂停2:
[Eden: 0.0B(4096.0M)->0.0B(4096.0M) Survivors: 0.0B->0.0B Heap: 12.0G(12.0G)->12.0G(12.0G)]
最终导致了完整的GC-
6086.564: [Full GC (Allocation Failure ) 11G->3795M(12G), 15.0980440 secs]
[Eden: 0.0B(4096.0M)->0.0B(4096.0M) Survivors: 0.0B->0.0B Heap: 12.0G(12.0G)->3795.2M(12.0G)]
通过将托儿所/年轻代缩小到默认最小值(占Java总堆的5%),可以避免使用Full GC。 如您所知,老一代产品足以容纳3795M的实时数据集(LDS)。 但是,LDS加上明确设置的最小4G代,使占用量增加到7891M以上。 由于标记阈值为默认值堆的45%(即5529M左右),因此标记周期较早开始,并且在混合收集期间很少回收。 堆占用率一直在增加,并且另一个标记周期开始了,但是到那时,标记周期已经完成,混合GC开始运行,占用率已经达到11.3G(如第一个堆利用率信息所示)。 此集合也遇到疏散失败。 因此,此问题属于过度调整和“过早开始标记周期”类别。
我想在本文中介绍的最后一件事是对于许多最终用户而言可能是一个新概念-大型对象(H-objs)和G1 GC对H-objs的处理。
那么,为什么我们需要为H-objs使用不同的分配路径? --
对于G1 GC,如果对象跨越G1区域大小的50%或更多,则被认为是巨大的。 大量分配需要一组连续的区域。 可以想象,如果G1在年轻一代中分配H-obj,并且它们可以存活足够长的时间,那么将有很多不必要且昂贵的操作(请记住H-obj需要连续的区域)将H-objs保留到生存空间中,然后最终将这些H-objs提升为老一代。 因此,为了避免这种开销,可以直接将H-objs分配到旧一代之外,然后进行分类或映射为巨大区域。
通过在老一代中直接分配H-obj,G1避免将它们包含在任何疏散暂停中,因此它们永远不会移动。 在一个完整的垃圾回收周期中,G1 GC围绕实时H-obj压缩。 在完整GC之外,在多阶段并发标记周期的清理阶段,将回收死H-obj。 换句话说,H-obj是在清理阶段收集的,或者是在完整GC期间收集的。
在分配H-obj之前,G1 GC将检查是否由于分配而超出了初始堆占用百分比(标记阈值)。 如果可以,则G1 GC将启动G1并发标记周期。 这样做是因为我们要尽可能避免疏散失败和完整的垃圾收集周期。 结果,我们将尽早检查,以便在没有更多可用区域用于活动对象撤离之前,为G1并发周期提供尽可能多的时间来完成。
对于G1 GC,使用H-objs的基本前提是希望这些对象不太多,并且寿命长。 但是,由于G1 GC的区域大小取决于您的最小堆大小,因此可能会发生,根据分配的区域大小,您的“常规”分配对于G1 GC可能显得有些笨拙。 然后,这将导致大量H-obj分配占用了较早的区域,这最终将导致撤离失败,因为G1无法跟上那些庞大的分配。
现在,您可能正在思考如何找出庞大的分配是否导致疏散失败。 -在这里,-XX:+ PrintAdaptiveSizePolicy将再次为您提供帮助。
在您的GC日志中,您将看到以下内容:
1361.680: [G1Ergonomics (Concurrent Cycles)request concurrent cycle initiation , reason: occupancy higher than threshold, occupancy: 1459617792 bytes, allocation request: 4194320 bytes , threshold: 1449551430 bytes (45.00 %), source: concurrent humongous allocation ]
因此,现在您可以确定由于存在4194320 bytes的巨大分配请求, 因此请求了并发周期 。
该信息很有用,因为您不仅可以知道应用程序进行了多少次巨大的分配(以及是否过多),而且还可以知道分配的大小。 此外,如果您认为分配过多,那么您要做的就是增加G1区域的大小以适合常规的H-obj。 因此,例如,分配大小刚好在4M字节以上。 因此,为了使此分配成为常规分配,我们需要16M的区域大小。 因此,这里的建议是在命令行上显式设置:-XX:G1HeapRegionSize = 16M
注意:回想一下我的上一篇文章,G1区域可以从1MB扩展到32MB(以2的幂),并且分配请求略微超过4 MB。 因此,一个8MB的区域大小不足以避免繁琐的分配。 我们需要转至2的下一个幂16 MB。
好。 我想我已经在这里介绍了大多数重要问题和G1 GC概念。 再次感谢您的收看!
翻译自: https://www.infoq.com/articles/tuning-tips-G1-GC/?topicPageSponsorship=c1246725-b0a7-43a6-9ef9-68102c8d48e1
g1垃圾回收器的优缺点