G1 垃圾回收器

1、概述

G1 垃圾回收器(Garbage-First)并不新,是在 Java 7 update 4 时引入的一个新的垃圾回收器。官方在 ZGC 还没有出现时也推荐使用 G1 来代替选择 CMS。

G1 最大的特点是引入分区的思路,弱化了分代的概念,合理利用垃圾收集各个周期的资源,解决了其他收集器的众多缺陷。G1 回收器和 CMS 比起来,有以下不同:

  • G1垃圾回收器是 compacting 的,因此其回收得到的空间是连续的。这避免了 CMS 回收器因为不连续空间所造成的问题,例如:需要更大的堆空间,更多的 floating garbage。连续空间意味着 G1 垃圾回收器可以不必采用空闲链表的内存分配方式,而可以直接采用 bump-the-pointer 的方式;

  • G1 回收器的内存与 CMS 回收器要求的内存模型有极大的不同。G1 将内存划分一个个固定大小的 region,每个 region 即可以是年轻代,也可以是老年代的。内存的回收是以 region 作为基本单位的

  • G1 还有一个及其重要的特性:软实时(soft real-time)。所谓的实时垃圾回收,是指在要求的时间内完成垃圾回收。“软实时”则是指,用户可以指定垃圾回收时间的限时,G1 会努力在这个时限内完成垃圾回收,但是 G1 并不担保每次都能在这个时限内完成垃圾回收。通过设定一个合理的目标,可以让达到 90% 以上的垃圾回收时间都在这个时限内。

2、G1 的内存模型

G1 的内存模型

2.1 分区 Region

G1 采用了分区 (Region) 的思路,将整个堆空间分成若干个大小相等的内存区域,每次分配对象空间将逐段地使用内存。因此,在堆的使用上,G1 并不要求对象的存储一定是物理上连续的,只要逻辑上连续即可;每个分区也不会确定地为某个代服务,可以按需在年轻代和老年代之间切换。启动时可以通过参数 -XX:G1HeapRegionSize=n 可指定分区大小 (1MB~32MB,且必须是2的幂),默认将整堆划分为 2048 个分区。

2.2 卡片 Card

在每个分区内部又被分成了若干个大小为 512 Byte 卡片 (Card),标识堆内存最小可用粒度。所有分区的卡片,将会记录在全局卡片表 (Global Card Table) 中,分配的对象会占用物理上连续的若干个卡片,当查找分区内对象的引用时,便可通过卡片来查找该引用对象 (见 RSet)。每次对内存的回收,都是对指定分区的卡片进行处理。

总之,G1 对内存的使用以分区 (Region) 为单位,而对对象的分配则以卡片 (Card) 为单位。

image

2.3 巨形对象 Humongous Region

一个大小达到甚至超过分区大小一半的对象称为巨型对象 (Humongous Object)。因为巨型对象的移动成本很高,而且有可能一个分区不能容纳巨型对象。因此,巨型对象会直接在老年代分配,所占用的连续空间称为巨型分区 (Humongous Region)。G1 内部做了一个优化,一旦发现没有引用指向巨型对象,则可直接在年轻代收集周期中被回收。

巨型对象会独占一个、或多个连续分区,其中第一个分区被标记为开始巨型 (StartsHumongous),相邻连续分区被标记为连续巨型 (ContinuesHumongous)。由于需要一片连续的内存空间需要扫描整堆,因此确定巨型对象开始位置的成本非常高,如果可以,应用程序应避免生成巨型对象。

2.4 已记忆集合 Remember Set (RSet)

除了 Card Table 数组之外,每个 Region 还会有一个 RSet 数据结构。RSet 主要用来记录哪个 Region 的哪个 Card 上的对象引用了本 Region 中的对象。

实际实现中,RSet 默认是一个 HashMap,Map 的 key 是引用的 Region,value 是一个 List,List 中存储引用 Region 中的引用 Card 列表。Region、Card Table 以及 RSet 的示意图如下所示:

RSet

