Java G1垃圾回收器核心概念深度解析
前言
G1(Garbage First)垃圾回收器是Oracle在JDK 7中引入的一款面向服务端应用的垃圾回收器,在JDK 9中成为默认的垃圾回收器。G1的设计目标是在保持高吞吐量的同时实现低延迟的垃圾回收,特别适用于大堆内存环境。本文将从理论角度深入分析G1的核心机制和关键概念。
G1的设计理念与目标
核心设计理念
G1垃圾回收器的设计基于以下几个核心理念:
- 分区式内存管理:摒弃传统的连续分代布局,采用统一的分区管理
- 增量式回收:避免全堆回收,每次只回收部分区域
- 可预测的停顿时间:通过启发式算法预测和控制停顿时间
- 并发与并行结合:在不同阶段采用不同的并发策略
设计目标
- 低延迟:停顿时间目标可控制在200ms以内
- 高吞吐量:在大堆环境下维持良好的应用吞吐量
- 内存效率:减少内存碎片,提高内存利用率
- 可扩展性:支持数GB到数TB的堆内存
G1的分代管理机制
逻辑分代 vs 物理分代
与传统垃圾回收器不同,G1采用逻辑分代而非物理分代的概念:
传统分代模型:
┌─────────────┬─────────────┬─────────────┐
│ Young │ Survivor │ Old │
│ Generation│ Space │ Generation │
├─────────────┼─────────────┼─────────────┤
│ Eden │ From/To │ Tenured │
└─────────────┴─────────────┴─────────────┘
物理上连续的内存区域
G1逻辑分代模型:
┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│ E │ S │ O │ E │ H │ O │ S │ E │ O │ F │
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
逻辑上分代,物理上不连续的Region
分代回收策略
G1的分代管理采用渐进式年龄增长策略:
- 年轻代回收频率高:主要回收Eden和Survivor区域
- 老年代回收频率低:通过并发标记周期识别垃圾区域
- 混合回收:同时回收年轻代和部分老年代区域
Region区域管理
Region的基本概念
Region是G1内存管理的基本单位,具有以下特点:
大小特性:
- 固定大小:1MB到32MB(必须是2的幂)
- 数量范围:堆内存被划分为2048个Region
- 大小计算:Region大小 = 堆大小 / 2048(向上取整到2的幂)
类型分类:
- Eden Region:存放新分配的对象
- Survivor Region:存放经过一次GC的年轻对象
- Old Region:存放长期存活的老年对象
- Humongous Region:存放大对象(≥Region大小的50%)
- Free Region:空闲可分配的区域
Region的状态转换
Region状态转换图:
┌─────────┐ 分配对象 ┌─────────┐
│ Free │ ─────────→ │ Eden │
└─────────┘ └─────────┘
│ Young GC
▼
┌─────────┐ 对象晋升 ┌─────────┐
│ Old │ ←───────── │Survivor │
└─────────┘ └─────────┘
│ │
│ Mixed GC │ 年龄增长
▼ ▼
┌─────────┐ ┌─────────┐
│ Free │ │Survivor │
└─────────┘ └─────────┘
Region内部结构
每个Region内部维护以下数据结构:
对象存储区域:
- 对象数据区:实际存储对象的内存空间
- 位图标记区:用于并发标记的位图数据
- 年龄信息:对象的GC年龄统计
元数据区域:
- Region元数据:Region类型、大小、状态等信息
- 回收成本估算:基于存活率的回收成本预测
- 分配指针:当前分配位置的指针信息
RSet(Remembered Set)机制
RSet的设计目的
Remembered Set是G1解决跨Region引用问题的核心机制,其主要目的是:
- 避免全堆扫描:GC时无需扫描整个堆来寻找引用
- 支持增量回收:只回收部分Region时能够准确识别根对象
- 提高GC效率:减少GC过程中的内存扫描开销
RSet的工作原理
跨Region引用追踪:
Region A Region B
┌─────────────┐ ┌─────────────┐
│ Object X │ ───→ │ Object Y │
│ │ │ │
└─────────────┘ └─────────────┘
│ ▲
│ RSet记录
▼ │
┌─────────────────────────────┐
│ Region B的RSet: │
│ - Region A的引用信息 │
│ - 具体的引用位置 │
└─────────────────────────────┘
RSet的数据结构
RSet采用多层数据结构来优化空间和时间效率:
稀疏表(Sparse Table):
- 用于记录少量跨Region引用
- 直接存储引用Region的ID和偏移量
细粒度表(Fine Table):
- 用于记录中等数量的跨Region引用
- 采用位图方式记录引用位置
粗粒度表(Coarse Table):
- 用于记录大量跨Region引用的情况
- 只记录引用的Region ID,不记录具体位置
RSet的维护机制
写屏障机制:
写屏障伪代码:
void updateReference(Object obj, Object newValue) {
Object oldValue = obj.field;
obj.field = newValue; // 实际的引用更新
// 维护RSet
if (inDifferentRegion(obj, newValue)) {
Region targetRegion = getRegion(newValue);
targetRegion.rset.addReference(obj);
}
if (inDifferentRegion(obj, oldValue)) {
Region oldRegion = getRegion(oldValue);
oldRegion.rset.removeReference(obj);
}
}
内存划分与LAB(Local Allocation Buffer)
内存分配层次
G1的内存分配采用多层次的分配策略:
分配层次结构:
应用线程分配请求
│
▼
┌─────────────────┐
│ Thread LAB │ ←── 线程本地分配缓冲区
└─────────────────┘
│ LAB耗尽
▼
┌─────────────────┐
│ PLAB Pool │ ←── PLAB池分配
└─────────────────┘
│ 池耗尽
▼
┌─────────────────┐
│ Region 分配 │ ←── 新Region分配
└─────────────────┘
LAB(Local Allocation Buffer)机制
TLAB(Thread Local Allocation Buffer):
- 目的:减少线程间的分配竞争
- 大小:通常为几KB到几十KB
- 分配:每个线程独享一个TLAB
- 回收:TLAB用完后返回给Region
PLAB(Promotion Local Allocation Buffer):
- 目的:优化对象晋升过程中的分配
- 使用场景:Young GC时对象从Eden/Survivor晋升到Old
- 管理方式:GC线程独享,避免晋升时的锁竞争
LAB的内存管理
TLAB分配流程:
对象分配算法:
1. 检查当前TLAB是否有足够空间
2. 如果空间足够,直接在TLAB中分配
3. 如果空间不足:
a. 尝试从Region获取新的TLAB
b. 如果Region空间不足,申请新的Eden Region
c. 如果无法获得新Region,触发GC
PLAB管理策略:
PLAB大小动态调整:
初始大小 = Region大小 / GC线程数
调整策略 = 基于历史分配统计的自适应调整
最大限制 = Region大小的一定比例
CARD与CardTable机制
Card的基本概念
Card是比Region更细粒度的内存管理单位:
Card特性:
- 大小:固定512字节
- 用途:跟踪内存区域的修改状态
- 范围:每个Region包含多个Card
- 状态:Clean(未修改)或Dirty(已修改)
CardTable数据结构
CardTable的组织形式:
内存布局与CardTable映射:
┌─────────────────────────────────────┐
│ Heap Memory │
├───┬───┬───┬───┬───┬───┬───┬───┬───┤
│512│512│512│512│512│512│512│512│512│ ←── Card边界
└───┴───┴───┴───┴───┴───┴───┴───┴───┘
│ │ │ │ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼
┌─┬─┬─┬─┬─┬─┬─┬─┬─┐
│0│0│1│0│1│0│0│1│0│ ←── CardTable(0=Clean, 1=Dirty)
└─┴─┴─┴─┴─┴─┴─┴─┴─┘
CardTable的维护机制
写屏障更新CardTable:
Card标记算法:
void cardTableWriteBarrier(Object* addr) {
// 计算地址对应的Card索引
size_t cardIndex = (addr - heapBase) >> CardShift;
// 标记对应的Card为Dirty
cardTable[cardIndex] = DIRTY_CARD;
}
CardTable在GC中的应用:
-
根集合扫描优化:
- 只扫描Dirty Card对应的内存区域
- 跳过Clean Card,减少扫描开销
-
RSet维护:
- 基于Dirty Card更新RSet信息
- 避免全Region扫描来发现跨Region引用
-
并发标记支持:
- 在并发标记过程中跟踪引用变化
- 与SATB配合保证标记的正确性
G1的并发标记算法
SATB(Snapshot At The Beginning)算法
SATB是G1并发标记的核心算法,其理论基础如下:
SATB不变性:
- 在标记开始时创建堆的逻辑快照
- 保证快照中的所有可达对象最终都会被标记
- 允许标记过程中的对象变化,但不影响标记正确性
三色标记理论:
对象颜色状态:
• 白色:未被访问的对象(潜在垃圾)
• 灰色:已被访问但子对象未完全扫描的对象
• 黑色:已被访问且子对象已完全扫描的对象
SATB保证:不会出现黑色对象指向白色对象的情况
并发标记的理论模型
标记过程的形式化描述:
-
初始标记阶段:
根集合 R = {GC Roots} 标记集合 M = ∅ 快照时间 T₀ -
并发标记阶段:
对于任意时间 t > T₀: 如果 obj ∈ 快照(T₀) 且 可达(obj, R) 则 最终 obj ∈ M -
标记完成性:
标记完成 ⟺ ∀obj ∈ 快照(T₀) : 可达(obj, R) → obj ∈ M
G1的内存回收策略
垃圾优先(Garbage First)原理
G1的名称来源于其核心回收策略——优先回收垃圾最多的Region:
回收价值计算:
Region回收价值 = 垃圾对象大小 / 预期回收时间
其中:
垃圾对象大小 = Region大小 × (1 - 存活率)
预期回收时间 = 基于历史数据的回收时间预测
Collection Set(CSet)选择算法
CSet选择的理论模型:
-
约束条件:
总停顿时间 ≤ MaxGCPauseMillis 回收Region数量 ≤ 系统处理能力 -
优化目标:
最大化:∑(Region回收价值) 约束于:∑(Region回收时间) ≤ 停顿时间目标 -
选择策略:
- 年轻代:总是包含所有Eden Region
- 老年代:基于回收价值排序选择
- 平衡策略:在回收效益和停顿时间之间平衡
G1的理论优势与局限性
理论优势
- 空间局部性:Region-based设计提高缓存效率
- 时间可预测性:基于历史数据的停顿时间预测
- 并发程度高:大部分GC工作与应用并发执行
- 内存利用率:动态的Region分配减少内存碎片
理论局限性
- 元数据开销:RSet、CardTable等数据结构的空间开销
- 写屏障成本:维护RSet和CardTable的运行时开销
- 复杂性:算法复杂度高,调优参数多
- 最坏情况:在某些极端场景下性能可能退化
总结
G1垃圾回收器通过一系列精巧的理论设计实现了低延迟和高吞吐量的平衡:
核心理论贡献:
- Region-based内存管理:统一了分代和分区的概念
- RSet机制:解决了分区回收中的引用追踪问题
- SATB算法:保证了并发标记的正确性
- 垃圾优先策略:优化了回收效益
设计哲学:
- 分而治之:将复杂的全堆回收分解为多个小的增量回收
- 预测控制:基于历史数据预测和控制GC行为
- 并发优化:最大化并发执行,最小化停顿时间
- 自适应调整:根据应用特征动态调整回收策略

浙公网安备 33010602011771号