第九章 G1垃圾收集器详解

100 阅读12分钟

第九章 G1垃圾收集器详解

概述

G1垃圾收集器简介

Garbage-First (G1) 垃圾收集器是Oracle HotSpot JVM中的一款低延迟垃圾收集器,专为多处理器机器和大内存环境设计。G1的设计目标是在保持高吞吐量的同时,提供可预测的低延迟垃圾收集暂停时间。

G1的核心特点:

  • 分代式:支持年轻代和老年代的概念
  • 增量式:可以增量地回收堆内存
  • 并行:多个GC线程并行工作
  • 大部分并发:大部分工作与应用线程并发执行
  • Stop-The-World:某些操作仍需要暂停应用线程
  • 疏散式:通过复制存活对象来回收内存
  • 暂停时间目标监控:每次STW暂停都会监控暂停时间目标

G1的设计目标

G1垃圾收集器旨在为具有以下特征的应用程序和环境提供最佳的延迟和吞吐量平衡:

  • 大堆内存:堆大小达到数十GB或更大,超过50%的Java堆被活跃数据占用
  • 动态分配模式:对象分配和晋升速率随时间显著变化
  • 堆碎片化:堆中存在大量内存碎片
  • 可预测的暂停时间:暂停时间目标不超过几百毫秒,避免长时间的垃圾收集暂停

G1与其他收集器的对比

G1在设计上与传统的吞吐量收集器有所不同。虽然G1的垃圾收集暂停通常比传统收集器短得多,但应用程序的吞吐量也可能略低,因为G1会使用一些原本可用于应用程序的处理器资源。

1. G1基本概念

1.1 启用G1收集器

G1垃圾收集器是默认的收集器,通常不需要执行任何额外操作。也可以通过在命令行提供-XX:+UseG1GC来显式启用它。

1.2 堆布局(Heap Layout)

G1将堆内存划分为大小相等的区域(regions),每个区域的大小通常在1MB到32MB之间,具体大小由堆大小决定。这种设计使得G1能够更灵活地管理内存和控制暂停时间。

graph TB
    subgraph "G1堆内存布局"
        subgraph "Young Generation"
            E1["Eden Region"]
            E2["Eden Region"]
            E3["Eden Region"]
            S1["Survivor Region"]
            S2["Survivor Region"]
        end
        
        subgraph "Old Generation"
            O1["Old Region"]
            O2["Old Region"]
            O3["Old Region"]
            O4["Old Region"]
        end
        
        subgraph "Special Regions"
            H1["Humongous Region"]
            H2["Humongous Region"]
            F1["Free Region"]
            F2["Free Region"]
        end
    end
    
    style E1 fill:#e3f2fd
    style E2 fill:#e3f2fd
    style E3 fill:#e3f2fd
    style S1 fill:#e8f5e8
    style S2 fill:#e8f5e8
    style O1 fill:#fff3e0
    style O2 fill:#fff3e0
    style O3 fill:#fff3e0
    style O4 fill:#fff3e0
    style H1 fill:#fce4ec
    style H2 fill:#fce4ec
    style F1 fill:#f5f5f5
    style F2 fill:#f5f5f5

区域类型说明:

  • Eden区域:新分配的对象首先放置在这里
  • Survivor区域:经过一次或多次垃圾收集后仍然存活的年轻对象
  • Old区域:长期存活的对象
  • Humongous区域:存储大对象(超过区域大小一半的对象)
  • Free区域:未使用的区域,可以分配给任何代

1.3 垃圾收集周期(Garbage Collection Cycle)

G1的垃圾收集周期包含两个主要阶段:0

  1. Young-Only阶段:只收集年轻代区域
  2. Space-Reclamation阶段:除了年轻代,还收集老年代区域
