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的时候(对老年代region回收也是同理),在判断Eden区中的一个对象是否存活时,需要去扫描所有的region(包括Old区,Eden区等),导致了在回收年轻代的时候,还需要扫描老年代,同时扫描表示所有Eden区和Old区的region,相当于做了一个全堆扫描,这会大大降低YGC的效率。(在CMS等分代回收的垃圾回收器中,也存在跨代引用的问题,即如果老年代对象引用了新生代的对象,那么回收新生代时需要扫描从老年代到新生代的所有引用),也是使用卡表来解决的。

为了解决上述问题,通常采用记忆集(Remembered Set,RSet)来避免全堆扫描。记忆集在不同的垃圾收集器中的实现方式不同,G1采用卡表的形式来实现记忆集,具体方式如下:
(1)G1将Java堆划分为
相等大小
的一个个区域,这个小的区域大小是512 Byte,称为Card(卡页)。并维护了一个字节数组Card Table,Card Table的数组下标映射着每一个Card,一个Card的内存中通常包含不止一个对象,只要Card内有一个(或更多)对象的字段存在着跨代指针,那就将对应卡表的数组元素的值标识为1,称为这个元素变脏(Dirty),没有则标识为0。

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

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

(4)RSet其实是一个Hash Table,Key是其他的Region的起始地址,Value是一个集合,里面的元素是Card Table 数组中的index,既Card对应的Index,映射到对象的Card地址

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

另一个问题就是何时更新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);  
    }  
  }  
}

GC过程

G1的GC操作可以分为三种:Young GC,**并发标记周期(Old 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。

Old GC

1.初始标记
标记所有从GC根对象直接可达的对象。在CMS中需要STW,在G1中是在Minor GC的时候一起执行。并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象

2.并发标记
从GC Root并发遍历扫描对象图,标记所有的存活对象。该阶段不用stop the world,因此会使用三色标记法以及SATB机制来保证标记的正确性。当对象图扫描完成以后,还要重新处理SATB(Snapshot-At-The-Beginning,SATB)记录下的在并发时有引用变动的对象。此过程和用户线程并发执行
3.最终标记
对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。如果不引入一个采用了STW的最终标记(Final Marking)的过程,那么新的引用变更会不断产生,永远就无法达成完成标记的条件。
4.筛选回收
负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。

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。

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吗?如果是,会使用卡表吗?

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),并且确定是否结束混合收集周期

CMS和G1对比

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

参考

G1详细过程
G1
G1

相关推荐

  1. G1垃圾收集

    2024-03-11 03:34:02       21 阅读
  2. JVM垃圾收集之CMS垃圾收集G1垃圾收集

    2024-03-11 03:34:02       22 阅读
  3. 如何选择G1收集与CMS收集

    2024-03-11 03:34:02       13 阅读
  4. JVM-GC-G1垃圾回收

    2024-03-11 03:34:02       8 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-03-11 03:34:02       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-03-11 03:34:02       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-03-11 03:34:02       18 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-03-11 03:34:02       20 阅读

热门阅读

  1. 使用Golang开发以太坊(一)

    2024-03-11 03:34:02       23 阅读
  2. 【Vue3】Ref 和 ShallowRef 的区别

    2024-03-11 03:34:02       29 阅读
  3. MySQL和Redis Common Command

    2024-03-11 03:34:02       27 阅读
  4. 什么是生活?(2024-2-26)

    2024-03-11 03:34:02       27 阅读
  5. vim基本使用

    2024-03-11 03:34:02       26 阅读
  6. 京东面试官问我,你在catch块中写业务代码吗?

    2024-03-11 03:34:02       32 阅读
  7. Docker容器管理的内容与作用

    2024-03-11 03:34:02       27 阅读
  8. 鸿蒙os开发做全局路由拦截

    2024-03-11 03:34:02       37 阅读
  9. WPF自定义快捷命令

    2024-03-11 03:34:02       26 阅读
  10. web蓝桥杯真题:冰墩墩心情刻度尺

    2024-03-11 03:34:02       29 阅读