G1垃圾收集器

概念

G1收集器开创了面向局部收集的设计思路和基于region的内存布局。前代的所有包括CMS在内的其他收集器,垃圾收集的目标范围是整个新生代(Minor GC)、整个老年代(Major GC)或者是整个Java堆(Full GC)。G1收集器可以面向堆内存任何部分来组成回收集CSet(Collection Set)进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多并且回收收益最大,这也是G1的Garbage First名字的由来。给它设定的目标是在延迟可控的情况下获得尽可能高的吞吐量

G1仍然保留新生代和老年代的概念,但新生代和老年代不再是固定的了,它们都是一系列区域(不需要连续)的动态集合。G1把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间和Survivor空间,或者老年代空间。收集器能够对扮演不同角色的Region采用不同的策略去处理。Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象。

引用标记问题

1.G1将Java堆分成多个独立Region后,在进行可达性标记的过程,Region里面存在的跨Region引用对象导致需要全堆扫描,如何解决?
使用记忆集。
由于G1垃圾收集器将内存分成了不同的region,因此在垃圾标记和回收时,不可避免的会有跨Region引用的情况产生:一个region中存储的对象可能被其他任意region(这些region可能Old区或者Eden区)中的对象所引用。这样一来,在进行YGC的时候,在判断Eden区中的一个对象是否存活时,需要去扫描所有的region(包括Old区,Eden区等),导致了在回收年轻代的时候,还需要扫描老年代,同时扫描表示所有Eden区和Old区的region,相当于做了一个全堆扫描,这会大大降低YGC的效率。(在CMS等分代回收的垃圾回收器中,也存在跨代引用的问题,即如果老年代对象引用了新生代的对象,那么回收新生代时需要扫描从老年代到新生代的所有引用),也是使用卡表来解决的。

为了解决上述问题,通常采用记忆集(Remembered Set,RSet)来避免全堆扫描。记忆集在不同的垃圾收集器中的实现方式不同,G1采用卡表的形式来实现记忆集,具体方式如下:

(1)每个Region初始化时,会初始化一个remembered set

(2)RSet里面记录了引用——其他Region中指向本Region中所有对象的所有引用

(3)RSet其实是一个Hash Table,Key是其他的Region的起始地址,Value是一个集合,里面的元素是引用了本region对象的对象所在的卡页的起始地址。

引用关系的记录方式通常有两种方式:「我引用了谁」和「谁引用了我」,前一种记录简单,但是在回收时需要对记录集做全部扫描,后一种用空间换时间,但是在回收时只需要关注对象本身,即可通过 RSet 直接定位到引用关系。G1 的 RSet 使用的是后一种「谁引用了我」的记录方式,其数据结构可本质上是一个哈希表。每次向引用类型字段赋值时,会触发:「写屏障 -> 线程队列 -> 全局队列 -> 并发 RSet 更新」这样一个过程。在垃圾收集时,无需遍历所有的Region,只需要从记忆集取出Card卡页内存块中包含的跨代指针,将跨代引用的对象加入GCRoot

写后屏障是无条件记录的,可以记录下所有从老年代到老年代,老年代到新生代,新生代到老年代,新生代到新生代的引用。G1中的记忆集是在youngGC的时候发挥作用,记忆集中只有老年代到新生代的引用得到了处理并使用

另一个问题就是何时更新RSet,G1会采用post-write barrier(写后屏障)来完成RSet的更新,即引用字段赋值后同时通过JVM的post-write barrier机制完成记忆集状态的更新。G1 中的写屏障分为 pre_write_barrierpost_write_barrier,其中 SATB机制使用了pre_write_barrier ,RSet使用了post_write_barrier。如下面的代码所示,应用 field 将要被赋予新值 value,由于 field 指向的旧的引用对象会丢失引用关系,因此在赋值之前会触发 pre_write_barrier,更新 SATB 日志记录,记录下引用关系变化时旧的引用值,在并发标记阶段结束后扫描;在正式赋值之后,会执行 post_write_barrier,更新被引用对象所在的RSet,即下面代码的步骤3。

// 赋值操作,将 value 赋值给 field 所在的引用
void assign_new_value(oop* field, oop value) {  
  pre_write_barrier(field);         // 步骤1:更新 SATB 日志记录
  *field = value;                   // 步骤2:引用赋值
  post_write_barrier(field, value); // 步骤3:更新引用对象所在的RSet
}

2.引用关系改变,用户线程和GC线程同时运行的情况可能导致漏标,如何解决?
使用原始快照。为了实现原始快照搜索(SATB)(Snapshot-At-The-Beginning,SATB)算法,还需要使用写前屏障来跟踪并发时的指针变化情况。

SATB 的过程可以简单理解为:当并发标记阶段引用的关系发生变化时,旧引用所指向的对象就会被标记,同时其子引用对象也会被递归标记,这样快照的完整性就得到保证了。SATB 的记录更新是由 pre_write_barrier 写屏障触发的,下面是 G1 论文中介绍的 SATB 原始表述,具体实现时,还是由两级的队列结构缓存,再由并发标记线程批量处理进入标记队列satb_mark_queue的记录。