flowchart TD
    A["应用程序运行"] --> B{"需要分配内存?"}
    B -->|是| C{"Eden区域可用?"}
    B -->|否| A
    
    C -->|是| D["在Eden区域分配对象"]
    C -->|否| E["触发Young GC"]
    
    D --> A
    
    E --> F["Young-Only Phase"]
    F --> G{"老年代占用率达到阈值?"}
    
    G -->|否| H["继续Young-Only Phase"]
    G -->|是| I["开始并发标记"]
    
    H --> A
    
    I --> J["并发标记完成"]
    J --> K["Space-Reclamation Phase"]
    
    K --> L["Mixed GC"]
    L --> M{"老年代空间充足?"}
    
    M -->|是| N["返回Young-Only Phase"]
    M -->|否| L
    
    N --> A
    
    style E fill:#ffcdd2
    style I fill:#e1f5fe
    style L fill:#fff3e0

2. G1内部机制

2.1 启动堆占用率确定(Determining Initiating Heap Occupancy)

G1使用启动堆占用率阈值来决定何时开始并发标记周期。这个阈值通过-XX:G1HeapRegionSize和相关参数来控制。当老年代的占用率达到这个阈值时,G1会启动并发标记过程。

关键参数:

  • -XX:G1HeapRegionSize:设置区域大小
  • -XX:G1MixedGCCountTarget:混合GC的目标次数
  • -XX:G1MixedGCLiveThresholdPercent:区域中存活对象的阈值百分比

2.2 标记(Marking)

G1的标记过程是并发进行的,主要包括以下阶段:

  1. 初始标记(Initial Mark):STW阶段,标记GC Roots直接可达的对象
  2. 并发标记(Concurrent Mark):与应用线程并发执行,标记整个堆中的存活对象
  3. 最终标记(Final Mark):STW阶段,处理并发标记期间的变化
  4. 清理(Cleanup):STW阶段,计算每个区域的存活对象信息
sequenceDiagram
    participant App as 应用线程
    participant GC as GC线程
    
    Note over App,GC: 并发标记周期开始
    
    GC->>App: STW - 初始标记
    Note over App: 应用暂停
    GC->>GC: 标记GC Roots
    GC->>App: 恢复应用
    
    par 并发执行
        App->>App: 继续运行
    and
        GC->>GC: 并发标记堆对象
    end
    
    GC->>App: STW - 最终标记
    Note over App: 应用暂停
    GC->>GC: 处理标记变化
    GC->>App: 恢复应用
    
    GC->>App: STW - 清理
    Note over App: 应用暂停
    GC->>GC: 计算区域信息
    GC->>App: 恢复应用
    
    Note over App,GC: 标记周期完成

2.3 极端堆内存情况下的行为

G1在堆内存极度紧张的情况下会采取特殊的处理策略:0

  • 分配失败:当无法分配新对象时,G1会尝试立即进行垃圾收集
  • Full GC后备:如果常规的G1收集无法释放足够空间,会退化为Full GC
  • 内存压缩:在极端情况下进行堆压缩以获得连续空间

2.4 Humongous对象处理

Humongous对象是指大小超过区域大小一半的对象。G1对这类对象有特殊的处理机制:0

  • 直接分配到老年代:Humongous对象直接分配到专门的Humongous区域
  • 连续区域分配:大对象可能需要多个连续的区域
  • 特殊回收策略:在并发标记周期结束时回收不可达的Humongous对象
flowchart TD
    A["分配新对象"] --> B{"对象大小 > Region Size / 2?"}
    
    B -->|否| C{"Eden区域有空间?"}
    B -->|是| D["标记为Humongous对象"]
    
    C -->|是| E["在Eden区域分配"]
    C -->|否| F["触发Young GC"]
    
    D --> G{"有连续的Free区域?"}
    
    G -->|是| H["分配到Humongous区域"]
    G -->|否| I["触发GC释放空间"]
    
    F --> J["GC后重新尝试分配"]
    I --> K["GC后重新尝试分配"]
    
    J --> C
    K --> G
    
    style D fill:#fce4ec
    style H fill:#fce4ec
    style F fill:#ffcdd2
    style I fill:#ffcdd2

2.5 Young-Only阶段的代大小调整

