G1 基础

258 阅读10分钟

G1:垃圾优先(Garbage First)。

基本概念

G1 把堆划分为多个大小相等的 Region(区域)「物理上不连续」,而不是传统的固定划分(如 Eden、Survivor、Old)。

每一个region的大小是1 - 32M不等,必须是2的整数次幂。使用不同的region可以来表示Eden、幸存者0区、幸存者1区、老年代等。

每个 Region 的角色在运行中可变:可能是年轻代(Young)、老年代(Old)或空闲。

G1 有计划的避免在整个JVM堆中进行全区域的垃圾回收。G1根据各个Region里的垃圾价值大小(回收获得的空间大小以及回收所需时间),在后台维护一个优先队列,优先回收价值最大的Region

适用场景

  • 多核CPU以及多大内存设备「CMS对大堆的碎片处理能力差」
  • 响应时间敏感的应用。
  • 极高概率满足GC停顿的同时还兼容服务高吞吐量

G1支持可配置停顿目标(例如 -XX:MaxGCPauseMillis=200),用停顿预测模型在每次 GC 前挑选一组性价比最高的 Region,在预算时间内尽量完成回收,所以大多数时候能达到设置的最大停顿时间。

特征

  1. 并行与并发
    • G1充分发挥多核性能,使用多GPU并行缩短STW时间
  2. 分代收集
    • 能够自己管理不同分代内的已创建对象和新对象
  3. 空间整合
    • 整体基于"标记-整理"算法实现,从局部(相关的两块Region)上来看是基于‘复制’算法实现,这两种算法都不会产生内存空间碎片
  4. 停顿时间可预测
    • G1 可以通过参数 -XX:MaxGCPauseMillis 设置最大停顿时间目标(如 200ms)。
    • G1 会智能选择哪些 Region 要回收,以满足这个目标。

G1相比与CMS的优缺点

优点

  1. 停顿可控,并且利用多核并行缩短STW时间
  2. 内存碎片更少
  3. 并发可以使工作线程与GC线程同时运行
  4. Mixed GC 可以同时回收年轻代+部分老年代,避免频繁Full GC
  5. 不需要其他收集器配合就能独立管理整个堆,仍然保留分代概念。以不同的方式处理新创建的对象和已经存活一段时间,熬过多次GC的旧对象

缺点

  1. 无论是内存占用还是额外负载,多要比CMS高
  2. 整体吞吐量可能略低于CMS
  3. 在小内存应用上,CMS的表现大概率优于G1,平衡点在6-8GB之间

特性

incremental

多次、小批量回收,使单次回收停顿可控

  • 主要体现在Mixed GC阶段,并不是一次性全部清理
    • 每次只处理一部分老年代(按“回收收益”排序)+ 年轻代
    • 多轮Mixed GC累计清理老年代,避免一次Full GC
  • 实现停顿可控依赖的机制
    • Region粒度小,易于分批选择回收
    • 会评估每个Region的回收成本和回收收益,按性价比排序
    • 并行回收
    • 并发标记

Copying

  • G1 中所有对象复制(Copying)操作都发生在 STW 的并行阶段(Parallel),并不在并发阶段(Concurrent)进行。

generational:分代

  • 老年代中的对象可能持有对年轻代对象的引用,使用Minor GC只回收年轻的,不扫描老年代找引用
    • 使用卡表(Card Table) + Write Barrier记录跨带引用
    • 当老年代引用年轻代对象,使用Remembered Set记录下来。Minor GC时,只扫描Remembered Set中的老年代引用
  • 晋升(Promotion)机制
    • 年轻代中存活下来的对象会被复制到老年代
    • JVM需要维护对象年龄,判断是否晋升
    • 晋升过程中可能触发老年代GC或者晋升失败(产生Full GC)
  • Survivor区管理
    • 年轻代中有 Eden、Survivor From 和 To 区域,用于复制式回收。
    • 每次 Minor GC 要决定:
      • 谁晋升?谁留在 Survivor?
      • 对象年龄如何变化?

Region

image.png

年轻一代包含Eden(红色)和Survivor(红色带“S”) Old为浅蓝色

一个region可能属于Eden,Survivor或者Old区域,但是只可能属于一个角色

还有一种内存区域,叫做Humongous区域,作用是存储大对象。超过1.5个region,就放到H

对于堆中的大对象,默认直接分配到老年代,如果是一个短期存在的大对象,会对垃圾收集器造成负面影响。为了解决这个问题,划分了H区。G1大多数行为都将H区作为老年代的一部分看待

如果一个H区放不下一个大对象,会寻找连续的H区域来存储。

为了能找到连续的H区,有时候不得不启动Full GC

Card Table

CardTable是一种字节数组,每个字节对应堆内存的一小块(称为“卡”,也就是说,一个字节对应一个卡)。用于记录引用的变化

  • 堆被划分为多个固定大小的“卡”(通常是512字节)
  • 当程序的写操作可能创建跨代或者区引用时,相关的卡会被标记为“脏”
  • 脏卡会被添加到一个脏卡队列当中,供后续处理 作用:提供了一个快速的方式来识别哪些内存区域可能包含跨代或者区的引用,从而减少在垃圾回收时需要扫描的内存范围

当写入老年代的引用指向年轻代时,标记的是老年代对应的 Card Table 卡

Remembered Set

Remembered Set是G1中每个Region维护的数据结构,记录其他区域中哪些卡可能包含执行当前区域的引用

  • 当一个区域(如老年代)中的对象引用了另一个区域(如年轻代)中的对象时,相关的卡会被记录在被引用区域的 RSet (年轻代)中
  • 在垃圾回收时,G1 只需扫描 RSet 中记录的卡,而无需扫描整个堆,从而提高效率 作用:是G1在部分堆回收时(年轻代或混合回收)时,准确地识别被其他区域引用的对象