void pre_write_barrier(oop* field) {  
  oop old_value = *field;  
  if (old_value != null) {  
    if ($gc_phase == GC_CONCURRENT_MARK) {
      $current_thread->satb_mark_queue->enqueue(old_value);  
    }  
  }  
}

3.巨型对象
大小超过 50% 标准 region 大小的对象称为巨型对象(Humongous Object)。当线程为巨型对象分配空间时,不能简单在TLAB进行分配,因为巨型对象的移动成本很高,而且有可能一个分区不能容纳巨型对象。因此,巨型对象会直接在老年代分配所占用的连续空间称为巨型分区(Humongous Region)。G1内部做了一个优化,一旦发现没有引用指向巨型对象,则可直接在年轻代收集周期中被回收。

由于需要确定一片连续的内存空间需要扫描整堆,因此确定巨型对象开始位置的成本非常高,如果可以,应用程序应避免生成巨型对象。

GC过程

G1的GC操作可以分为三种:Young GC,并发标记周期和Mixed GC。

YGC

与其他收集器的新生代gc类似,G1的Young GC也是采用标记-复制-清除算法。G1的Young GC并不是说现有的Eden区放满了就会马上触发,G1会计算下现在Eden区回收大概要多久时间,如果回收时间远远小于参数 -XX:MaxGCPauseMills 设定的值,那么增加年轻代的region,继续给新对象存放,不会马上做Young GC;直到下一次Eden区放满,G1计算回收时间接近参数 -XX:MaxGCPauseMills 设定的值,那么就会触发Young GC。
也会回收没有使用的巨大对象的区域。

G1 always sizes the young generation at the end of a normal young collection for the next mutator phase. This way, G1 can meet the pause time goals that were set using -XX:MaxGCPauseTimeMillis and -XX:PauseTimeIntervalMillis based on long-term observations of actual pause time.

If not otherwise constrained, then G1 adaptively sizes the young generation size between the values that -XX:G1NewSizePercent and -XX:G1MaxNewSizePercent determine to meet pause-time.

Only specifying one of these latter options fixes young generation size to exactly the value passed with -XX:NewSize and -XX:MaxNewSize respectively. This disables pause time control. 通过这两个参数指定年轻代大小,会导致动态调整年轻代大小的特性失效。

并发标记周期

1.初始标记
需要STW。在CMS中需要STW,在G1中是在Minor GC的时候一起执行。标记可能引用老年代代中对象的幸存者region(根区域)。
2.根区域扫描
扫描幸存者region以获取对老年代的引用。该阶段必须在年轻 代GC 发生之前完成。

3.并发标记
从GC Root并发遍历扫描对象图,标记所有的存活对象。该阶段不用stop the world,因此会使用三色标记法以及SATB机制来保证标记的正确性。当对象图扫描完成以后,还要重新处理SATB(Snapshot-At-The-Beginning,SATB)记录下的在并发时有引用变动的对象。
4.最终标记
需要STW,对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。如果不引入一个采用了STW的最终标记(Final Marking)的过程,那么新的引用变更会不断产生,永远就无法达成完成标记的条件。
5.筛选回收
负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间制定回收计划
这个阶段并不会实际去做垃圾的回收,也不会执行存活对象的拷贝。
执行的操作有:
1.RSet梳理:启发式算法会根据活跃度和RSet尺寸对分区定义不同等级,有助于发现无用的引用。

Scrubs the Remembered Sets. (Stop the world)

2.整理堆分区:为混合收集识别回收收益高(基于释放空间和暂停目标)的老年代分区集合

Performs accounting on live objects and completely free regions. (Stop the world)

3.识别所有空闲分区:即发现无存活对象的分区,该分区可在清除阶段直接回收,无需等待下次收集周期。

Reset the empty regions and return them to the free list. (Concurrent)

Mixed GC

Mixed GC是指目标为收集整个新生代以及部分老年代的垃圾收集。目前只有G1收集器会有这种行为。成功完成并发标记周期后, 若是老年代的堆占有率达到参数(-XX:InitiatingHeapOccupancyPercent)设定的值则触发Mixed GC流程,回收所有的Young和部分Old(根据期望的GC停顿时间确定old区垃圾收集的优先顺序)以及大对象区,Mixed GC过程主要使用复制算法,把各个region中存活的对象拷贝到别的region里去,拷贝过程中如果发现没有足够的空region能够承载拷贝对象就会触发一次Full GC

Full GC

