G1收集器

248 阅读8分钟

G1收集器

1.回顾

之前的收集器特点:

  • 年轻代、老年代是独立且连续的内存块;
  • 年轻代收集使用单eden、双survivor进行复制算法;
  • 老年代收集必须扫描整个老年代区域;
  • 都是以尽可能少而块地执行GC为设计原则。

2.概述

G1(Garbage First)垃圾收集器是当今垃圾回收技术最前沿的成果之一。早在JDK7就已加入JVM的收集器大家庭中,成为HotSpot重点发展的垃圾回收技术。同优秀的CMS垃圾回收器一样,G1也是关注最小时延的垃圾回收器,也同样适合大尺寸堆内存的垃圾收集,官方也推荐使用G1来代替选择CMS。G1最大的特点是引入分区的思路,弱化了分代的概念,合理利用垃圾收集各个周期的资源,解决了其他收集器甚至CMS的众多缺陷。

3.G1内存模型

3.1 分区(Region)

G1采取了分区思路,将内存分为若干个大小相等的内存区域,每次分配对象的时候逐段使用内存。每个分区也不会确定为哪个分区进行服务,可以在年老代,年轻代切换。

启动时可以通过参数-XX:G1HeapRegionSize=n可指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为2048个分区。

3.2 卡片(Card)

每个Region会被分成若干个大小为512byte的Card,分配的对象会占用物理上连续的若干个卡片,当查找对分区内对象的引用时便可通过记录卡片来查找该引用对象(见RSet)。每次对内存的回收,都是对指定分区的卡片进行处理。

3.3 堆(Heap)

G1同样可以通过-Xms/-Xmx来指定堆空间大小。当发生年轻代收集或混合收集时,通过计算GC与应用的耗费时间比(吞吐量),自动调整堆空间大小。如果GC频率太高,则通过增加堆尺寸,来减少GC频率,相应地GC占用的时间也随之降低;目标参数-XX:GCTimeRatio即为GC与应用的耗费时间比,G1默认为9,而CMS默认为99,因为CMS的设计原则是耗费在GC上的时间尽可能的少。另外,当空间不足,如对象空间分配或转移失败时,G1会首先尝试增加堆空间,如果扩容失败,则发起担保的Full GC。Full GC后,堆尺寸计算结果也会调整堆空间。

3.4 分代(Generation)

G1依然使用了分代的思想。与其他垃圾收集器类似,G1将内存在逻辑上划分为年轻代和老年代,其中年轻代又划分为Eden空间和Survivor空间。但年轻代空间并不是固定不变的,当现有年轻代分区占满时,JVM会分配新的空闲分区加入到年轻代空间。

  • XX:G1NewSizePercent(默认整堆5%) > 整个年轻代内存会在初始空间 <
    -XX:G1MaxNewSizePercent(默认60%)之间动态变化,

  • 且由参数目标暂停时间-XX:MaxGCPauseMillis(默认200ms)、需要扩缩容的大小以及分区的已记忆集合(RSet)计算得到。

  • G1也可以设置固定的年轻代大小(参数-XX:NewRatio、-Xmn),但同时暂停目标将失去意义。

4. G1用到的数据结构以及算法

4.1 已记忆集合 Remember Set(Rset)

G1为了避免STW式的整堆扫描,在每个分区记录了一个已记忆集合(RSet),内部类似一个反向指针,记录引用分区内对象的卡片索引。当要回收该分区时,通过扫描分区的RSet,来确定引用本分区内的对象是否存活,进而确定本分区内的对象存活情况。

并非所有的引用都需要记录在RSet中,如果一个分区确定需要扫描,那么无需RSet也可以无遗漏的得到引用关系。那么引用源自本分区的对象,当然不用落入RSet中;同时,G1 GC每次都会对年轻代进行整体收集,因此引用源自年轻代的对象,也不需要在RSet中记录。最后只有老年代的分区可能会有RSet记录,这些分区称为拥有RSet分区(an RSet’s owning region)。

Rset内部使用Per Region Table (PRT)记录分区情况,如果一个分区非常"受欢迎",那么RSet占用的空间会上升,从而降低分区的可用空间。

应对这个问题采用了改变RSet的密度的方式,在PRT中将会以三种模式记录引用:

  • 稀少:直接记录引用对象的卡片索引
  • 细粒度:记录引用对象的分区索引
  • 粗粒度:只记录引用情况,每个分区对应一个比特位 由上可知,粗粒度的PRT只是记录了引用数量,需要通过整堆扫描才能找出所有引用,因此扫描速度也是最慢的。