G1会动态调整年轻代的大小以满足暂停时间目标。调整策略包括:0

  • 基于暂停时间:如果暂停时间超过目标,减少年轻代大小
  • 基于分配速率:根据应用的分配模式调整年轻代大小
  • 最小/最大限制:在配置的最小和最大年轻代大小之间调整

2.6 Space-Reclamation阶段的代大小调整

Space-Reclamation阶段的大小调整更加复杂,需要考虑:

  • 混合GC的效率:选择回收效率最高的老年代区域
  • 暂停时间预算:在暂停时间目标内尽可能多地回收空间
  • 存活对象密度:优先回收存活对象较少的区域

3. G1的人体工程学默认值

G1提供了一套经过优化的默认配置,这些默认值在大多数情况下都能提供良好的性能:0

关键默认参数:

  • 暂停时间目标:200毫秒(-XX:MaxGCPauseMillis=200
  • 区域大小:根据堆大小自动计算(1MB-32MB)
  • 年轻代大小:动态调整(堆的5%-60%)
  • 并发线程数:根据CPU核心数自动设置
pie title G1默认配置分布
    "暂停时间目标" : 25
    "区域大小自动计算" : 20
    "年轻代动态调整" : 20
    "并发线程自动设置" : 15
    "其他优化参数" : 20

4. G1性能调优

4.1 一般建议

一般建议是使用G1的默认设置,如果需要的话,可以给它一个不同的暂停时间目标,并通过使用-Xmx设置最大Java堆大小。1

基本调优原则:

  • 从默认设置开始:移除所有影响垃圾收集的选项
  • 设置堆大小:只设置-Xmx和可选的-Xms
  • 设置暂停时间目标:根据应用需求调整-XX:MaxGCPauseMillis
  • 避免固定年轻代大小:不要使用-Xmn-XX:NewRatio等参数

4.2 从其他收集器迁移到G1

从其他收集器(特别是CMS收集器)迁移到G1时,应该:1

  1. 移除所有GC相关选项:清除之前的GC调优参数
  2. 只保留基本设置:仅设置暂停时间目标和堆大小
  3. 观察性能表现:监控应用在新配置下的表现
  4. 逐步优化:根据实际需要进行细微调整

4.3 改善G1性能

G1设计为无需额外选项即可提供良好的整体性能。但在某些情况下,默认启发式算法或默认配置可能提供次优结果。1

4.3.1 观察Full垃圾收集

Full GC通常非常耗时。由老年代占用率过高引起的Full GC可以通过在日志中查找"Pause Full (G1 Compaction Pause)"来检测。1

Full GC的常见原因:

  • 老年代空间不足
  • Humongous对象分配失败
  • 元空间不足
  • 显式调用System.gc()

解决策略:

  • 增加堆大小(-Xmx
  • 调整G1HeapRegionSize
  • 优化应用减少大对象创建
  • 禁用显式GC(-XX:+DisableExplicitGC
4.3.2 Humongous对象碎片化

Humongous对象可能导致堆碎片化,影响性能:1

优化策略:

  • 增加区域大小(-XX:G1HeapRegionSize
  • 优化应用避免创建大对象
  • 使用对象池重用大对象
  • 考虑数据结构优化
4.3.3 延迟调优

针对延迟敏感的应用,可以进行以下调优:

异常系统或实时使用:

  • 调整暂停时间目标(-XX:MaxGCPauseMillis
  • 启用G1的实时模式(如果可用)
  • 优化GC线程数量

引用对象处理时间过长:

  • 调整引用处理线程数(-XX:G1ConcRefinementThreads
  • 优化弱引用、软引用的使用

Young-Only阶段的Young收集时间过长:

  • 减少年轻代大小上限
  • 调整Eden区域数量
  • 优化对象分配模式

Mixed收集时间过长:

  • 调整混合GC参数(-XX:G1MixedGCCountTarget
  • 优化老年代区域选择策略
  • 增加混合GC的并行度
4.3.4 吞吐量调优

对于吞吐量敏感的应用:1

  • 放宽暂停时间目标:使用-XX:MaxGCPauseMillis设置更大的值
  • 提供更大的堆:增加-Xmx
  • 调整并发线程数:优化-XX:ConcGCThreads
  • 优化年轻代比例:在允许范围内增加年轻代大小
4.3.5 堆大小调优

合适的堆大小对G1性能至关重要:

  • 最小堆大小:设置合理的-Xms避免频繁扩展
  • 最大堆大小-Xmx应该为应用提供足够的空间
  • 区域大小:通过-XX:G1HeapRegionSize优化区域大小
  • 新生代比例:让G1自动调整,避免手动设置
flowchart TD
    A["G1性能问题"] --> B{"问题类型?"}
    
    B -->|Full GC频繁| C["增加堆大小<br/>优化大对象"]
    B -->|暂停时间过长| D["调整暂停时间目标<br/>优化年轻代大小"]
    B -->|吞吐量不足| E["放宽暂停时间<br/>增加堆大小"]
    B -->|内存碎片化| F["调整区域大小<br/>优化对象分配"]
    
    C --> G["监控GC日志"]
    D --> G
    E --> G
    F --> G
    
    G --> H{"性能改善?"}
    H -->|是| I["调优完成"]
    H -->|否| J["进一步分析<br/>应用级优化"]
    
    style C fill:#e3f2fd
    style D fill:#e8f5e8
    style E fill:#fff3e0
    style F fill:#fce4ec

5. G1监控和诊断

5.1 GC日志分析

为了诊断目的,G1提供了全面的日志记录。一个好的开始是使用-Xlog:gc*=debug选项,然后根据需要细化输出。1

关键日志信息:

  • 收集类型(Young GC、Mixed GC、Full GC)
  • 暂停时间分解
  • 各阶段耗时详情
  • 内存使用情况
  • 区域分配和回收统计

5.2 性能指标监控

关键监控指标:

  • GC频率:Young GC、Mixed GC、Full GC的频率
  • 暂停时间:平均暂停时间和最大暂停时间
  • 吞吐量:应用线程运行时间占比
  • 内存使用率:各代的内存使用情况
  • 分配速率:对象分配的速度
  • 晋升速率:对象从年轻代晋升到老年代的速度

5.3 常见问题诊断

flowchart LR
    A["G1性能问题诊断"] --> B["收集GC日志"]
    B --> C{"分析日志内容"}
    
    C --> D["Full GC频繁"]
    C --> E["暂停时间过长"]
    C --> F["吞吐量下降"]
    C --> G["内存泄漏"]
    
    D --> D1["检查老年代使用率"]
    D --> D2["分析Humongous对象"]
    D --> D3["检查元空间使用"]
    
    E --> E1["分析暂停时间分解"]
    E --> E2["检查年轻代大小"]
    E --> E3["分析并发标记效率"]
    
    F --> F1["检查GC开销"]
    F --> F2["分析分配模式"]
    F --> F3["优化应用代码"]
    
    G --> G1["分析堆转储"]
    G --> G2["检查对象引用链"]
    G --> G3["使用内存分析工具"]

6. G1最佳实践

6.1 应用设计建议

对象生命周期管理:

  • 避免创建过多短生命周期的大对象
  • 合理使用对象池减少分配压力
  • 优化数据结构减少内存占用
  • 及时释放不需要的对象引用

内存分配模式:

  • 避免突发性的大量对象分配
  • 平滑分配速率减少GC压力
  • 合理设计缓存策略
  • 使用合适的集合类型和初始容量

6.2 JVM参数配置

基础配置:

# 基本G1配置
-XX:+UseG1GC
-Xmx8g
-Xms8g
-XX:MaxGCPauseMillis=200

# 日志配置
-Xlog:gc*:gc.log:time,tags

# 可选优化参数
-XX:G1HeapRegionSize=16m
-XX:G1MixedGCCountTarget=8
-XX:G1MixedGCLiveThresholdPercent=85

生产环境建议:

# 生产环境G1配置示例
-XX:+UseG1GC
-Xmx32g
-Xms32g
-XX:MaxGCPauseMillis=100
-XX:G1HeapRegionSize=32m
-XX:+G1UseAdaptiveIHOP
-XX:G1MixedGCCountTarget=8
-XX:G1MixedGCLiveThresholdPercent=90
-XX:+UnlockExperimentalVMOptions
-XX:+UseTransparentHugePages

6.3 监控和告警

关键监控项:

  • GC暂停时间超过目标值的频率
  • Full GC的发生频率
  • 堆内存使用率趋势
  • 应用吞吐量变化
  • Humongous对象分配情况

告警阈值建议:

  • 暂停时间超过目标值的50%
  • Full GC频率超过每小时1次
  • 堆内存使用率持续超过80%
  • 应用吞吐量下降超过10%

7. G1与现代垃圾收集器对比

7.1 G1 vs ZGC

特性G1ZGC
暂停时间<200ms(可配置)<10ms(固定)
堆大小支持几十GBTB级别
内存开销中等较高
成熟度中等
适用场景通用应用超大堆、极低延迟

7.2 G1 vs Shenandoah

特性G1Shenandoah
并发程度部分并发高度并发
暂停时间可预测极低且稳定
吞吐量较高中等
配置复杂度简单中等
生产就绪

7.3 选择建议

flowchart TD
    A["选择垃圾收集器"] --> B{"堆大小?"}
    
    B -->|<8GB| C["考虑Parallel GC"]
    B -->|8GB-64GB| D{"延迟要求?"}
    B -->|>64GB| E{"极低延迟需求?"}
    
    D -->|<200ms| F["选择G1 GC"]
    D -->|<50ms| G["考虑Shenandoah"]
    D -->|<10ms| H["考虑ZGC"]
    
    E -->|是| I["选择ZGC"]
    E -->|否| J["选择G1 GC"]
    
    F --> K["G1是最佳选择"]
    G --> L["评估Shenandoah"]
    H --> M["评估ZGC"]
    I --> N["ZGC是最佳选择"]
    J --> K
    
    style F fill:#4caf50
    style K fill:#4caf50

8. 总结

8.1 G1的核心优势

  1. 可预测的低延迟:通过暂停时间目标控制,提供相对稳定的GC暂停时间
  2. 高吞吐量:在保证低延迟的同时,维持较高的应用吞吐量
  3. 自适应调整:能够根据应用行为自动调整各种参数
  4. 大堆支持:适合处理大内存应用场景
  5. 易于配置:默认参数在大多数情况下表现良好

8.2 适用场景

G1垃圾收集器特别适合以下场景:

  • 大内存应用:堆大小在几GB到几十GB的应用
  • 延迟敏感应用:需要可预测的GC暂停时间
  • 长期运行的服务:需要稳定性能的服务器应用
  • 复杂对象图:具有复杂对象引用关系的应用
  • 动态负载:负载模式随时间变化的应用

8.3 发展趋势

G1垃圾收集器作为JVM的默认收集器,将继续发展和优化:

  • 更好的并发性:减少STW时间,提高并发收集效率
  • 更智能的自适应:基于机器学习的参数自动调优
  • 更低的内存开销:优化内存使用,减少GC自身的开销
  • 更好的大对象处理:改进Humongous对象的分配和回收策略
  • 与新硬件的适配:更好地利用现代硬件特性

8.4 最佳实践总结

  1. 从默认配置开始:G1的默认参数经过精心调优,适合大多数应用
  2. 监控和测量:建立完善的监控体系,基于数据进行调优
  3. 应用级优化:优化应用代码往往比调整GC参数更有效
  4. 渐进式调优:一次只调整一个参数,观察效果后再进行下一步
  5. 持续关注:随着应用负载的变化,定期评估和调整GC配置

G1垃圾收集器代表了JVM垃圾收集技术的重要进步,它在延迟和吞吐量之间找到了良好的平衡点。通过理解其工作原理和合理配置,可以为大多数Java应用提供优秀的内存管理性能。随着技术的不断发展,G1将继续演进,为Java生态系统提供更强大的垃圾收集能力。