在垃圾收集和处理过程中,还有几种情况下会触发Full GC:
(1)并发模式失效
G1启动并发标记周期但是在混合gc之前,老年代就被填满了,这时候G1就会放弃标记周期,改为执行Full gc,对应的gc日志为:[GC concurrent-mark-abort]
解决办法:发生这种失败意味着堆的大小应该增加了,或者G1收集器的后台处理应该更早开始,或者需要调整周期,让它运行得更快(如增加后台处理的线程数)。
(2)晋升失败
G1在进行新生代gc时老年代没有足够的内存提供给晋升对象,将会触发Full gc。对应的gc日志为:to-space exhausted。解决这种问题的方式是:
a. 增加 -XX:G1ReservePercent 选项的值(并相应增加总的堆大小),为“目标空间”增加预留内存量。
b. 通过减少 -XX:InitiatingHeapOccupancyPercent 提前启动标记周期
c. 也可以通过增加 -XX:ConcGCThreads 选项的值来增加并行标记线程的数目
(3) 疏散失败
进行新生代gc时,survivor和老年代没有足够的空间容纳存活的对象。对应的gc日志为: to-space overflow。解决办法与晋升失败的情况是一样的。
(4) 巨型对象分配失败
巨型对象分配失败也会触发Full gc,解决办法:增大regionSize,就不会被认为是巨型对象,走正常的GC。
(5)metaspace gc
metaspace大小达到阈值(metaspaceSize大小,是动态的),会触发Full gc。解决办法:增大metaspace大小

Full GC会对整堆做标记清除和压缩,最后将只包含纯粹的存活对象。由于G1的应用场合往往堆内存都比较大,所以Full GC的收集代价非常昂贵,应该避免Full GC的发生

可预测的停顿时间

为什么G1能做到这一点呢?G1回收是选择一些Region进行回收,而不是整代内存来回收,这是G1跟其它GC非常不同的一点。其它GC每次回收都会回收整个Generation的内存(Eden, Old), 而回收内存所需的时间就取决于内存的大小以及实际垃圾的多少所以垃圾回收时间是不可控的;而G1每次并不会回收整代内存,到底回收多少内存就看用户配置的暂停时间,配置的时间短就少回收点,配置的时间长就多回收点。在G1的参数中,用户可以通过指定-XX:MaxGCPauseMillis参数来指定G1的停顿时间。

G1收集器要怎么做才能预测并控制停顿时间呢?G1的停顿时间的预测模型是以衰减均值(Decaying Average)为理论基础的,在垃圾收集过程中,G1收集器会根据region的统计数据计算得出region的回收成本和收益,同时再通过这些信息预测现在开始回收的话,由哪些Region组成回收集才可以在不超过用户设置的期望停顿时间的条件下获得最高的收益,以此来规划出满足用户指定停顿时间的垃圾收集步骤

问题

1.InitiatingHeapOccupancyPercent控制什么时候进行垃圾收集,默认为45%,那么是整个堆内存占用率到达45%,还是老年代内存占用超过45%?
参考文章
2.G1的起始快照是为了解决GC线程和用户线程并发运行导致的漏标问题,起始快照执行的流程是什么样的?
根据三色标记的处理过程,漏标必须要同时满足以下两个条件:

  • 赋值器插入了一条或者多条从黑色对象到白色对象的新引用;
  • 赋值器删除了全部从灰色对象到该白色对象的直接或间接引用。

原始快照破坏的是第二个条件,当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,并发扫描结束后,在将这些记录重新扫描一次。

3.G1的Full GC会同时回收年轻代和老年代吗?
是。

4.并发标记周期会将年轻代的对象加入GcRoot吗?如果是,会使用记忆集吗?
是。在全局并发标记的initial marking阶段是借用youngGC的暂停完成的,因为youngGC和mixedGC的initial marking阶段都要扫描根集合。
判断新生代对老年代的引用不会使用记忆集

5.TAMS指针的机制是什么?

6.并发标记周期和Mixed GC的关系是什么?
年轻代收集和混合收集周期,是G1回收空间的主要活动。当应用运行开始时,堆内存可用空间还比较大,只会在年轻代满时,触发年轻代收集;随着老年代内存增长,当到达IHOP阈值-XX:InitiatingHeapOccupancyPercent(老年代占整堆比,默认45%)时,G1开始着手准备收集老年代空间首先经历并发标记周期识别出高收益的老年代分区。但随后G1并不会马上开始一次混合收集,而是让应用线程先运行一段时间,等待触发一次年轻代收集。在这次STW中,G1将保准整理混合收集周期。接着再次让应用线程运行,当接下来的几次年轻代收集时,将会有老年代分区加入到CSet中,即触发混合收集,这些连续多次的混合收集称为混合收集周期(Mixed Collection Cycle)。

单次的混合收集与年轻代收集并无二致。根据暂停目标,老年代的分区可能不能一次暂停收集中被处理完,G1会发起连续多次的混合收集,称为混合收集周期(Mixed Collection Cycle)。G1会计算每次加入到CSet中的分区数量混合收集进行次数,并且在上次的年轻代收集、以及接下来的混合收集中,G1会确定下次加入CSet的分区集(Choose CSet),并且确定是否结束混合收集周期

7.什么是evacuate
对象的复制或者移动

CMS和G1对比

虽然G1有不少优秀的特性,但是G1在垃圾收集时的内存占用每个region都有记忆集)和程序额外负载(维护记忆集和SATB的写屏障)都比CMS要高,因此具体用什么垃圾收集器还是要从各方面考虑。一般来说,小内存应用上,CMS会比G1更占优;而在大内存的服务器上(6G以上的内存),G1垃圾收集器能够发挥出更大的优势。

参考

G1详细过程
G1
G1
G1 GC
G1数据结构
G1介绍

  • 24
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值