4.2 收集集合 Cset

收集集合(CSet)代表每次GC暂停时回收的一系列目标分区。在任意一次收集暂停中,CSet所有分区都会被释放,内部存活的对象都会被转移到分配的空闲分区中。因此无论是年轻代收集,还是混合收集,工作的机制都是一致的。年轻代收集CSet只容纳年轻代分区,而混合收集会通过启发式算法,在老年代候选回收分区中,筛选出回收收益最高的分区添加到CSet中。

候选老年代分区的CSet准入条件,可以通过活跃度阈值-XX:G1MixedGCLiveThresholdPercent(默认85%)进行设置,从而拦截那些回收开销巨大的对象;同时,每次混合收集可以包含候选老年代分区,可根据CSet对堆的总大小占比-XX:G1OldCSetRegionThresholdPercent(默认10%)设置数量上限。

4.3 本地分配缓存(Generation)

每个应用线程和GC线程都会独立的使用分区,进而减少同步时间,提升GC效率,这个分区称为本地分配缓冲区(Lab)。

应用线程可以独占一个本地缓冲区(TLAB)来创建的对象,而大部分都会落入Eden区域(巨型对象或分配失败除外),因此TLAB的分区属于Eden空间;而每次垃圾收集时,每个GC线程同样可以独占一个本地缓冲区(GCLAB)用来转移对象,每次回收会将对象复制到Suvivor空间或老年代空间;对于从Eden/Survivor空间晋升(Promotion)到Survivor/老年代空间的对象,同样有GC独占的本地缓冲区进行操作,该部分称为晋升本地缓冲区(PLAB)。

G1的内存使用是一Region为单位的,对象的分配是以Card为单位的。

4.3 STAB

在开始标记的时候生成一个快照图标记存活对象,在并发标记的时候把所有write barrier 里引用指向的对象都变成非白的。

5.收集器

5.1 年轻代收集器

当JVM分配对象是Eden区满了,则会触发一次STW式的年轻代收集器。在年轻代收集中, Eden区的存活的对象将被拷贝到Survivor分区,原来Survivor分区存活的对象,将被晋升到本地缓存区,新的Survivor分区和old区。原来的年轻代分区直接回收。(复制算法)

5.2 Mix收集器

年轻代经过活动后,老年代也会被填充,当老年代的内存大小超过-XX:InitiatingHeapOccupancyPercent(默认45%)时,G1会启动一次混合GC。

-XX:G1MixedGCCountTarget(默认8) 混合周期的最大次数 -XX:G1HeapWastePercent(默认5%) 堆废物百分比

JVM会通过混合周期的最大次数和老年代分区总数,确定每次包含CSet的最小分区数。 JVM会根据堆废物百分比,当收集未达到百分比时,不启动混合收集。

Mix收集去分为5个阶段
1.初始阶段:标记GC Root引用对象和Root所在的Region区(整个过程是STW的)
2.根分区扫描:这个阶段GC的线程可以和应用线程并发运行。其主要扫描初始标记以及之前YoungGC对象转移到的Survivor分区,并标记Survivor区中引用的对象。
3.并发标记:会并发标记所有非完全空闲的分区的存活对象,也即使用了SATB算法,标记各个分区。所有标记必须在堆满之前完成,否则会触发一次串行Full GC。
4.重新标记:主要处理SATB缓冲区,以及并发标记阶段未标记到的漏网之鱼(存活对象),会STW。
5.清除:主要是Rset梳理,整理堆空间,识别所有的空分区。(STW)
注意点:
  • 当触发mix收集时,并不会直接执行,而时会等下一次年轻代收集器,利用年轻代收集器的STW时间,完成初始标记。
  • 根分区扫描必须在下一次年轻代垃圾收集启动前完成,因为GC每次都会产生新的存活对象。
  • -XX:+ClassUnloadingWithConcurrentMark会开启一个优化,如果一个类不可达,则会在重新标记阶段直接卸载

6. Full GC

G1在对象复制/转移失败或者没法分配足够内存(比如巨型对象没有足够的连续分区分配)时,会触发FullGC。FullGC使用的是stop the world的单线程的Serial Old模式,所以一旦触发FullGC则会STW应用线程,并且执行效率很慢。JDK 8版本的G1是不提供Full gc的处理的。对于G1 GC的优化,很大的目标就是没有FullGC。