17G1垃圾回收器原理
JDK9 之后默认的垃圾回收器是 G1(Garbage First)垃圾回收器。Parallel Scavenge 关注吞吐量,允许用户设置最大暂停时间,但是会减少年轻代可用空间的大小;CMS 关注暂停时间,但是吞吐量方面会下降。而 G1 的设计目标就是将上述两种垃圾回收器的优点融合:
- 支持巨大的堆空间回收,并有较高的吞吐量。
- 支持多 CPU 并行垃圾回收。
- 允许用户设置最大暂停时间。
JDK9 之后强烈建议使用 G1 垃圾回收器。
1.G1 内存结构
G1 出现之前的垃圾回收器,年轻代和老年代一般是连续的,内存结构如下图:

G1 的整个堆会被划分成多个大小相等的区域,称之为区 Region,区域不要求是连续的。分为 Eden、Survivor、Old 三种类型。

Region 的大小通过 堆空间大小 / 2048 计算得到,也可以通过参数 -XX:G1HeapRegionSize=32m 指定(其中 32m 指定 region 大小为 32M)。Region size 必须是 2 的指数幂,取值范围从 1M 到 32M。
2.G1 垃圾回收的两种方式
G1 垃圾回收有两种方式:
- 年轻代回收(Young GC):回收 Eden 区和 Survivor 区中不用的对象。
- 混合回收(Mixed GC):回收所有年轻代和部分老年代的对象以及大对象区。

3.年轻代回收(Young GC)
年轻代回收(Young GC)回收 Eden 区和 Survivor 区中不用的对象,会导致 STW。G1 中可以通过参数 -XX:MaxGCPauseMillis=n(默认 200)设置每次垃圾回收时的最大暂停时间毫秒数,G1 垃圾回收器会尽可能地保证暂停时间。
3.1执行流程
- 新创建的对象会存放在 Eden 区。当 G1 判断年轻代区不足(max 默认 60%),无法分配对象时需要回收时会执行 Young GC。

- 标记出 Eden 和 Survivor 区域中的存活对象。
- 根据配置的最大暂停时间选择某些区域,将存活对象复制到一个新的 Survivor 区中(年龄 +1),清空这些区域。

G1 在进行 Young GC 的过程中会去记录每次垃圾回收时每个 Eden 区和 Survivor 区的平均耗时,以作为下次回收时的参考依据。这样就可以根据配置的最大暂停时间计算出本次回收时最多能回收多少个 Region 区域了。比如 -XX:MaxGCPauseMillis=n(默认 200),每个 Region 回收耗时 40ms,那么这次回收最多只能回收 4 个 Region。

- 后续 Young GC 时与之前相同,只不过 Survivor 区中存活对象会被搬运到另一个 Survivor 区。

-
当某个存活对象的年龄到达阈值(默认 15),将被放入老年代。
-
部分对象如果大小超过 Region 的一半,会直接放入老年代,这类老年代被称为 Humongous 区。比如堆内存是 4G,每个 Region 是 2M,只要一个大对象超过了 1M 就被放入 Humongous 区,如果对象过大会横跨多个 Region。

3.2年轻代回收核心技术
G1 年轻代回收的核心技术包含三个部分:
1. 卡表(Card Table)
每一个 Region 都拥有一个自己的卡表。卡表是一个字节数组,如果产生了跨代引用(老年代引用年轻代),G1 会将卡表上引用对象所在的位置字节内容进行修改为 0,称为脏卡。卡表的主要作用是生成记忆集。卡表会占用一定的内存空间,堆大小是 1G 时,卡表大小为 1G / 512 = 2MB。
2. 记忆集 RememberedSet(简称 RS 或 RSet)
每一个 Region 都拥有一个自己的记忆集。如果产生了跨代引用,记忆集中会记录引用对象所在的卡表位置。标记阶段将记忆集中的对象加入 GC Root 集合中一起扫描,就可以将被引用的对象标记为存活。
3. 写屏障(Write Barrier)
G1 使用写屏障技术,在执行引用关系建立的代码执行后插入一段指令,完成卡表的维护工作。会损失一部分的性能,大约在 5%~10% 之间。
记忆集的生成流程分为以下几个步骤:
- 通过写屏障获得引用变更的信息。
- 将引用关系记录到卡表中,并记录到一个脏卡队列中。
- JVM 中会由 Refinement 线程定期从脏卡队列中获取数据,生成记忆集。不直接写入记忆集的原因是避免过多线程并发访问记忆集。
4.混合回收(Mixed GC)
多次回收之后,会出现很多 Old 老年代区,此时总堆占有率达到阈值时(-XX:InitiatingHeapOccupancyPercent 默认 45%)会触发混合回收 MixedGC。回收所有年轻代和部分老年代的对象以及大对象区,采用复制算法来完成。

