1. G1垃圾收集器的设计初衷与核心思想
G1(Garbage-First)垃圾收集器是JDK 7中首次引入的全新垃圾收集器,它的出现彻底改变了传统垃圾收集器的工作方式。在G1之前,HotSpot JVM主要使用CMS(Concurrent Mark-Sweep)和Parallel Scavenge等收集器,这些收集器都存在明显的局限性。
G1的核心设计目标可以概括为三点:
- 可控的停顿时间:能够明确指定期望的停顿时间目标(通常为几百毫秒)
- 高吞吐量:在满足停顿时间要求的前提下最大化应用吞吐量
- 大堆内存管理:能够高效管理从几百MB到几十GB不等的堆空间
与传统分代收集器不同,G1将堆划分为多个大小相等的Region(默认约2048个),每个Region可以是Eden、Survivor或Old区。这种设计带来了几个关键优势:
- 避免了全堆扫描:G1通过Remembered Set记录跨Region引用,只需扫描相关Region
- 动态区域分配:根据垃圾分布情况优先回收价值最高(Garbage-First)的Region
- 并行与并发结合:充分利用多核CPU并行处理,同时减少STW(Stop-The-World)时间
提示:G1的Region大小可以通过-XX:G1HeapRegionSize参数调整,必须是2的幂次方,范围1MB到32MB。JVM会根据堆大小自动计算默认值。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. G1的核心工作流程解析
2.1 初始标记阶段(Initial Marking)
初始标记是G1收集器的第一个STW阶段,它需要暂停所有应用线程。这个阶段主要完成两项工作:
- 标记GC Roots直接可达的对象
- 扫描Survivor区(标记从老年代指向年轻代的引用)
这个阶段会与常规的年轻代GC(Young GC)同步触发,因此实际上借用了Young GC的暂停时间。G1通过SATB(Snapshot-At-The-Beginning)算法保证标记的准确性,即在标记开始时对对象图建立快照。
2.2 并发标记阶段(Concurrent Marking)
并发标记阶段是G1最复杂的阶段,它与应用线程并发执行。主要步骤包括:
- 从GC Roots开始遍历整个对象图
- 处理SATB队列(记录并发修改的引用)
- 重新标记(Remark)被修改的对象
- 清理(Cleanup)空Region
这个阶段会使用多个后台线程并行工作,默认线程数约为CPU核心数的1/4。关键参数包括:
- -XX:ConcGCThreads:控制并发标记线程数
- -XX:InitiatingHeapOccupancyPercent(IHOP):触发并发标记的老年代占用阈值(默认45%)
2.3 混合回收阶段(Mixed Collection)
在标记完成后,G1进入混合回收阶段。这个阶段会同时回收年轻代和老年代的Region,具体过程如下:
- 根据用户设置的MaxGCPauseMillis计算本次回收的Region集合
- 优先选择回收价值高的Region(即垃圾比例高的)
- 对这些Region中的存活对象进行疏散(Evacuation)
- 将存活对象复制到新的Region中,同时压缩内存
混合回收可能会进行多次,直到回收了足够多的空间。每次回收的Region数量由-XX:G1MixedGCCountTarget参数控制(默认8次)。
3. G1的关键数据结构与算法
3.1 Remembered Set(记忆集)
每个Region都有一个Remembered Set(RSet),用于记录其他Region中指向本Region的引用。这样在回收某个Region时,只需扫描其RSet而不用扫描整个堆。RSet的实现采用哈希表结构:
- Key:引用来源的Region地址
- Value:引用对象的卡表(Card Table)索引
RSet的维护通过写屏障(Write Barrier)实现。当程序修改引用字段时,会触发以下操作:
java复制void oop_field_store(oop* field, oop new_value) {
pre_write_barrier(field); // 记录旧引用
*field = new_value; // 实际写操作
post_write_barrier(field, new_value); // 记录新引用
}
3.2 SATB算法
SATB(Snapshot-At-The-Beginning)是G1处理并发标记期间对象图变化的算法。其核心思想是:
- 在标记开始时对对象图建立逻辑快照
- 通过写屏障记录所有被删除的引用
- 在重新标记阶段处理这些变更
写屏障会将删除的引用存入SATB队列:
cpp复制void pre_write_barrier(oop* field) {
oop old_value = *field;
if (old_value != NULL && !is_marked(old_value)) {
satb_enqueue(old_value);
}
}
3.3 疏散失败处理
当G1无法在指定时间内找到足够的空闲Region时,会发生疏散失败(Evacuation Failure)。此时G1会:
- 中止当前回收周期
- 回滚所有已复制的对象
- 调整IHOP阈值(增加-XX:G1ReservePercent预留空间)
- 可能触发Full GC(Serial Old收集器)
4. G1的调优实践与常见问题
4.1 关键性能参数
- -XX:MaxGCPauseMillis:目标最大停顿时间(默认200ms)
- -XX:G1NewSizePercent / -XX:G1MaxNewSizePercent:年轻代占比范围
- -XX:G1HeapWastePercent:允许浪费空间比例(默认5%)
- -XX:G1MixedGCLiveThresholdPercent:混合回收的Region存活对象阈值(默认85%)
4.2 常见性能问题排查
问题1:并发模式失败(Concurrent Mode Failure)
表现:日志中出现"GC pause (G1 Humongous Allocation)"或"Evacuation Failure"
解决方案:
- 增加-XX:G1ReservePercent(默认10%)
- 提前触发并发标记(降低IHOP)
- 检查是否有大对象分配(调整-XX:G1HeapRegionSize)
问题2:长时间停顿
表现:个别GC暂停时间远超MaxGCPauseMillis
可能原因:
- 大对象分配(Humongous Region)
- 引用处理队列溢出
- 系统负载过高导致线程调度延迟
排查工具:GC日志结合-XX:+PrintGCDetails和-XX:+PrintGCApplicationStoppedTime
4.3 与CMS的性能对比
在相同硬件环境下(8核CPU,16GB堆内存)的典型对比:
| 指标 | G1 | CMS |
|---|---|---|
| 平均停顿时间 | 200ms(可控) | 不可预测 |
| 吞吐量 | 稍低(约低5-10%) | 更高 |
| 内存占用 | 额外10-20%(RSet等) | 更低 |
| 大堆适应性 | 优秀(TB级) | 较差(超过4GB效率下降) |
5. G1的演进与最新改进
从JDK 7到JDK 17,G1经历了多次重要改进:
- JDK 8u20:字符串去重(-XX:+UseStringDeduplication)
- JDK 9:默认垃圾收集器(取代Parallel Scavenge + CMS)
- JDK 10:并行Full GC(替代Serial Old)
- JDK 12:可中止的混合回收
- JDK 16:并发标记线程动态调整
最新的JDK 17中,G1的改进重点包括:
- 更智能的IHOP预测(-XX:G1AdaptiveIHOP)
- 改进的RSet处理(减少内存占用)
- 对ZGC和Shenandoah特性的反向移植
在实际生产环境中,G1特别适合以下场景:
- 堆大小超过6GB
- 要求相对稳定的GC停顿时间
- 应用有大量短期存活对象
- 需要平衡吞吐量和延迟
