G1
概述
G1 (Garbage-First) 是一款面向服务端应用、为大内存和多处理器环境设计的垃圾回收器,于Java 7u4版本正式引入。它的核心目标是在保持高吞吐量的同时,满足用户设定的停顿时间目标(软实时)。
G1的核心优势
- 并行与并发: 利用多CPU核心来缩短停顿时间(STW),并通过与应用线程并发执行的方式完成部分工作。
- 分代但非物理分区: 逻辑上保留分代概念(年轻代、老年代),但物理上不再是连续空间,提供了更高的灵活性。
- 空间整合(压缩): G1从整体上看是一款基于“标记-复制”算法的收集器,从局部(两个Region之间)看是“复制”算法。这意味着G1在回收后不会产生内存碎片,有利于程序长时间稳定运行。
- 可预测的停顿: G1的核心设计哲学。用户可以指定一个期望的最大停顿时间(通过
-XX:MaxGCPauseMillis),G1会尽力达成此目标。
G1与CMS的主要区别
- 算法: G1使用“标记-复制”,而CMS使用“标记-清除”,因此G1天然避免了内存碎片问题。
- 内存模型: G1基于分区(Region)模型,而CMS基于传统的连续分代模型。
- 可预测性: G1通过其“Garbage-First”策略和分区模型,在停顿时间控制上比CMS更具可预测性。
G1的内存模型
G1颠覆了传统GC将堆划分为连续物理空间的做法,引入了分区(Region)的概念。
核心概念
- 分区 (Region)
- 整个Java堆被划分为约2048个大小相等的独立分区(Region),每个分区大小在1MB到32MB之间,必须是2的幂。
-
每个Region在任何时刻都扮演一个特定的角色:Eden、Survivor、Old或Humongous。这种角色是动态变化的。
-
收集集合 (CSet - Collection Set)
- CSet是指在一次GC停顿时,被选中进行回收的一系列分区的集合。
-
G1的回收操作(无论是年轻代收集还是混合收集)都是针对CSet中的分区进行的。
-
已记忆集合 (RSet - Remembered Set)
- RSet是G1解决跨分区引用问题的关键。每个分区都有一个RSet,用于记录其他分区中的对象对本分区内对象的引用。
-
当回收一个分区时,只需扫描其RSet,即可知道外部引用是否存活,从而避免了全堆扫描,这是实现可预测停顿的关键。RSet的维护通过写屏障(Write Barrier)实现。
-
巨型对象 (Humongous Object)
- 当一个对象的大小超过一个Region容量的50%时,它被视为巨型对象。
- 巨型对象会直接分配在老年代的连续分区(Humongous Region)中。G1对巨型对象的回收有特殊优化,例如在并发标记阶段发现其不被引用,就可以在清理阶段直接回收。
G1的垃圾收集周期
G1的收集活动主要分为两种:年轻代收集(Young Collection)和混合收集(Mixed Collection)。它没有传统的Full GC概念,除非发生转移失败(Evacuation Failure)。
年轻代收集 (Young Collection)
- 触发时机: 当Eden空间被占满,新的对象无法分配时触发。
- 工作内容:
- 这是一个完全STW(Stop-The-World)的过程。
- G1将所有年轻代分区(Eden + Survivor)作为CSet。
- 存活的对象被复制到新的Survivor分区或晋升到老年代分区。
- 回收完成后,原有的年轻代分区被完全清空,变为可用空闲分区。
- 作用: 完成常规的垃圾回收,并累积老年代的使用率。
混合收集周期 (Mixed Collection Cycle)
混合收集是G1回收老年代空间的核心机制,它不是一次单独的GC,而是一个完整的周期,包含并发阶段和STW回收阶段。
-
触发时机: 当老年代占用堆总大小的比例达到
InitiatingHeapOccupancyPercent(IHOP,默认45%)阈值时,G1会启动混合收集周期。 -
周期阶段:
-
初始标记 (Initial Mark)
- STW阶段。
- 此阶段通常“借用”一次正常的年轻代收集(Piggybacking)的STW时间来完成。
- 它仅标记GC Roots能直接关联到的对象,速度很快。
-
并发标记 (Concurrent Marking)
- 与应用程序并发执行,不产生STW。
- G1从此前的GC Roots出发,遍历整个对象图,查找并标记所有存活对象。
- 这个阶段会使用SATB(Snapshot-at-the-Beginning)算法来保证并发标记的正确性。
-
最终标记 (Remark)
- STW阶段。
- 这个阶段需要短暂暂停应用,以处理并发标记阶段结束后对象引用关系的少量变化(处理SATB日志),并最终确定所有存活对象。
-
清理 (Cleanup)
- STW阶段。
- 计算各个老年代分区的存活对象数量和空间,并进行排序,识别出回收价值最高的分区(Garbage-First)。
- 识别并直接回收完全没有存活对象的空闲分区。
-
混合收集 (Mixed Collection)
- STW阶段。
- 在这个阶段,G1会执行一次“混合”模式的GC。
- CSet中不仅包含所有年轻代分区,还包含了部分根据清理阶段计算出的、回收价值最高的老年代分区。
- 这个阶段会执行多次,直到回收的老年代分区达到了预设目标或不再有高收益的分区为止。
转移失败的担保机制 (Full GC)
当G1在进行对象复制(Evacuation)时,如果目标空间(To-Space)不足以容纳所有存活对象,就会发生“转移失败”(Evacuation Failure)。此时,G1会放弃原有的回收策略,转而触发一次单线程、长时间STW的Full GC,对整个堆进行标记、整理和压缩。这是G1中应极力避免的情况。
核心算法与机制
并发标记与漏标问题
- 三色标记法: 是并发标记的基础,将对象分为白(未访问)、灰(自身已访问,但其引用未完全扫描)、黑(自身和其引用都已扫描)三类。
- 漏标问题: 在并发标记过程中,如果应用线程修改了对象引用(如一个黑色对象引用了白色对象,同时灰色对象到这个白色对象的引用被切断),可能会导致本应存活的白色对象被错误回收。
G1的解决方案:SATB
- SATB (Snapshot-at-the-Beginning / 起始快照): G1解决漏标问题的核心算法。
- 原理: 在并发标记开始时,G1逻辑上为堆创建了一个“快照”。它不关心标记过程中新增的引用,而是关注那些“消失”的引用。
- 实现: 通过写前屏障(Pre-Write Barrier)实现。当一个对象引用发生改变时,它原来的引用值(即那个将要“消失”的引用)会被记录到一个日志(SATB Log Buffer)中。在后续的标记阶段,G1会处理这些日志,确保快照中存活的对象最终被正确标记。
RSet的维护
- RSet的更新依赖于写后屏障(Post-Write Barrier)。
- 当一个对象字段被写入新的引用时,写后屏障会检查这是否是一个跨分区的引用。如果是,它会将这个修改信息记录到一个“脏卡片队列”(Dirty Card Queue)中。
- G1有专门的并发优化线程(Concurrent Refinement Threads)在后台异步处理这些队列,将信息更新到对应分区的RSet中,从而维护RSet的准确性。