Card Table 与 Remembered Set 的关系

Card Table 和 Remembered Set 是协同工作的:

  1. 标记阶段:当程序执行写操作时,相关的卡被标记为脏,并添加到脏卡队列中
  2. 更新阶段:G1的并发线程会处理脏卡队列,将这些信息更新到RSet中
  3. 回收阶段:在回收时,G1通过扫描RSet中记录的卡,快速定位被其他区域引用的对象

举例

假设有两个对象:

  • 对象 A 位于老年代的 Region_Old 中,属于 Card #42
  • 对象 B 位于年轻代的 Region_Young 中
  • A 引用了 B(即 A -> B)
  1. 写屏障阶段
    • 当老年代对象A的字段被赋值为B的引用时,写屏障机制会立即生效,将 A 所在的 Card(Card #42)标记为“脏卡”(Dirty Card)
  2. 并发处理阶段
    • Concurrent Refinement 线程会扫描这些脏卡,查找是否存在指向年轻代对象的引用,此时找到A -> B,则B所在的Region的RSet会加入A所在的卡信息
  3. 更新Remembered Set
    • 年轻代对象 B 所在的 Region(Region_Young)的 RSet 中,会记录下 A 所在的 Card 信息(即 "Card #42 in Region_Old")。也就是说:RSet 会反向记录“有哪些老年代的 Card 指向当前 Region 中的对象”
  4. 下一层Minor GC时
    • 不需要扫描整个老年代。只需通过年轻代各 Region 的 RSet,找到记录中提到的老年代 Card(如 Card #42),检查是否仍然存在指向年轻代对象的引用。若存在,这些引用将被作为 GC Root,确保这些年轻代对象不会被误回收。

Write Barrier

用于在对象引用发生变化时,辅助垃圾收集器准确地追踪和管理对象的引用关系 主要有两个作用

  1. 支持并发标记(SATB):在并发标记阶段,G1使用SATB算法来标记存活对象。当对象引用发生变化事,Write Barrier会记录旧的引用,确保引用被更新,原来的引用对象也不会漏标
  2. 维护Remembered Set:当程序的写操作可能创建跨代引用时,通过写屏障,修改对象的卡,供后续RSet使用

类型

  • Pre-Write Barrier(写前屏障):在对象引用被更新之前执行,用于记录旧的引用,支持 SATB 算法
  • Post-Write Barrier(写后屏障):在对象引用被更新之后执行,用于更新 RSet,维护跨 Region 的引用信息

STAB

在初始标记暂停时,拍摄对的虚拟快照,此时在标记开始时存活的所有对象在标记的剩余时间都被视为存活。这意味着在标记期间变得死亡(不可达)的对象仍然被认为是可达的

原理

具体实现上,G1 将堆划分为多个大小相同的 Region,每个 Region 包含五个指针:

  • bottom:当前分配区域的起始位置。
  • top:当前分配区域的结束位置。
  • end:当前 Region 的最大分配位置。
  • nextTAMS:上次并发标记开始时的 top 值。
  • prevTAMS:上上次并发标记开始时的 top 值。

在并发标记开始时,G1 将当前 Region 的 top 值赋给 nextTAMS,标记期间所有新分配的对象都位于 [nextTAMS, top] 区间内。当并发标记结束时,nextTAMS 的值被赋给 prevTAMS,生成 [bottom, prevTAMS] 区间内对象的存活快照。在后续的并发标记中,G1 会根据这些快照确保新分配的对象被正确标记为存活对象。

STAB 的生命周期

基本术语解释
  • TAMS(Top At Mark Start):标记开始时的对象分配边界。PrevTAMS 表示上次标记的边界,NextTAMS 是当前标记的边界。
  • PrevBitmap / NextBitmap:两张 bitmap 图用于记录对象是否存活。一个记录上次标记的对象(PrevBitmap),一个记录当前标记周期的存活对象(NextBitmap)。
  • Bottom / Top:表示某个堆区间的开始与当前对象分配的结束位置。
  • 灰色区域:表示已分配的内存。
  • 白色区域:表示空闲未分配的内存。 image.png
阶段 A:Initial Marking(初始标记)STW
  • 初始标记从 GC 根对象开始标记。
  • 设置 NextTAMS
  • NextBitmap 为空,等待标记。
阶段 B:并发标记开始 -> Remark(重标记)STW前
  • 并发线程根据 TAMS 和对象图构建 NextBitmap
  • 期间新对象仍可被分配到nextTAMS Top 之间。
  • NextTAMS 被用于识别当前新分配对象的起始位置。
阶段 C:Cleanup / GC Pauses(清理阶段)
  • 交换 PrevBitmapNextBitmap
  • 设置 PrevTAMS 和新的 NextTAMS
  • 清理 BottomPrevTAMS之间的对象。
阶段 D:新周期 Initial Marking
  • 与阶段 A 相似,此时新的 NextTAMS 已设置。
  • 使用新的 NextBitmap 开始新一轮标记
阶段 E:并发标记与 Remark 前状态
  • 新一轮 NextBitmap 开始构建。
  • PrevBitmap 仍记录上一个周期的存活对象。
  • 此时PrevBitmap记录的对象,但是没有被NextBitmap记录,则认为这个对象是不可达的。
阶段 F:Cleanup / GC Pauses(清理阶段)
  • 交换 PrevBitmapNextBitmapNextBitmap 置为空
  • 并发清理无用对象(那些没有在 PrevBitmap 中标记的对象)。
  • 确保只回收真正不再使用的对象。
  • 最终进行 minor GC(或 mixed GC),清理所有无效对象。

官方文档

docs.oracle.com/en/java/jav…