G1垃圾收集器简述

G1垃圾收集器

由美团技术团队文章总结而成:https://tech.meituan.com/

​ G1是一种服务器端的垃圾收集器,应用在多处理器和大容量内存环境中,在实现高吞吐量的同时,尽可能的满足垃圾收集暂停时间的要求。它是专门针对以下应用场景设计的:

  • 像CMS收集器一样,能与应用程序线程并发执行。
  • 整理空闲空间更快。
  • 需要GC停顿时间更好预测。
  • 不希望牺牲大量的吞吐性能。
  • 不需要更大的Java Heap。

G1收集器的设计目标是取代CMS收集器,它同CMS相比,在以下方面表现的更出色:

  • G1是一个有整理内存过程的垃圾收集器,不会产生很多内存碎片。
  • G1的Stop The World(STW)更可控,G1在停顿时间上添加了预测机制,用户可以指定期望停顿时间。

在传统的垃圾收集器中,STW的时间是无法预测的,G1可实现STW的时间可预测。这也是使用G1垃圾回收器(-XX:+UseG1GC)不得不设置的一个参数:-XX:MaxGCPauseMillis=10。

1. G1的堆内存划分

​ 为了实现STW的时间可预测,G1将堆内存“化整为零”,将堆内存划分成多个大小相等独立区域(Region),每一个Region都可以根据需要变成新生代的Eden空间、Survivor空间,或者老年代空间。收集器能够对扮演不同角色的Region采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果。
在这里插å¥å›¾ç‰‡æè¿°

​ Region可能是Eden,也有可能是Survivor,也有可能是Old,另外Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个Region容量一半(50%)的对象即可判定为大对象。每个Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围为1MB~32MB,且应为2的N次幂。而对于那些超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region之中,G1的进行回收大多数情况下都把Humongous Region作为老年代的一部分来进行看待。

注:G1在逻辑上还是划分Eden、Survivor、Old,但是物理上他们不是连续的。

1.1 SATB

​ 全称是Snapshot-At-The-Beginning,由字面理解,是GC开始时活着的对象的一个快照。它是通过Root Tracing得到的,作用是维持并发GC的正确性。 它是根据三色标记怎么维持并发GC的正确性。

在这里插å¥å›¾ç‰‡æè¿°

G1的运行过程与CMS大体一致,分为以下四个步骤:

  • 初始标记:仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。
  • 并发标记:从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,并发时有引用变动的对象会产生漏标问题,G1中会使用SATB算法来解决,后面会详细介绍。
  • 最终标记:对用户线程做一个短暂的暂停,用于处理并发标记阶段仍遗留下来的最后那少量的SATB记录(漏标对象)。
  • 筛选回收:负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多个收集器线程并行完成的。
  • TAMS是什么?要达到GC与用户线程并发运行,必须要解决回收过程中新对象的分配,所以G1为每一个Region区域设计了两个名为TAMS(Top at Mark Start)的指针,从Region区域划出一部分空间用于记录并发回收过程中的新对象。这样的对象认为它们是存活的,不纳入垃圾回收范围

1.2 三色标记

​ 在三色标记法之前有一个算法叫Mark-And-Sweep(标记清除)。这个算法会设置一个标志位来记录对象是否被使用。最开始所有的标记位都是0,如果发现对象是可达的就会置为1,一步步下去就会呈现一个类似树状的结果。等标记的步骤完成后,会将未被标记的对象统一清理,再次把所有的标记位设置成0方便下次清理。

​ 这个算法最大的问题是GC执行期间需要把整个程序完全暂停,不能实现用户线程和GC线程并发执行。因为在不同阶段标记清扫法的标志位0和1有不同的含义,那么新增的对象无论标记为什么都有可能意外删除这个对象。对实时性要求高的系统来说,这种需要长时间挂起的标记清扫法是不可接受的。所以就需要一个算法来解决GC运行时程序长时间挂起的问题,那就是三色标记法。

