JAVA GC算法和种类(G1 & CMS对比)

34 阅读5分钟

结论

  1. 算法选择:根据应用场景选择回收器——高吞吐量选 Parallel,低延迟选 CMS/G1,大内存选 ZGC/Shenandoah。
  2. 版本适配:JDK8 后优先使用 G1,避免 CMS 的内存碎片问题。
  3. 参数调优:结合监控工具(如 VisualVM)分析 GC 日志,逐步调整参数,避免盲目设置。

何为STW

Java中Stop-The-World机制简称STW,是在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起(除了垃圾收集帮助器之外)

通过合理配置 GC 算法与参数,可显著提升应用性能与稳定性,减少因内存管理不当导致的系统卡顿或崩溃风险。

一、GC 算法概述

垃圾回收(Garbage Collection, GC)是自动管理内存的核心机制,其核心目标是识别并释放不再使用的对象内存。常见的 GC 算法可分为以下几类:

1. 引用计数法

graph TD
    A[对象A被创建] -->|初始引用计数 = 1| B[引用计数: 1]
    C[对象B引用对象A] -->|引用计数 +1| B
    D[对象C引用对象A] -->|引用计数 +1| B
    E[对象B释放对A的引用] -->|引用计数 -1| B
    F[对象C释放对A的引用] -->|引用计数 -1| B
    G[引用计数 = 0] -->|触发回收| H[对象A被GC回收]

    style A fill:#f9f,stroke:#333
    style B fill:#bbf,stroke:#333
    style H fill:#fbb,stroke:#333

原理:通过记录每个对象被引用的次数来判断其是否为垃圾。当引用计数降为零时,对象即可回收。
优点:实现简单,实时性较高。
缺点

  • 循环引用问题(如对象 A 引用 B,B 引用 A,导致计数无法归零);最终会导致内存泄漏
  • 频繁的计数增减操作影响性能。
    应用场景:Python、ActionScript3 等语言采用,但未被 Java 采用。

2. 标记-清除算法

graph TD
    subgraph 标记阶段
        A[根节点 Roots] --> B[对象A]
        A --> C[对象B]
        B --> D[对象C]
        C --> E[对象D]
        B --> F[对象E]
        G[对象F]  %% 不可达对象
        H[对象G]  %% 不可达对象

        %% 标记可达对象
        style B fill:#9f9,stroke:#333  %% 绿色表示存活
        style C fill:#9f9,stroke:#333
        style D fill:#9f9,stroke:#333
        style E fill:#9f9,stroke:#333
        style F fill:#9f9,stroke:#333
        style G fill:#f99,stroke:#333  %% 红色表示垃圾
        style H fill:#f99,stroke:#333
    end

    subgraph 清除阶段
        I[清除未标记对象] --> J[释放对象F]
        I --> K[释放对象G]
        L[内存整理后状态] --> M[对象A/B/C/D/E保留]
    end

    %% 流程连接
    标记阶段 --> 清除阶段

    classDef marked fill:#9f9,stroke:#333;
    classDef unmarked fill:#f99,stroke:#333;

原理:分为两个阶段:

  1. 标记阶段:从根节点(如栈、静态变量等)出发,遍历所有可达对象并标记;
  2. 清除阶段:回收未被标记的对象内存。
    缺点:内存碎片化严重,可能影响大对象分配效率。
    适用场景:适用于存活对象较多的老年代。

3. 标记-压缩算法

graph TD
    subgraph 标记-压缩算法
        A[标记阶段] -->|遍历根节点标记存活对象| B[内存碎片状态]
        B --> C[压缩阶段]
        C -->|移动存活对象至内存一端| D[整理后连续内存]
        D --> E[清除边界外碎片]

        style A fill:#cff,stroke:#333
        style B fill:#fcc,stroke:#333
        style C fill:#ffc,stroke:#333
        style D fill:#cfc,stroke:#333
    end

优化点:在标记-清除的基础上增加内存整理步骤,将存活对象向内存一端移动,消除碎片。
优点:解决内存碎片问题,提升内存利用率。
适用场景:适合老年代,尤其是对象存活率高的场景。

4. 复制算法

graph TD

    subgraph 复制算法
        F[内存块A 使用中] -->|存活对象复制到内存块B| G[内存块B 激活]
        G --> H[清空内存块A]
        H -->|切换使用区域| F

        style F fill:#fcc,stroke:#333
        style G fill:#cfc,stroke:#333
    end

原理:将内存分为大小相等的两块,每次只使用一块。垃圾回收时,将存活对象复制到另一块,并清空当前块。
优点:无碎片问题,分配效率高。
缺点:内存利用率仅 50%,不适用于大对象或高存活率场景。
适用场景:新生代(如 Eden 区与 Survivor 区的设计)。


二、主流的 Java 垃圾回收器(区分 JDK7 与 JDK8+)

1. JDK7 及之前的回收器

  • Serial 收集器

    • 单线程工作,适用于客户端应用或低配置环境;
    • 新生代采用复制算法,老年代采用标记-压缩算法;
    • 参数:-XX:+UseSerialGC
  • Parallel 收集器(吞吐量优先)

    • 多线程并行回收,关注系统吞吐量;
    • 新生代使用复制算法,老年代支持并行标记-压缩(-XX:+UseParallelOldGC);
    • 参数:-XX:+UseParallelGC
  • CMS 收集器(低延迟优先)

    • 并发标记清除,减少停顿时间;
    • 分为初始标记、并发标记、重新标记、并发清除四阶段;
    • 缺点:内存碎片化,可能触发 Full GC;
    • 参数:-XX:+UseConcMarkSweepGC

