G1真的这么强大嘛?

介绍

我们知道,CMS收集器会有两个极端的缺点。
在Minor GC时,由于Surviver救助空间放不下了,多余的对象会提升(promotion)到老年代,但是老年代由于垃圾收集的缘故,产生了大量的内存碎片,无法分配足够的内存给这些对象,因此会产生concurrent mode failure的错误,这个时候,会降级到Serail Old收集器进行垃圾收集,这就会产生promotion failed。

这就将一次简单的Major GC演化成了非常耗时的Full GC,而且,无法预测停顿时间。
而G1垃圾收集器就是应对这一无法预测停顿时间而产生的强大利器,它可以根据设定的停顿时间为目标,尽量达成这个目标,它能够推算出本次要收集的大体区域,以增量的方式完成收集。

G1 的全称是 Garbage­First GC,就是为了干掉CMS的。

深入

G1采用弱化了分代概念,采用了一种“分而治之”的思想,将堆划分为多个大小一致的区间(Region),它的数值是在 1M 到 32M 字节之间的一个 2 的幂值数,region大小可通过-XX:G1HeapRegionSize=520M 配置,不配置的话,则会通过计算得到,这篇文章有介绍到。每个区间既可以是新生代,也可以是老年代,G1也是有 Eden 区和 Survivor 区的概念的,只不过它们在内存上不是连续的,而是由一小份一小份组成的。

但是当有非常大的对象需要分配内存时,普通的region是难以放下的。
G1有一个专门针对大对象的region,就是下面的Humongous区,对象大小超过 Region 50% 的对象,将会在这里分配。
下面的图片很直观的展示出来了。
在这里插入图片描述

怎么回收垃圾

有没有疑惑,它在回收的时候,到底回收哪些region区呢?是随机的还是按某个规律呢?

其实,它优先回收垃圾最多的region区,这应该是它名字的由来吧:Garbage ­First

回收的过程

首先了解一下几个名词

  1. RSet
    它其实是一个以空间换时间的结构。
    其实,在老年代引用了年轻代时,老年代会有一个卡表(card table)记录了相关的引用,属于points-out(我引用了谁的对象)。
    而RSet的功能与此类似,它的全称是 Remembered Set,用于记录和维护 Region 之间的对象引用关系。但是又和这个有所不同,RSet记录了其他 Region 中的对象引用本 Region 中对象的关系,属于 points-into 结构(谁引用了我的对象)。

    G1垃圾收集器里每一个RSet对应的是一个Region内部对象引用情况,说白了就是存在Region中存活对象的指针。在标记存活对象的时候,G1使用RSet概念,将每个分区指向分区内的引用记录在该分区,避免对整个堆扫描,并行独立处理垃圾集合。它使得部分收集成为了可能。
    RSet主要保存下面的引用映射关系:

  • 老年代对年轻代的引用,维护老年代分区指向年轻代分区的指针
  • 老年代对老年代的引用。在这里,老年代中不同分区的指针将被维护在老年代拥有分区的RSet中
    如下图:我们可以看到3个分区,X(年轻代分区)、Y和Z(老年代分区)。X有一个来自Z和Y的对内引用。这个引用记录在X的RSet中,分区Y有2个对内引用,一个来自X一个来自Z,因为年轻代分区作为一个整体回收的,这时,Eden 区变空了,而在回收过程中会扫描 Survivor 分区,也没必要保存来自年轻代的引用,所以只需记录来自Z的对内引用,不用记录X的对内引用。
    在这里插入图片描述
    对于年轻代的 Region,它的 RSet 只保存了来自老年代的引用,这是因为年轻代的回收是针对所有年轻代 Region 的,没必要搞这个。所以说年轻代 Region 的 RSet 有可能是空的。

RSet 通常会占用很大的堆内存空间,大约 5% 或者更高。不仅仅是空间方面,很多计算开销也是比较大的。

事实上,为了维护 RSet,程序运行的过程中,写入某个字段就会产生一个 post-write barrier 。为了减少这个开销,将内容放入 RSet 的过程是异步的,而且经过了很多的优化:Write Barrier 把脏卡信息存放到本地缓冲区(local buffer),有专门的 GC 线程负责收集,并将相关信息传给被引用 Region 的 RSet。

参数 -XX:G1ConcRefinementThreads 或者 -XX:ParallelGCThreads 可以控制这个异步的过程。如果并发优化线程跟不上缓冲区的速度,就会在用户进程上完成。

  1. CSet
    这个比较好理解,它的全称是 Collection Set,即收集集合,也就是在垃圾收集暂停过程中被回收的目标。GC时在CSet中的所有存活数据(Live Data)都会被转移,分区释放回空闲分区队列。
    如图显示,左边的年轻代收集CSet代表年轻代的一部分分区,右边的混合收集CSet代表年轻代的一部分区和老年代的多个分区:
    在这里插入图片描述
  2. IHOP

InitiatingHeapOccupancyPercent,简称IHOP。缺省情况是Java堆内存的45%。当老年代的空间超过45%,G1会启动一次混合周期收集

这也是G1和CMS之间较大的区别,G1的百分比是相对于整个Java堆而言的,CMS(CMSInitiatingOccupancyFraction)仅仅是针对老年代空间的占比。
》》 这是因为G1没有固定物理上分割一块内存作为老年代,而是用了Region的思想,这些Region可能是eden,survivor、老年代或者巨型分区,所以获取针对老年代本身的占用百分比没有意义

G1 的垃圾回收过程

G1是在逻辑上分为年轻代和老年代,但它的年轻代和老年代比例,并不是那么固定,为了达到 MaxGCPauseMillis 所规定的效果,G1 会自动调整两者之间的比例。

