G1垃圾收集器

https://javadoop.com/post/g1

G1 垃圾收集算法主要应用在多 CPU 大内存的服务中,在满足高吞吐量的同时,尽可能的满足垃圾回收时的暂停时间。G1采用复制方式收集垃圾,不会产生很多的垃圾碎片;G1的Stop The World(STW)更可控,G1在停顿时间上添加了预测机制,用户可以指定期望停顿时间-XX:MaxGCPauseMillis

特点

  • 带压缩的收集器,在回收老年代的分区时,是将存活的对象从一个分区拷贝到另一个可用分区,这个拷贝的过程就实现了局部的压缩
  • 兼顾吞吐量和停顿时间
  • 整体上看是基于“标记整理”算法,局部是基于“复制算法”
  • 最大的特点是引入分区的概念,弱化分代概念
  • 易预测的GC暂停时间

概念

为了达到可预测的GC暂停时间目标,G1使用了几项技术:

heap region

  • 每个region维护一个Remembered Set,记录所有region之外引用当前region对象的指针,只需要扫描RSet就可以对单个region单独回收,这是使用region方式的基础 (谁引用了了我的对象)
  • 每个region被分成了多个card,在不同region中的card会相互引用
  • 在串行和并行收集器中,GC通过整堆扫描,来确定对象是否处于可达路径中。然而G1为了避免STW式的整堆扫描,在每个分区记录了一个已记忆集合(RSet)。当要回收该分区时,通过扫描分区的RSet,来确定引用本分区内的对象是否存活,进而确定本分区内的对象存活情况。

SATB

Mutator说成应用程序。mutator实际上进行的操作有两种

  • 生成对象
  • 更新指针

是GC开始时存活对象的一个快照。它是通过Root Tracing得到的,作用是维持并发GC的正确性

  • 周期性分析全局可达性,提供completeness(完备性?),即保证所有垃圾最后被识别出来;

  • 计算每个region的live数据的数量,G1总是优先收集live数据比较少的region,即优先收集垃圾多的region

  • CMS的incremental update设计使得它在remark阶段必须重新扫描所有线程栈和整个young gen作为root;G1的SATB设计在remark阶段则只需要扫描剩下的satb_mark_queue

    SATB算法创建了一个对象图,它是堆的一个逻辑“快照”。标记数据结构包括了两个位图:previous位图和next位图。previous位图保存了最近一次完成的标记信息,并发标记周期会创建并更新next位图,随着时间的推移,previous位图会越来越过时,最终在并发标记周期结束的时候,next位图会将previous位图覆盖掉。

收集集合(CSet)

  • 一组可被回收的分区的集合。在CSet中存活的数据会在GC过程中被移动到另一个可用分区,CSet中的分区可以来自Eden空间、survivor空间、或者老年代。CSet会占用不到整个堆空间的1%大小。

它同CMS相比,在以下方面表现的更出色:

  • G1是一个有整理内存过程的收集器,不会产生很多内存碎片。
  • G1的STW更可控,G1在停顿时间上添加了预测机制,用户也可以指定期望停顿时间,关注最小时延

G1收集器的收集活动主要有四种操作:

  • YongGC
  • 并发标记周期
  • MixedGC
  • 必要时候的Full GC

