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 的内存模型
![](https://img.haomeiwen.com/i2708793/2234b49bdbb8b202.png)
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) 为单位。
![](https://img.haomeiwen.com/i2708793/cb835de990206b62.png)
2.3 巨形对象 Humongous Region
一个大小达到甚至超过分区大小一半的对象称为巨型对象 (Humongous Object)。因为巨型对象的移动成本很高,而且有可能一个分区不能容纳巨型对象。因此,巨型对象会直接在老年代分配,所占用的连续空间称为巨型分区 (Humongous Region)。G1 内部做了一个优化,一旦发现没有引用指向巨型对象,则可直接在年轻代收集周期中被回收。
巨型对象会独占一个、或多个连续分区,其中第一个分区被标记为开始巨型 (StartsHumongous),相邻连续分区被标记为连续巨型 (ContinuesHumongous)。由于需要一片连续的内存空间需要扫描整堆,因此确定巨型对象开始位置的成本非常高,如果可以,应用程序应避免生成巨型对象。
2.4 已记忆集合 Remember Set (RSet)
在串行和并行收集器中,GC 通过整堆扫描,来确定对象是否处于可达路径中。然而 G1 为了避免 STW 式的整堆扫描,在每个分区记录了一个已记忆集合 (RSet),内部类似一个反向指针,记录引用分区内对象的卡片索引。当要回收该分区时,通过扫描分区的 RSet,来确定引用本分区内的对象是否存活,进而确定本分区内的对象存活情况。
![](https://img.haomeiwen.com/i2708793/f5d90e2f932b362d.png)
1、Rset 记录当前 Region 被谁(其他 Region)引用;
2、每个卡 card 在卡表 card table 中对应一个比特位,当老年代中的某个对象,持有新生代对象的引用时,就把这个对象对应的 Card 所在的位置标记为 dirty (bit 位设置为 1),这样在 Minor GC 时就不用扫描整个老年代,而是扫描 Card 为 Dirty 对应的那些内存区域。
2.5 收集集合 (CSet)
CSet 收集示意图
![](https://img.haomeiwen.com/i2708793/9a90af480a43dc3f.jpeg)
收集集合 (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。
![](https://img.haomeiwen.com/i2708793/0fea1c5aefecc31a.png)
3.2 混合收集
年轻代收集不断活动后,老年代的空间也会被逐渐填充。当老年代占用空间超过整堆比阈值 -XX:InitiatingHeapOccupancyPercent
(默认 45%) 时,G1 就会启动一次混合垃圾收集周期。
为了满足暂停目标,G1 可能不能一口气将所有的候选分区收集掉,因此 G1 可能会产生连续多次的混合收集与应用线程交替执行,每次 STW 的混合收集与年轻代收集过程相类似。
![](https://img.haomeiwen.com/i2708793/84dfab10f6f43098.png)
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 的次数会相应增加。
![](https://img.haomeiwen.com/i2708793/68010988ac4494ed.jpeg)
减少 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) 收集的方式。即每次收集既可能只收集年轻代分区 (年轻代收集),也可能在收集年轻代的同时,包含部分老年代分区 (混合收集),这样即使堆内存很大时,也可以限制收集范围,从而降低停顿。
网友评论