资讯专栏INFORMATION COLUMN

译文-调整G1收集器窍门

Lycheeee / 1489人阅读

摘要:注意到通过并发更新它们来帮助维护通常在应用运行期间。累积状态包括的总和和最大数量,已占用的数量,最大尺寸信息。阶段期间和标记期间并发标记多阶段的一部分处理引用。之后,期间的清除阶段死亡的被回收。

原文出处:Tips for Tuning the Garbage First Garbage Collector

这是由两部分组成的系列的第二篇关于G1垃圾回收器的文章,你可以在2013.07.17的InfoQ上找到第一部分:G1: One Garbage Collector To Rule Them All。

在我我们了解如何调整G1 GC之前,首先我必须了解G1定义的关键概念。在这篇文章里,我会首先介绍概念,然后讨论如何调整G1(适当的时候)。

Remembered Sets

从之前的文章回忆起它:Remembered Sets(RSets)是每一个region里面帮助G1 GC追踪外部指向这个region的引用。因此现在,取代因为引用指向这个region扫描整个heap区,G1只需要扫描RSets。

1: Remembered Sets

我们看一下示意图。上面的示意图向我们展示三个region(灰色)。Region 1, Region 2和Region 3和它们关联的RSets(粉红色),RSets代表一些card的集合。Region 1和Region 3恰好引用Region2里的对象。因此,Region 2的RSets记录了两个引用Region2的对象,Region2就是“owning region”。

这里有两个概念帮助理解RSets:

Post-write barriers

Concurrent refinement threads

屏障代码在写操作之后(因此名称是“post-write barrier”),为了记录帮助追踪跨region更新。包含更新引用字段的card更新可靠的日志缓冲区。一旦这些缓冲区满了,它们就停止工作。Concurrent refinement threads处理这些缓冲区日志。

注意到Concurrent refinement threads通过并发更新它们来帮助维护RSets(通常在应用运行期间)。Concurrent refinement threads调度是分层的。开始只有少量的线程被部署,最终添加取决于更新充满缓冲区操作的数量。concurrent refinement threads的最大数量可以由-XX:G1ConcRefinementThreads或者-XX:ParallelGCThreads控制。如果concurrent refinement threads的数量赶不上装满缓冲区的数量,然后mutator threads处理缓冲区过程-通常你应该努力去避免这中情况。

OK,回到RSets-每一个Region有一个RSets。RSets由三种级别的粒度-Sparse, Fine和Coarse。一个Per-Region-Table (PRT)是RSet存储颗粒度级别一个抽象。sparse PRT是一个包含Card目录的hash table。G1 GC内部维护这些card。card包含来自region的引用,这个region的引用是card到“owning region”的关联的地址。fine-grain PRT是一个开放的hash table,每一个entry代表一个指向owning region的引用的region。region里面的card目录,是一个bitmap。当达到fine-grain PRT的最大容量,coarse grain bitmap里面的相应的coarse-grained bit被设置,相应地entry从 fine grain PRT删除。coarse bitmap有一个每个region对应的bit。coarse grain map设置bit意味着关联的region包含到“owning region”的引用。

Collection Set (CSet)是一个gc期间即将被回收的region的set。对于 Young gc,CSet只包含Young Region,对于混合回收,CSets包含Young Region和Old Region。

如果CSets包含许多携带coarsened RSets的Region(注意,“coarsening of RSets”是根据RSets贯穿不同级别颗粒度的过渡期定义的),然后你会看到扫描RSets消耗时间的增长。GC阶段这些扫描时间就是GC日志里面的“Scan RS (ms)”。如果RSets扫描时间相当于GC阶段总时间很高,或者你的应用中它们表现很高,然后通过使用诊断选项-XX:+G1SummarizeRSetStats请观察你的 Young GC 日志输出的“Did xyz coarsenings”(你可以通过设置-XX:G1SummarizeRSetStatsPeriod=period指定周期频率报告(GCs的数量))。