三色标记最大的好处是可以异步执行,从而可以以中断时间极少的代价或者完全没有中断来进行整个GC。

三色标记法很简单。首先将对象用三种颜色表示,分别是白色、灰色和黑色。

  • 黑色:表示根对象,或者该对象与它引用的对象都已经被扫描过了。
  • 灰色:该对象本身已经被标记,但是它引用的对象还没有扫描完。
  • 白色:未被扫描的对象,如果扫描完所有对象之后,最终为白色的为不可达对象,也就是垃圾对象。

在这里插å¥å›¾ç‰‡æè¿°

在这里插å¥å›¾ç‰‡æè¿°

由于并发阶段的存在,Mutator和Garbage Collector线程同时对对象进行修改,就会出现白对象漏标的情况:

  • 假设此时,对象A及其引用的对象都已经被扫描完,那么对象A将会被标记为黑色。
  • 用户线程将对象B和对象C之间的引用断开,将对象A指向对象C,此时对象C会被当成垃圾对象,会产生漏标问题,因为对象A不会再被扫描。

漏标问题在CMS和G1收集器中有着不同的解决方案。

  • CMS:采用IncrementalUpdate(增量更新)算法,在并发标记阶段时如果一个白色对象被一个黑色对象引用时,会将黑色对象重新标记为灰色,让垃圾收集器在重新标记阶段重新扫描。
  • G1:采用SATB(snapshot-at-the-beginning),在初始标记时做一个快照,当B和C之间的引用消失时要把这个引用推到GC的堆栈,保证C还能被GC扫描到,在最终标记阶段扫描STAB记录。

两种漏标解决方案的对比:

  • SATB算法关注的是引用的删除(B->C的引用)。
  • Incremental Update算法关注的是引用的增加(A->C 的引用),需要重新扫描,效率低。

1.3 记忆集与卡表

​ 跨代引用:堆空间通常被划分为新生代和老年代。由于新生代的垃圾收集通常很频繁,如果老年代对象引用了新生代的对象,那么回收新生代的话,需要扫描所有从老年代到新生代的所有引用,所以要避免YGC时扫描整个老年代,减少开销。

​ 记忆集全称是Remembered Set,是辅助GC过程的一种结构,典型的空间换时间工具,和卡表有些类似。还有一种数据结构也是辅助GC的:Collection Set(CSet),它记录了GC要收集的Region集合,集合里的Region可以是任意年代的。

在这里插å¥å›¾ç‰‡æè¿°

​ 记忆集(RSet,Remembered Set):用来记录从其他Region中的对象到本Region的引用,是一种抽象的数据结构。每一个Region都设有一个RSet,有了这个数据结构,在回收某个Region的时候,就不必对整个堆内存的对象进行扫描了,它使得部分收集成为了可能。

​ 对于年轻代的Region,它的RSet只保存了来自老年代的引用,这是因为年轻代的回收是针对所有年轻代Region的,没必要画蛇添足。所以说年轻代Region的RSet有可能是空的。

​ 而对于老年代的Region来说,它的RSet也只会保存老年代对它的引用。这是因为老年代回收之前,会先对年轻代进行回收。这时,Eden区变空了,而在回收过程中会扫描Survivor分区,所以也没必要保存来自年轻代的引用。

​ 在做YGC的时候,只需要选定年轻代的RSet作为GC ROOTs,这些RSet记录了old->young的跨代引用,避免了扫描整个老年代。 而mixed gc的时候,老年代中记录了old->old的RSet,young->old的引用从Survivor区获取(老年代回收之前,会先对年轻代进行回收,存活的对象放在Survivor区),这样也不用扫描全部老年代,所以RSet的引入大大减少了GC的工作量。

1.4 停顿预测模型