2. JDK8 及之后的改进

G1 收集器(Garbage-First)

1. 基本特性
  • JDK9+ 默认收集器:取代 CMS,适用于现代大内存应用。
  • 核心策略:优先回收垃圾最多的 Region(Garbage-First),最大化回收效率。
  • 算法融合:结合 标记-压缩(老年代)与 复制算法(新生代),平衡吞吐量与低延迟。
  • 启用参数-XX:+UseG1GC

2. 核心设计
设计特点说明
Region 划分堆内存划分为多个等大小 Region(默认 1MB~32MB),独立回收,避免全堆扫描。
复制算法应用存活对象少的 Region(如 Eden/Survivor)采用复制算法,无内存碎片。
标记-压缩应用老年代 Region 执行压缩整理,解决碎片问题。
可预测停顿通过 -XX:MaxGCPauseMillis 设定目标停顿时间(默认 200ms),软实时控制。

3. 工作流程
  1. 初始标记(STW) :短暂暂停,标记根直接引用对象。
  2. 并发标记:与用户线程并行,遍历对象图。
  3. 最终标记(STW) :修正并发阶段的引用变更。
  4. 筛选回收(STW) :按垃圾比例排序 Region,优先回收高垃圾 Region。

关键参数

-XX:+UseG1GC                           # 启用G1
-XX:MaxGCPauseMillis=200               # 目标停顿时间(毫秒)
-XX:G1HeapRegionSize=4m               # 设置Region大小(需为2的幂)

时序图

sequenceDiagram
    participant UserThread as 用户线程
    participant GCThread as GC线程
    participant Heap as 堆内存(多个Region)
    
    Note over GCThread,Heap: G1 收集器核心流程(兼顾低延迟与高吞吐)

    rect rgba(200,200,255,0.1)
        Note over GCThread: 初始标记(Initial Mark) - STW
        GCThread->>Heap: 1. 扫描根节点直接引用对象
        GCThread->>UserThread: 暂停用户线程(短暂)
        GCThread->>Heap: 2. 标记存活对象(快速)
        UserThread-->>GCThread: 恢复运行
    end

    rect rgba(200,255,200,0.1)
        Note over GCThread: 并发标记(Concurrent Marking)
        GCThread->>Heap: 3. 并发遍历对象图(与用户线程并行)
        loop 并发标记
            UserThread->>Heap: 用户线程修改对象引用
            GCThread->>Heap: 增量更新标记状态(SATB算法)
        end
    end

    rect rgba(255,200,200,0.1)
        Note over GCThread: 最终标记(Remark) - STW
        GCThread->>UserThread: 暂停用户线程(较短)
        GCThread->>Heap: 4. 修正并发阶段变更的引用
        GCThread->>Heap: 5. 标记最终存活对象
        UserThread-->>GCThread: 恢复运行
    end

    rect rgba(255,255,200,0.1)
        Note over GCThread: 筛选回收(Evacuation) - STW
        GCThread->>Heap: 6. 统计Region垃圾比例(Garbage-First策略)
        GCThread->>Heap: 7. 选择垃圾最多的Region优先回收
        alt 复制算法应用
            GCThread->>Heap: 复制存活对象到新Region(复制算法)
        else 标记-压缩应用
            GCThread->>Heap: 移动对象整理空间(标记-压缩算法)
        end
        GCThread->>Heap: 8. 清空旧Region(释放连续空间)
        UserThread-->>GCThread: 恢复运行
    end

    Note over Heap: 结果:堆内存高效回收<br>(低停顿 + 高吞吐)

通过时序图可见,G1 通过分阶段并发操作和智能 Region 选择,在低延迟与高吞吐间取得了平衡。

与 CMS 的对比

特性G1CMS
内存模型Region 分块连续分代(新生代/老年代)
碎片处理主动压缩(部分 Region)依赖 Full GC 压缩
停顿目标可控(软实时)不可控
适用场景大内存、低延迟中小内存、低延迟
  • ZGC 与 Shenandoah(JDK11+ 实验性)

    • 目标是将停顿时间控制在 10ms 以内;
    • 基于并发标记与压缩,适用于大内存场景。

三、GC 算法核心参数解析

1. 堆内存分配参数

  • -Xms / -Xmx:初始堆大小与最大堆大小(如 -Xms512m -Xmx1024m);
  • -XX:NewRatio:新生代与老年代的比例(默认 2,即老年代占 2/3);
  • -XX:SurvivorRatio:Eden 区与 Survivor 区的比例(默认 8:1:1)。

2. 回收器选择参数

  • 串行回收:-XX:+UseSerialGC
  • 并行回收:-XX:+UseParallelGC(新生代)、-XX:+UseParallelOldGC(老年代);
  • CMS 回收:-XX:+UseConcMarkSweepGC,需配合 -XX:CMSInitiatingOccupancyFraction(触发阈值);
  • G1 回收:-XX:+UseG1GC,可调整 -XX:MaxGCPauseMillis(目标最大停顿时间)。

3. 调优辅助参数

  • -XX:+PrintGCDetails:输出详细 GC 日志;
  • -XX:+HeapDumpOnOutOfMemoryError:内存溢出时生成堆转储文件;
  • -XX:MaxTenuringThreshold:对象晋升老年代的年龄阈值(默认 15)。