YoungGC

  • 存活对象拷贝到Survivor 区
  • 存活时间达到年龄阈值时,对象晋升到 Old 区
  • Young GC:选定所有年轻代里的region。通过控制年轻代的region个数,即年轻代内存大小,来控制young GC的时间开销。
  • 停下来所有的工作线程,然后将Eden上存活的对象拷贝到Suvivor区域,这里会将很多个对象从多个不同的区域拷贝到少数的几个区域内,所以这一步在G1中叫做疏散(Evacuation),同时把Suvivor上触及年龄阈值的对象晋升到老年代区域。
  • STW (对应上边日志中的`Pause Young)

Evacuation Pauses

满足一定条件后(后面会解释),G1停止所有的线程,开始一次evacuation pause:选取一个region集合,称为collection set(后面称为CSet),然后这个region集合中的活对象拷贝到堆的其他region中,释放CSet占用的空间。这里需要STW(stop the world)是为了做compact,因为对象的移动对mutator来说必须是原子的,在并发环境下要满足这种原子性代价比较高。

为了尽量缩短evacuation pause的时间,G1尽量利用多线程去处理。第一步是串行选择CSet,然后进入主要的并行阶段。GC线程竞争获取任务:扫描remember set log,更新RSet;扫描RSet和其他GC root,得到活对象;撤离活对象。这些任务只要保证只有一个任务在执行,不需要显式的同步。

CSet中可能只包含young region,也可能是所有young region和部分non-young region的混合,两者的处理方式是一致的,分别对应fully youngpartially youngevacuation pause模式

Concurrent Marking

并发标记很重要,在G1中主要有两个功能:保证收集完备性和存活数据信息。G1使用snatshot at the beginning(SATB)并发标记算法,基本思想是保证识别出所有标记开始时刻的垃圾,提供那个时间点对象的一个snapshot。标记过程成新产生的对象之间标记为活的,不需要trace,这大大减少了并发标记的消耗。

  • 在并发收集周期中,至少有一次(或多次)新生代垃圾收集
  • 一些分区被标记为X,这些分区属于老年代,它们就是标记周期找出的包含最多垃圾的分区

并发标记周期包括多个阶段:

采用的算法是我们前文提到的SATB标记算法,产出是找出一些垃圾对象最多的老年代分区。

  1. 初始标记 GC pause (young) (initial-mark)
    1. STW
    2. 标记根
    3. 通常初始标记阶段会跟一次新生代收集一起进行,既然这两个阶段都需要暂停应用,G1 GC就重用了新生代收集来完成初始标记的工作
    4. 在新生代垃圾收集中进行初始标记的工作,会让停顿时间稍微长一点,并且会增加CPU的开销。初始标记做的工作是设置两个TAMS变量(NTAMS和PTAMS)的值,所有在TAMS之上的对象在这个并发周期内会被识别为隐式存活对象;
  2. 根分区扫描(root-region-scan)
    1. 在初始标记或新生代收集中被拷贝到survivor分区的对象,都需要被看做是根,这个阶段G1开始扫描survivor分区,所有被survivor分区所引用的对象都会被扫描到并将被标记。
    2. survivor分区就是根分区,正因为这个,该阶段不能发生新生代收集
  3. 并发标记阶段(concurrent-mark)
    1. 默认情况下,G1垃圾收集器会将这个线程总数设置为并行垃圾线程数的四分之一;并发标记会利用trace算法找到所有活着的对象,并记录在一个bitmap中,因为在TAMS之上的对象都被视为隐式存活,因此我们只需要遍历那些在TAMS之下的
    2. 记录在标记的时候发生的引用改变,SATB的思路是在开始的时候设置一个快照,然后假定这个快照不改变,根据这个快照去进行trace,这时候如果某个对象的引用发生变化,就需要通过pre-write barrier logs将该对象的旧的值记录在一个SATB缓冲区中,如果这个缓冲区满了,就把它加到一个全局的列表中——G1会有并发标记的线程定期去处理这个全局列表。
  4. 重新标记阶段(remarking),重新标记阶段是最后一个标记阶段,需要暂停整个应用,G1垃圾收集器会处理掉剩下的SATB日志缓冲区和所有更新的引用,同时G1垃圾收集器还会找出所有未被标记的存活对象。这个阶段还会负责引用处理等工作。
    1. STW
  5. 清理阶段(cleanup),清理阶段真正回收的内存很小,截止到这个阶段,G1垃圾收集器主要是标记处哪些老年代分区可以回收,将老年代按照它们的存活度(liveness)从小到大排列。

并发标记很重要,在G1中主要有两个功能:保证收集完备性和存活数据信息。G1使用snatshot at the beginning(SATB)并发标记算法,基本思想是保证识别出所有标记开始时刻的垃圾,提供那个时间点对象的一个snapshot。标记过程成新产生的对象之间标记为活的,不需要trace,这大大减少了并发标记的消耗。

2019-08-19T20:58:30.327-0800: 4.999: [GC concurrent-root-region-scan-start]
2019-08-19T20:58:30.330-0800: 5.003: [GC concurrent-root-region-scan-end, 0.0037160 secs]
2019-08-19T20:58:30.330-0800: 5.003: [GC concurrent-mark-start]
2019-08-19T20:58:30.343-0800: 5.016: [GC concurrent-mark-end, 0.0127121 secs]
2019-08-19T20:58:30.343-0800: 5.016: [GC remark 2019-08-19T20:58:30.344-0800: 5.016: [Finalize Marking, 0.0002255 secs] 2019-08-19T20:58:30.344-0800: 5.016: [GC ref-proc, 0.0000974 secs] 2019-08-19T20:58:30.344-0800: 5.017: [Unloading, 0.0023743 secs], 0.0029011 secs]
 [Times: user=0.01 sys=0.00, real=0.00 secs] 
2019-08-19T20:58:30.347-0800: 5.020: [GC cleanup 36M->32M(700M), 0.0006370 secs]
 [Times: user=0.01 sys=0.00, real=0.00 secs] 
2019-08-19T20:58:30.348-0800: 5.020: [GC concurrent-cleanup-start]
2019-08-19T20:58:30.348-0800: 5.020: [GC concurrent-cleanup-end, 0.0000123 secs]
2019-08-19T20:58:31.580-0800: 6.253: [GC pause (Metadata GC Threshold) (young) (initial-mark), 0.0139133 secs]

MixedGC

回收整个 young region,回收一部分的 old region

  • Mixed GC:选定所有年轻代里的region,外加根据global concurrent marking统计得出收集收益高的若干老年代region。在用户指定的开销目标范围内尽可能选择收益高的老年代 region。
  • G1的老年代回收是在老年代空间触及一个阈值(Initiating Heap Occupancy Percent)之后,这个回收伴随着年轻代的回收工作

MixedGC时机

  • InitiatingHeapOccupancyPercent 默认45%

  • G1HeapWastePercent

    • 在global concurrent marking结束之后,可以知道区有多少空间要被回收,在每次YGC之后和再次发生Mixed GC之前,会检查垃圾占比是否达到此参数,只有达到了,下次才会发生Mixed GC。
  • G1MixedGCLiveThresholdPercent

    • Old区的region被回收的时候的存活对象占比
  • G1MixedGCCountTarget

    • 一次global concurrent marking之后,最多执行Mixed GC的次数
  • G1OldGCSetRegionThresholdPercent

    • 一次Mixed GC中能被选入CSet的最多old区的region数量
  • 混合收集只会回收一部分老年代分区

  • 混合收集会执行多次,一直运行到(几乎)所有标记点老年代分区都被回收

  • 当整个堆的使用率超过指定的百分比时,G1 GC会启动新一轮的并发标记周期。在混合收集周期中,对于要回收的分区,会将该分区中存活的数据拷贝到另一个分区,这也是为什么G1收集器最终出现碎片化的频率比CMS收集器小得多的原因——以这种方式回收对象,实际上伴随着针对当前分区的压缩

-个假想的混合的STW时间线:
启动程序
-> young GC 
-> young GC
-> young GC
-> young GC + initial marking
(.... concurrent marking ...
-> young GC (.... concurrent marking ..
(.... concurrent marking ...
-> young GC (... concurrent marking ...
-> final marking
-> cleanup
-> mixed GC
-> mixed GC
-> mixed GC
-> mixed GC
-> young GC + initial marking
(.... concurrent marking ...

举个例子,一个典型的G1执行过程如下图:

0)当全局并发标记正在工作时,G1不会选择做mixed GC;反之如果有mixed GC正在进行中G1也不会启动initial marking

1)在最开始的3次GC都是young GC,日志中表现为:GC pause (young)

2)堆占用率达到IHOP,开始并发标记阶段,由于初始标记过程借用young GC,日志中表现为:GC pause (young) (initial-mark)

3)并发标记阶段与应用线程并发执行,期间可能会有多次young GC

4)并发标记结束,执行最终标记(GC remark)与清理(GC cleanup)

5)为了在混合收集时尽可能多地回收老年代,在清理暂停之后还会有一次young GC

6)之后就是4次mixed GC过程,日志中表现为:GC pause (mixed)

万物有始有终,混合收集周期结束的条件(以下任一情况满足):

1)mixed GC的次数达到-XX:G1MixedGCCountTarget,默认为8。在标记完所有老年代分区后,G1不是将其一次性全部收集,首先按选项-XX:G1MixedGCLiveThreadsholdPercent指定的存活率阈值筛选一批老年代分区(默认85%),再按剩余分区的收集效率从大到小排序,再除以-XX:G1MixedGCCountTarget,将每次mixed GC收集的老年代Region数量平均拆分

2)上次mixed GC执行完后,剩余老年代垃圾的占比数小于-XX:G1HeapWastePercent,默认为5%。意思是G1会容忍一定的老年代垃圾,从而提升混合收集周期的效率

FullGC

G1的出现就是为了避免这个情况的

如果对象内存分配速度过快,mixed gc 来不及回收,导致老年代被填满,就会触发一次 full gc,G1 的 full gc 算法就是单线程执行的 serial old gc,会导致异常长时间的暂停时间,需要进行不断的调优,尽可能的避免 full gc。

G1的最佳实践

  1. 关键参数项
  • -XX:+UseG1GC,告诉JVM使用G1垃圾收集器
  • -XX:MaxGCPauseMillis=200,设置GC暂停时间的目标最大值,这是个柔性的目标,JVM会尽力达到这个目标
  • -XX:INitiatingHeapOccupancyPercent=45,如果整个堆的使用率超过这个值,G1会触发一次并发周期。记住这里针对的是整个堆空间的比例,而不是某个分代的比例。
  • 添加吞吐量 -XX:GCTimeRatio=99
  1. 最佳实践
  • 避免使用-Xmn、-XX:NewRatio设置年轻代的大小

    • 通过-Xmn显式设置年轻代的大小,会干扰G1收集器的默认行为:
    • 这样G1不再以设定的暂停时间为目标
    • 这样G1不能按需扩大或缩小年轻代的大小
  • 停顿时间目标:暂停时间不要太严苛,其吞吐量目标是90%的应用程序和10%的垃圾回收时间,太严苛会直接影响到吞吐量;

  • 是否需要切换到G1

    • 50%以上的堆被存活的对象占用
    • 对象分配和晋升的速度变化非常大
    • 垃圾回收时间特别长,超过1秒
    • G1适合内存比较大的情况下
      • CMS的fullGC会扫描整个老年代

G1调优:

  • G1 的调优相对简单、直观,因为可以直接设定暂停时间等目标,并且其内部引入了各种智能的自适应机制

打印日志相关参数:

-XX:+DisableExplicitGC
-XX:+HeapDumpOnOutOfMemoryError 
-XX:HeapDumpPath=$CATALINA_HOME/logs/ 
-XX:+PrintGCDetails 
-XX:+PrintGCTimeStamps 
-XX:+PrintGCDateStamps 
-Xloggc:$CATALINA_HOME/logs/gc.log
-Xms128M
-Xmx128M

-XX:+UseG1GC 开启 G1
-XX:G1HeapRegionSize=n, Region 的大小,1-32M,最多2048个
-XX:MaxGCPauseMillis=200 最大停顿时间
-XX:G1NewSizePercent、-XX:G1MaxNewSizePercent
-XX:G1ReservePercent=10 保留防止 to space溢出
-XX:ParallelGCThreads=n SWT线程数
-XX:ConcGCThreads=n 并发线程数=1/4*并行

G1日志格式

  1. 分析调优结果,主要关注:

Full GC 执行时间
Minor GC 执行时间
Full GC 执行间隔
Minor GC 执行间隔
Entire Full GC 执行时间
Entire Minor GC 执行时间
Entire GC 执行时间
Full GC 执行时间

Minor GC 执行时间

参考:https://mp.weixin.qq.com/s/Shw0jtVse1QqNbFCyYmfZA

与CMS比较

这个现在是垃圾回收器的标配,G1和CMS也不例外。但是G1同时回收老年代和年轻代,而CMS只能回收老年代,需要配合一个年轻代收集器。另外G1的分代更多是逻辑上的概念,G1将内存分成多个等大小的region,Eden/ Survivor/Old分别是一部分region的逻辑集合,物理上内存地址并不连续。

CMS在old gc的时候会回收整个Old区,对G1来说没有old gc的概念,而是区分Fully young gcMixed gc,前者对应年轻代的垃圾回收,后者混合了年轻代和部分老年代的收集,因此每次收集肯定会回收年轻代,老年代根据内存情况可以不回收或者回收部分或者全部(这种情况应该是可能出现)。

1. 如何处理跨代引用

在垃圾回收的时候都是从Root开始搜索,这会先经过年轻代再到老年代,对于年轻代引用老年代的这种跨代不需要单独处理。但是老年代引用年轻代的会影响young gc,这种跨代需要处理。
为了避免在回收年轻代的时候扫描整个老年代,需要记录老年代对年轻代的引用,young gc的时候只要扫描这个记录。CMS和G1都用到了Card Table,但是用法不太一样。JVM将内存分成一个个固定大小的card,然后有一个专门的数据结构(即这里的Card Table)维护每个Card的状态,一个字节对应一个Card,有点像内存page的概念,只是page是硬件上的,Card Table是软件上的。当一个Card上的对象的引用发生变化的时候,就将这个Card对应的Card Table上的状态置为dirty,young gc的时候扫描状态是dirtyCard即可。这是基本的用法,CMS基本上就是这么使用。
G1在Card Table的基础上引入的remembered set(下面简称RSet)。每个region都会维护一个RSet,记录着引用到本region中的对象的其他region的Card。比如A对象在regionA,B对象在regionB,且B.f = A,则在regionA的RSet中需要记录B所在的Card的地址。这样的好处是可以对region进行单独回收,这要求RSet不只是维护老年代到年轻代的引用,也要维护这老年代到老年代的引用,对于跨代引用的每次只要扫描这个region的RSet上的Card即可。
上面说过年轻代到老年代的引用不需要单独处理,这带来了很大的性能上的提升,因为年轻代的对象引用变化很大,如果都需要记录下来成本会很高。同时也说明只需要在老年代维护Card Table

2. Full GC

导致CMS Full GC的可能原因主要有两个:Promotion FailureConcurrent Mode Failure,前者是在年轻代晋升的时候老年代没有足够的连续空间容纳,很有可能是内存碎片导致的;后者是在并发过程中jvm觉得在并发过程结束前堆就会满了,需要提前触发Full GC。CMS的Full GC是一个多线程STW的Mark-Compact过程,,需要尽量避免或者降低频率。
G1的初衷就是要避免Full GC的出现,Full GC会会对所有region做Evacuation-Compact,而且是单线程的STW,非常耗时间。导致G1 Full GC的原因可能有两个:1. Evacuation的时候没有足够的to-space来存放晋升的对象;2. 并发处理过程完成之前空间耗尽。这两个原因跟CMS类似。

3.Region 拷贝压缩

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值