上图中,RegionA 和 RegionB 中分别有对象引用 RegionC 中的对象,在 RegionC 对应的 RSet 就会记录这样的引用关系。该 RSet 中有两个 KV 对,第一个 KV 的 key 是 RegionA,value 是一个列表,列表中有两个元素 3 和 65534,分别代表 RegionA 中引用对象在对应 Card Table 中的下标。第二个 KV 对的 key 是 RegionB,value 中列表只有一个元素 1565,代表 RegionB 中引用对象在对应 Card Table 中的下标。

2.5 收集集合 (CSet)

CSet 收集示意图

CSet

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

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

由上述可知,G1 的收集都是根据 CSet 进行操作的,年轻代收集与混合收集没有明显的不同,最大的区别在于两种收集的触发条件。

3、G1 的收集过程

3.1 年轻代收集

G1 的 YoungGC 和 CMS 的 Young GC,其标记-复制全过程 STW。

Young GC

3.2 混合收集

年轻代收集不断活动后,老年代的空间也会被逐渐填充。当老年代占用空间超过整堆比阈值 -XX:InitiatingHeapOccupancyPercent (默认 45%) 时,G1 就会启动一次混合垃圾收集周期。

为了满足暂停目标,G1 可能不能一口气将所有的候选分区收集掉,因此 G1 可能会产生连续多次的混合收集与应用线程交替执行,每次 STW 的混合收集与年轻代收集过程相类似。

混合收集

G1 的混合回收过程可以分为标记阶段、清理阶段和复制阶段。

标记阶段停顿分析

  • 初始标记阶段:初始标记阶段是指从 GC Roots 出发标记全部直接子节点的过程,该阶段是 STW 的。由于 GC Roots 数量不多,通常该阶段耗时非常短。
  • 并发标记阶段:并发标记阶段是指从 GC Roots 开始对堆中对象进行可达性分析,找出存活对象。该阶段是并发的,即应用线程和 GC 线程可以同时活动。并发标记耗时相对长很多,但因为不是 STW,所以我们不太关心该阶段耗时的长短。
  • 再标记阶段:重新标记那些在并发标记阶段发生变化的对象。该阶段是 STW 的。

清理阶段停顿分析

  • 清理阶段清点出有存活对象的分区和没有存活对象的分区,该阶段不会清理垃圾对象,也不会执行存活对象的复制。该阶段是 STW 的。

复制阶段停顿分析

  • 复制算法中的转移阶段需要分配新内存和复制对象的成员变量。转移阶段是 STW 的,其中内存分配通常耗时非常短,但对象成员变量的复制耗时有可能较长,这是因为复制耗时与存活对象数量与对象复杂度成正比。对象越复杂,复制耗时越长。

四个 STW 过程中,初始标记因为只标记 GC Roots,耗时较短。再标记因为对象数少,耗时也较短。清理阶段因为内存分区数量少,耗时也较短。转移阶段要处理所有存活的对象,耗时会较长。因此,G1 停顿时间的瓶颈主要是标记-复制中的转移阶段 STW。为什么转移阶段不能和标记阶段一样并发执行呢?主要是 G1 未能解决转移过程中准确定位对象地址的问题。

4、参数优化

上文简单介绍了 G1 的工作原理,知道原理后,在我们实际使用 G1 过程中,再配合一些常用参数的设置,就能更好的优化程序的运行。

-XX:MaxGCPauseMillis

GC 最大暂停时间,默认 200ms。这是一个软性目标,G1会尽量达成,如果达不成,会逐渐做自我调整。

对于 Young GC,会逐渐减少 Eden 区个数,减少 Eden 空间那么 Young GC 的处理时间就会相应减少。

对于 Mixed GC,G1 会调整每次 Cset 的比例,默认最大值是 10%,当然每次选择的 Cset 少了,所要经历的 Mixed GC 的次数会相应增加。

Cset