如果你回想起之前的文章,GC阶段的“Update RS (ms)”展示了更新RSets花费时间,"Processed Buffers" 展示了GC期间更新缓冲区过程。如果你的日志中发现这些问题,然后使用上述的选项去进一步深入这些问题。

哪些选项通常可以帮助确定更新日志缓冲区和concurrent refinement threads的问题。

-XX:+G1SummarizeRSetStats 设置成一-XX:G1SummarizeRSetStatsPeriod=1的输出样例:

上面的输出展示了已经处理过的card和已完成的buffer的数量。它展示concurrent refinement threads做了100%的工作,mutator threads 什么也没有做(这是我们说过的一个号的迹象!)。然后列出concurrent refinement thread的每一个线程摄入到工作的时间。

上面褐色的部分展示了自从HotSpot VM启动以来的累积状态。累积状态包括RSets的总和和最大RSets数量,已占用Card的数量,最大Region尺寸信息。它通常展示自VM启动以来任务完成的粗化总数。

此时此刻,介绍其他选项是合适的-XX:G1RSetUpdatingPauseTimePercent=10。设置GC evacuation(疏散)阶段期间G1 GC更新RSets消耗时间的百分比(默认是目标停顿时间的10%)。你可以增大或减小百分比的值,以便在stop-the-world(STW)GC阶段花费更多或更少的时间,让concurrent refinement thread处理相应的缓冲区。

记住,减少百分比的值,你在推迟concurrent refinement thread的工作;因此,你会看到并发任务增加。

Reference Processing

evacuation阶段期间和标记期间(并发标记多阶段的一部分)G1 GC处理引用。

evacuation阶段期间,扫描对象,复制,被处理后的时候找到引用对象。GC log里面,引用处理(Ref proc)时间和叫做“Other”下面的一组连续工作:

Note:无用的引用被添加进pending list,GC log里面展示的时间是reference enqueing time (Ref Enq)。

Remark阶段,发生在并发标记阶段之前。(note:多阶段并发标记周期的一部分。请参考上篇文章的更多细节。)remark阶段处理发现的引用过程。GC log里,你可以在GC remark部分看到reference processing (GC ref-proc)时间:

如果你看到引用处理期间时间很长,通过授权命令行选项 -XX:+ParallelRefProcEnabled打开并行引用处理。

Evacuation Failure

如果你在GC日志发现"evacuation failure", "to-space exhausted", "to-space overflow", "promotion failure"之类的字眼。这些术语的概念在G1 GC是相似的,请参考同一个。当没有更多的空闲region提升到Old代,或者复制到survivor空间,heap由于已经在最大值上而无法扩展,evacuation Failure聚会发生:

如果对象被成功复制,G1需要更新对象引用,region必须是tenured。

如果对象复制失败,G1会自己处理它们,在合适时机把region设置为tenured。

因此在G1 log 发生evacuation failure你应该怎么做:

找出调整的一些影响导致失败-获取heap最大和最小的基线和真实的停顿时间目标:移除任何heap大小的设置例如-Xmn, -XX:NewSize, -XX:MaxNewSize, -XX:SurvivorRatio等等。只使用-Xms, -Xmx,停顿时间目标-XX:MaxGCPauseMillis

如果问题在基线运行依然存在,大对象(看下面的章节)分配没有问题-矫正问题的方案是如果可以的话增加heap区大小。

如果增加heap区大小行不通,如果你注意到为了G1 GC可以回收Old代标记阶段不会提早执行,然后你删掉-XX:MaxGCPauseMillis。这个选项默认占用你heap区的45%。删除这个选项可以帮助提早开始标记阶段。相反的,如果标记阶段提早开始并没有回收到很多空间,你应该在threshold默认值上增加来确保你的应用的存活数据可以适应。

如果并发标记阶段准时启动,但是花了很长时间去完成;因此造成mixed gc周期延迟,最后导致evacuation失败,然后old代没有及时回收;使用选项-XX:ConcGCThreads增加并发标记线程数量。

