Skip to content

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的准确性。