减少 Eden 的总空间时,就会更加频繁的触发 Young GC,也就会加快 Mixed GC 的执行频率,因为 Mixed GC 是由 Young GC 触发的,或者说借机同时执行的。频繁 GC 会对对应用的吞吐量造成影响,每次 Mixed GC 回收时间太短,回收的垃圾量太少,可能最后 GC 的垃圾清理速度赶不上应用产生的速度,那么可能会造成串行的 Full GC,这是要极力避免的。

所以暂停时间肯定不是设置的越小越好,当然也不能设置的偏大,转而指望 G1 自己会尽快的处理,这样可能会导致一次全部并发标记后触发的 Mixed GC 次数变少,但每次的时间变长,STW 时间变长,对应用的影响更加明显。

-XX:G1NewSizePercent 和 -XX:G1MaxNewSizePercent

新生代比例有两个数值指定,下限:-XX:G1NewSizePercent,默认值 5%,上限:-XX:G1MaxNewSizePercent,默认值 60%。

G1 会根据实际的 GC 情况 (主要是暂停时间) 动态的调整新生代的大小,主要是 Eden Region 的个数。最好是 Eden 的空间大一点,因为 Young GC 的频率更高,大的 Eden 空间能够降低 Young GC 的发生次数。但同时也需要平衡好 Mixed GC 中新生代和老年代的 Region,如果 Eden 很大,那么留给老年代回收空间就不多了,最后可能会导致 Full GC。

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

-XX:G1MixedGCLiveThresholdPercent

指定被纳入 Cset 中 Region 的存活空间占比阈值,默认 85%。在全局并发标记阶段,如果一个 Region 的存活对象的空间占比低于此值,才有可能被纳入 Cset。

此值直接影响到 Mixed GC 选择回收的区域,当发现 GC 时间较长时,可以尝试调低此阈值,尽量优先选择回收垃圾占比高的 Region,但此举也可能导致垃圾回收的不够彻底,最终触发 Full GC。

-XX:InitiatingHeapOccupancyPercent

指定触发全局并发标记的老年代使用占比,默认值 45%,也就是老年代占堆的比例超过 45%。

如果 Mixed GC 结束后老年代使用率还是超过 45%,那么会再次触发全局并发标记过程,这样就会导致频繁的老年代 GC,影响应用吞吐量。同时老年代空间不大,Mixed GC 回收的空间肯定是偏少的。如果此值太高,很容易导致年轻代晋升失败而触发 Full GC,所以需要多次调整测试。

5、总结

G1 是一款非常优秀的垃圾收集器,不仅适合堆内存大的应用,同时也简化了调优的工作。通过主要的参数初始和最大堆空间、以及最大容忍的 GC 暂停目标,就能得到不错的性能。

  • G1 的设计原则是首先收集尽可能多的垃圾 (Garbage First)。因此,G1 并不会等内存耗尽的时候开始垃圾收集,而是在内部采用了启发式算法,在老年代找出具有高收集收益的分区进行收集。同时 G1 可以根据用户设置的暂停时间目标自动调整年轻代和总堆大小,暂停目标越短,年轻代空间越小、总空间就越大;
  • G1 采用内存分区 (Region) 的思路,将内存划分为一个个相等大小的内存分区,回收时则以分区为单位进行回收,存活的对象复制到另一个空闲分区中。由于都是以相等大小的分区为单位进行操作,因此 G1 天然就是一种压缩方案 (局部压缩);
  • G1 虽然也是分代收集器,但整个内存分区不存在物理上的年轻代与老年代的区别,也不需要完全独立的 survivor 堆做复制准备。G1 只有逻辑上的分代概念,或者说每个分区都可能随 G1 的运行在不同代之间前后切换;
  • G1 的收集都是 STW 的,但年轻代和老年代的收集界限比较模糊,采用了混合 (mixed) 收集的方式。即每次收集既可能只收集年轻代分区 (年轻代收集),也可能在收集年轻代的同时,包含部分老年代分区 (混合收集),这样即使堆内存很大时,也可以限制收集范围,从而降低停顿。

你可能感兴趣的:(G1 垃圾回收器)