​ Pause Prediction Model 即停顿预测模型。G1根据这个模型统计计算出来的历史数据来预测本次收集需要选择的Region数量,从而尽量满足用户设定的目标停顿时间。 停顿预测模型是以衰减标准偏差为理论基础实现的。

1.5 G1 GC模式

​ G1提供了两种GC模式,Young GC和Mixed GC,两种都是完全Stop The World的。

  • Young GC:选定所有年轻代里的Region。通过控制年轻代的region个数,即年轻代内存大小,来控制young GC的时间开销。
  • Mixed GC:选定所有年轻代里的Region,外加根据全局并发标记(global concurrent marking)统计得出收集收益高的若干老年代Region。在用户指定的开销目标范围内尽可能选择收益高的老年代Region。

注:Mixed GC不是full GC,它只能回收部分老年代的Region,如果mixed GC实在无法跟上程序分配内存的速度,导致老年代填满无法继续进行Mixed GC,就会使用serial old GC(full GC)来收集整个GC heap。所以我们可以知道,G1是不提供full GC的。

1.6 全局并发标记

全局并发标记 global concurrent marking:为Mixed GC提供标记服务的,并不是一次GC过程的一个必须环节。

  • 初始标记(initial mark,STW)。它标记了从GC Root开始直接可达的对象。
  • 并发标记(Concurrent Marking)。这个阶段从GC Root开始对heap中的对象标记,标记线程与应用程序线程并行执行,并且收集各个Region的存活对象信息。
  • 最终标记(Remark,STW)。标记那些在并发标记阶段发生变化的对象,将被回收。
  • 清除垃圾(Cleanup)。清除空Region(没有存活对象的),加入到free list。

1.7 发生Mixed GC的时机

其实是由一些参数控制着的,另外也控制着哪些老年代Region会被选入CSet。

  • G1HeapWastePercent:在全局并发标记结束之后,我们可以知道old gen regions中有多少空间要被回收,在每次YGC之后和再次发生Mixed GC之前,会检查垃圾占比是否达到此参数,只有达到了,下次才会发生Mixed GC。
  • G1MixedGCLiveThresholdPercent:老年代 region中的存活对象的占比,只有在此参数之下,才会被选入CSet。
  • G1MixedGCCountTarget:一次全局并发标记之后,最多执行Mixed GC的次数。
  • G1OldCSetRegionThresholdPercent:一次Mixed GC中能被选入CSet的最多老年 region数量。

2. 安全点与安全区域

​ 用户线程暂停,GC线程要开始工作,但是要确保用户线程暂停的这行字节码指令是不会导致引用关系的变化。所以JVM会在字节码指令中,选一些指令,作为“安全点”,比如方法调用、循环跳转、异常跳转等,一般是这些指令才会产生安全点。

​ 为什么它叫安全点,GC时要暂停用户线程,并不是抢占式中断(立马把业务线程中断)而是主动式中断。主动式中断是设置一个标志,这个标志是中断标志,各用户线程在运行过程中会不停的主动去轮询这个标志,一旦发现中断标志为 True,就会在自己最近的“安全点”上主动中断挂起。

​ 为什么需要安全区域?要是用户线程都不执行(用户线程处于Sleep或者是Blocked状态),那么程序就没办法进入安全点,对于这种情况,就必须引入安全区域。安全区域是指能够确保在某一段代码片段之中, 引用关系不会发生变化,因此,在这个区域中任意地方开始垃圾收集都是安全的。我们也可以把安全区域看作被扩展拉伸了的安全点。

​ 当用户线程执行到安全区域里面的代码时,首先会标识自己已经进入了安全区域,这段时间里JVM要发起GC就不必去管这个线程了。

​ 当线程要离开安全区域时,它要检查JVM是否已经完成了根节点枚举或者其他GC中需要暂停用户线程的阶段:

  • 如果完成了,那线程就当作没事发生过,继续执行。
  • 否则它就必须一直等待,直到收到可以离开安全区域的信号为止。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值