如果“to-space”Survivor区域有问题, 增加-XX:G1ReservePercent。默认是java heap的10%。G1 GC设置错误上限预留内存,以防万一"to-space"需要更多的空间。当然G1 GC只使用空间的50%,因此如果我们不想应用使用大的Young可以设置一个更大的值。

为了帮助解释evacuation failure的原因,我想介绍一个用户的选项:-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 log使用的命令行选项 server -Xms12g -Xmx12g -XX:+UseG1GC- -XX:NewSize=4g -XX:MaxNewSize=5g展示一些精彩的东西。

加粗部分展示用户限制region的范围在4-5G之间,因此限制了G1 GC的适应能力。如果G1需要去掉限制设置更小的值,它做不到;如果G1 GC需要增加空间范围,超过了给它分配的空间,它做不到!

这是evacuation结束时打印的heap明确信息:

[Eden: 3648.0M(3648.0M)->0.0B(3696.0M) Survivors: 448.0M->400.0M Heap: 11.3G(12.0G)->9537.9M(12.0G)]

阶段之后,G1不得不维持4096M作为最小范围(-XX:NewSize=4g),由于基于G1的计算器,3696M用于Eden区,400M用户Survivor区。然而,heap区标记回收的数据已经达到9537.9M。因此,G1用完“to-space”。下面两次 evacuation阶段的结果是evacuation failure:

混合evacuation阶段1:

[Eden: 2736.0M(3696.0M)->0.0B(4096.0M) Survivors: 400.0M->0.0B Heap: 12.0G(12.0G)->12.0G(12.0G)]

混合evacuation阶段2:

[Eden: 0.0B(4096.0M)->0.0B(4096.0M) Survivors: 0.0B->0.0B Heap: 12.0G(12.0G)->12.0G(12.0G)]

最终触发Full 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)] 

Full GC 可以通过减少范围/young代缩小至默认的minimum(Java heap的5%)。你也许会说old代足够容纳3795M的live data set (LDS)。然而, LDS明确耦合于设置young代minimum(4G),压缩7891M以上空间。因此设置threshold为默认的heap的45%(也就是 5529M左右),标记阶段提早触发
,混合回收期间回收很少的空间。heap使用保持增长,其他的标记阶段已经开始,但是标记阶段完成,踢开混合GC,已使用空间在11.3G(正如看到heap第一行的信息)。这次回收遭遇evacuation failure。因此,这个问题陷在 “starting marking cycle too early”上面。

Humongous Allocations

最后一个我想介绍的概念是,也许用户创建许多 humongous objects (H-objs),G1 GC处理H-objs。

辣么,我们为什么需要不同途径去分配H-objs?

如果对象占用Region50%的区域或者更多那么被判定为humongous。分配humongous连续的空间。你可以想象一下,如果G1在Young代分配humongous,而且它们存活很长时间,然后它们需要一些必要而且昂贵(记住H-obj连续的region)的操作,复制这些H-obj导survivor空间,最终这些H-obj升迁到Old代。因此,为了避免上面的操作,H-obj直接分配在Old代,然后分类整理,映射作为humongousregion。

通过在Old代直接分配H-obj,G1避免在任何evacuation阶段包含它们,它们因此也不用移动。Full GC期间,压缩存活的H-obj。Full GC之后,multi-phased concurrent marking期间的清除阶段 死亡的H-obj被回收。换句话说,H-obj在清除阶段被回收,或者说它们在full gc期间被回收。

分配H-obj之前,G1 GC因为分配会交叉执行检查heap使用百分比,标记的threshold。如果允许,G1 GC然后初始化concurrent marking周期。由于我们想避免evacuation failures和可能多的Full GC,这样的方式被执行。结果在没有更过的可用region存活对象evacuations尽可能早检查以便给G1尽量多的时间去完成并发周期。

对于G1 GC,基本前提是没有太多H-obj和他们存活时间很长。然而,由于G1 GC的region尺寸取决于你的heap的 minimum值,它的发生建立在分配的region的尺寸上,你的humongous可以看上去"normal"分配。这会导致需要H-obj分配在Old代的region上,甚至会导致evacuation failure,因为G1赶不上这些humongous分配。