当你强行使用 -Xmn 或者 -XX:NewRatio 去设定它们的比例的话,我们给 G1 设定的这个目标将会失效。

G1垃圾回收时会有三个阶段:

  1. “年轻代”的垃圾回收,也叫 Minor GC,这个过程和普通的差不多,发生时机就是 Eden 区满的时候。
  2. “老年代”的垃圾收集,严格来说不算是收集,它是一个“并发标记”的过程,顺便清理了一点点对象。
  3. 真正的清理,发生在“混合模式”,它不止清理年轻代,还会将老年代的一部分区域进行清理,上面那张图片可以看到。
    三个的如图所示:
    在这里插入图片描述

在 GC 日志里,这个过程描述特别有意思,(1)的过程,叫作 [GC pause (G1 Evacuation Pause) (young),而(2)的过程,叫作 [GC pause (G1 Evacuation Pause) (mixed)。Evacuation 是转移的意思,和 Copy 的意思有点类似。
这三种模式之间的间隔也是不固定的。比如,1 次 Minor GC 后,发生了一次并发标记,接着发生了 5 次 Mixed GC或者更多。

1. 年轻代回收
是一个 STW 的过程,它的跨代引用使用 RSet 数据结构来追溯,会一次性回收掉年轻代的所有 Region。
在应用刚启动,流量慢慢的进来了,开始生成对象。G1会选一个分区并指定它为Eden分区,当这块分区用满了之后,G1会选一个新的分区作为Eden分区,这个操作会一直进行下去直到达到Eden分区上限,也就是说Eden分区已经被占满,那么会触发一次年轻代收集.
回收过程:

首先做的就是迁移存活对象,它使用单eden,双survivor进行复制算法,它将存活的对象从eden分区转移到survivor分区,survivor分区内的某些对象达到了任期阈值之后,会晋升到老年代分区中。原有的年轻代分区会被整个回收掉。这里面的存活对象的判定依靠了RSet,当存在有老年代应用新生代的时候,它就会被判定为存活对象。

同时,年轻代收集还负责维护对象年龄,存活对象经历过年轻代收集总次数等信息。G1将晋升对象的尺寸总和和它们的年龄信息维护到年龄表中,结合年龄表、survivor占比(–XX:TargetSurvivorRatio 缺省50%)、最大任期阈值(–XX:MaxTenuringThreshold 缺省为15)来计算出一个合适的任期阈值

这里有个小细节:为什么最大任期阈值为15?
我在网上找到的答案说是:因为采用4bit来保存这个数据,所以最大为15.

调优:我们可以通过–XX:MaxGCPauseMillis,调优年轻代收集,缩小暂停时间

最后还会处理 Soft、Weak、Phantom、Final、JNI Weak 等引用。结束收集。

2. 并发标记
当整个堆内存使用达到一定比例(也就是IHOP,默认是 45%),并发标记阶段就会被启动。这个比例也是可以调整的,通过参数 -XX:InitiatingHeapOccupancyPercent 进行配置。
它其实是为了后面的混合收集提供标记服务的,并不是一次 GC 过程的必须环节。

G1的并发标记循环分5个阶段:

  1. 初始标记

这个过程共用了 年轻代收集的STW暂停,这是因为它们可以复用 Root Scan 操作。虽然是 STW 的,但是时间通常非常短。

  1. 根区间扫描

标记所有幸存者区间的对象引用

  1. 并发标记

这个阶段从 GC Roots 开始对堆中的对象标记,标记线程与应用程序线程并行执行,并且收集各个 Region 的存活对象信息。

  1. 重新标记

标记那些在并发标记阶段发生变化的对象,STW,完成所有标记工作。

  1. 清除

这个过程不需要 STW。如果发现 Region 里全是垃圾,在这个阶段会立马被清除掉。不全是垃圾的 Region,并不会被立马处理,它会在 Mixed GC 阶段,进行收集。并将回收的Region加入可用Region队列。

调优:
可以通过–XX:InitiatingHeapOccupancyPercent,配置适合应用的IHOP值(过大会可能转移失败,过小可能过早引起并发标记周期);
也可以通过–XX:ConcGCThreads,增加并发线程数。

如果在并发标记阶段,又有新的对象发生了变化,该怎么办?

这个由算法 SATB 保证的。SATB 的全称是 Snapshot At The Beginning,它作用是保证在并发标记阶段的正确性。具体我也没看嘻嘻。

3. 混合回收
当达到IHOP参数并完成上一小节的并发标记周期之后,混合收集周期就开始启动了,它是能并发清理老年代中的整个整个的Region是一种最优情形。混合收集过程,不只清理年轻代,还会将一部分老年代区域也加入到 CSet 中。

混合收集周期包含多次收集次数。那么什么影响收集次数呢?是固定的?还是?有两个参数比较重要:

  • -XX:G1MixedGCCountTarget:缺省值为8,意思是能启动混合收集的数目设定一个物理限制。G1根据将回收的老年分区除以该参数值得到每次混合收集的老年代CSet最小数量。
  • -XX:G1HeapWastePercent:缺省值为5%,每次混合收集暂停,G1算出废物百分比,根据堆废物百分比,当收集达到参数时,不再启动新的混合收集。因为通过 Concurrent Marking 阶段,我们已经统计了老年代的垃圾占比。在 Minor GC 之后,如果判断这个占比达到了某个阈值,下次就会触发 Mixed GC。因为这种情况下, GC 会花费很多的时间但是回收到的内存却很少。所以这个参数也是可以调整 Mixed GC 的频率的。

调优:当暂停时间和运行时间呈现指数级增长,可以通过-XX:G1HeapWastePercent,调高该参数会有所帮助,但这也导致更多碎片化

猜你喜欢

转载自blog.csdn.net/qq_41257365/article/details/107381058
G1