4.1混合回收的步骤
混合回收分为四个阶段:初始标记(initial mark)、并发标记(concurrent mark)、最终标记(remark 或 Finalize Marking)、并发清理(cleanup)。

各阶段说明:
| 阶段 | 是否 STW | 说明 |
|---|---|---|
| 初始标记 | 是 | 标记 GC Roots 引用的对象为存活 |
| 并发标记 | 否 | 将第一步中标记的对象引用的对象,标记为存活 |
| 最终标记 | 是 | 标记一些引用改变漏标的对象,不管新创建、不再关联的对象 |
| 并发清理 | 否 | 将存活对象复制到别的 Region,不会产生内存碎片 |
G1 对老年代的清理会选择存活度最低的区域来进行回收,这样可以保证回收效率最高,这也是 G1(Garbage First)名称的由来。最后清理阶段使用复制算法,不会产生内存碎片。

4.2初始标记与三色标记法
初始标记会暂停所有用户线程,只标记从 GC Root 可直达的对象,所以停顿时间不会太长。采用三色标记法进行标记,三色标记法在原有双色标记(黑色 1 代表存活,白色 0 代表可回收)的基础上增加了一种灰色,采用队列的方式保存标记为灰色的对象。
- 黑色:存活,当前对象在 GC Root 引用链上,同时它引用的其他对象也都已经标记完成。
- 灰色:待处理,当前对象在 GC Root 引用链上,它引用的其他对象还未标记完成。
- 白色:可回收,不在 GC Root 引用链上。
初始所有对象都是默认为白色。三色标记中的黑色和白色使用位图(bitmap)来实现,比如 8 个字节使用 1 个 bit 来标识标记的内容,黑色为 1,白色为 0,灰色不会体现在位图中,会单独放入一个队列中。
4.3并发标记与 SATB 技术
并发标记阶段会继续从未完成的标记任务开始,与用户线程并发执行。从灰色队列中获取尚未完成标记的对象,标记其关联的对象。
三色标记存在一个比较严重的问题:由于用户线程可能同时在修改对象的引用关系,会出现错标的情况。比如用户线程执行 B.c = null(去除 B 到 C 的引用),同时执行 A.c = c(添加 A 到 C 的引用)。此时会出现严重问题:C 是白色可回收,一旦回收代码中再去使用对象 C 会造成重大问题。
G1 为了解决这个问题,使用了 SATB 技术(Snapshot At The Beginning,初始快照)。SATB 技术处理如下:
- 标记开始时创建一个快照,记录当前所有对象,标记过程中新生成的对象直接标记为黑色。
- 采用前置写屏障技术,在引用赋值前比如
B.c = null之前,将之前引用的对象 c 放入 SATB 待处理队列中。SATB 队列每个线程都有一个,最终会汇总到一个大的 SATB 队列中。
SATB 的缺点是在本轮清理时可能会将不存活的对象标记成存活对象,产生了一些所谓的"浮动垃圾",等到下一轮清理时才能回收。
4.4转移阶段
转移的步骤如下:
- 根据最终标记的结果,可以计算出每一个区域的垃圾对象占用内存大小,根据停顿时间,选择转移效率最高(垃圾对象最多)的几个区域。
- 转移时先转移 GC Root 直接引用的对象,然后再转移其他对象。
- 回收老的区域,如果外部有其他区域对象引用了转移对象,也需要重新设置引用关系。
5.Full GC
注意:如果清理过程中发现没有足够的空 Region 存放转移的对象,会出现 Full GC。单线程执行标记-整理算法,此时会导致用户线程的暂停。所以尽量保证应用的堆内存有一定多余的空间。

6.G1 垃圾回收器参数与特性
G1 垃圾回收器的主要参数:
-XX:+UseG1GC:打开 G1 的开关,JDK9 之后默认不需要打开。-XX:MaxGCPauseMillis=毫秒值:最大暂停的时间,默认 200。-XX:G1HeapRegionSize=大小:设置 Region 的大小,取值范围 1M~32M,必须是 2 的指数幂。-XX:InitiatingHeapOccupancyPercent=百分比:触发 Mixed GC 的堆占有率阈值,默认 45。
回收年代和算法:年轻代 + 老年代,复制算法。
优点:
- 对比较大的堆(如超过 6G 的堆)回收时,延迟可控。
- 不会产生内存碎片。
- 并发标记的 SATB 算法效率高。
缺点:JDK8 之前还不够成熟。
适用场景:JDK8 最新版本、JDK9 之后建议默认使用。