现在你也许在思考如何找到humongous 分配导致evacuation failures的方法。这里,再一次-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]

因此,你可以看到一次并发周期被要求发生由于humongous分配需要4194320 bytes。

这些信息是有用的,因此你不但被告知你的应用有多少humongous分配(不管它们多还是少),而且还告诉你发配的大小。此外,如果你认为过多humongous分配,你能做的就是增加G1的region尺寸作为适合humongous的规则尺寸。因此,例如,分配尺寸仅仅在4M之上。所以,为了让这次分配可以规则分配,我们需要16M的region尺寸。所以,这里推荐明确设置命令行选项:-XX:G1HeapRegionSize=16M。

Note:回想一下我上篇文章,G1 region 跨越1M-32M(2的指数),分配要求稍微超过4M。因此,8M的region的尺寸不足以避免humongous分配。问我们需要下一个2的指数,16MB。

ok。我认为这次我已经讲解了大部分重要问题和G1概念。再一次,谢谢阅读!

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/66367.html

相关文章

  • 译文-G1集器

    摘要:原文出处设计的一个重要目标是设置阶段的持续时长和频率,因为垃圾收集器可预测,可配置。收集器尽自己最大努力高概率实现目标但不是必然,它会是硬实时。因此名称是收集器。运行不同使用独立的收集器。 原文出处:G1 – Garbage First G1设计的一个重要目标是设置stop-the-world阶段的持续时长和频率,因为垃圾收集器可预测,可配置。事实上,G1是一款软实时的收集器,意味着你...

    missonce 评论0 收藏0
  • G1 垃圾集器介绍

    摘要:之前根据的内存管理白皮书介绍了在分代算法中的几个垃圾收集器,本文将介绍垃圾收集器。本节介绍的收集过程,收集器主要包括了以下种操作年轻代收集并发收集,和应用线程同时执行混合式垃圾收集必要时的接下来,我们进行一一介绍。 之前根据 Sun 的内存管理白皮书介绍了在 HotSpot JVM 分代算法中的几个垃圾收集器,本文将介绍 G1 垃圾收集器。 G1 的主要关注点在于达到可控的停顿时间,在...

    nihao 评论0 收藏0
  • G1垃圾集器简述

    摘要:垃圾收集器简述全文共两部分有基础的读者只需要阅读第一部分垃圾收集器在最新几个版本的发展第二部分为基础部分垃圾收集器在最新几个版本的发展垃圾收集器始见于版本在后续的几个版本中对它进行了优化和改进在中垃圾收集器增加了几个可配置选项的自动发现功能 G1垃圾收集器简述 全文共两部分,有基础的读者只需要阅读第一部分G1垃圾收集器在最新几个版本的发展,第二部分为基础部分. G1垃圾收集器在最新几个...

    honhon 评论0 收藏0
  • [译文] JavaScript工作原理:V8引擎内部+5条优化代码的窍门

    摘要:本文将会深入分析的引擎的内部实现。该引擎使用在谷歌浏览器内部。同其他现代引擎如或所做的一样,通过实现即时编译器在执行时将代码编译成机器代码。这可使正常执行期间只发生相当短的暂停。 原文 How JavaScript works: inside the V8 engine + 5 tips on how to write optimized code 几周前我们开始了一个系列博文旨在深入...

    dreamans 评论0 收藏0
  • 【译】深入理解G1的GC日志(一)

    摘要:表示允许垃圾收集线程处理本次垃圾收集开始前没有处理好的日志缓冲区,这可以确保当前分区的是最新的。垃圾收集线程在完成其他任务的时间展示每个垃圾收集线程的最小最大平均差值和总共时间。 本文翻译自:https://www.redhat.com/en/blog/collecting-and-reading-g1-garbage-collector-logs-part-2?source=auth...

    spacewander 评论0 收藏0

发表评论

0条评论

Lycheeee

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<