Skip to content

垃圾收集器详解

前言

在Java虚拟机的内存管理体系中,如果说垃圾收集算法是理论基础,那么垃圾收集器就是这些理论的具体落地实现。每种收集器都有其独特的设计目标和适用场景。

理解各种垃圾收集器的工作原理和特性,对于我们选择合适的GC策略、优化应用性能至关重要。本文将深入剖析HotSpot虚拟机中的主要垃圾收集器。

INFO

没有"最好"的垃圾收集器,只有最适合特定场景的收集器。正因如此,HotSpot虚拟机才会提供多种垃圾收集器供开发者选择。

JDK默认垃圾收集器

不同JDK版本使用的默认垃圾收集器是不同的,可以通过以下命令查看:

bash
java -XX:+PrintCommandLineFlags -version

主要JDK版本的默认配置:

mermaid
graph LR
    A[JDK 8] --> B[Parallel Scavenge<br/>新生代]
    A --> C[Parallel Old<br/>老年代]
    
    D[JDK 9+] --> E[G1<br/>整堆回收]
    
    style A fill:#E94B3C,stroke:#B8341F,stroke-width:2px,rx:10,ry:10
    style B fill:#4A90E2,stroke:#2E5C8A,stroke-width:2px,rx:10,ry:10
    style C fill:#4A90E2,stroke:#2E5C8A,stroke-width:2px,rx:10,ry:10
    style D fill:#50C878,stroke:#2E7D4E,stroke-width:2px,rx:10,ry:10
    style E fill:#9370DB,stroke:#6B4DB3,stroke-width:2px,rx:10,ry:10

新生代收集器

Serial收集器

Serial收集器是最基础、历史最悠久的单线程垃圾收集器。"单线程"有两层含义:

  • 只使用一个CPU或一条收集线程完成垃圾回收
  • 在进行垃圾收集时,必须暂停所有工作线程(Stop The World)

核心特点:

  • 算法: 标记-复制算法
  • 工作模式: 单线程串行
  • 应用场景: Client模式下的虚拟机
  • 优势: 简单高效,无线程交互开销
mermaid
sequenceDiagram
    participant UT as 用户线程
    participant GC as GC线程
    
    UT->>UT: 正常运行
    Note over UT,GC: 发生Minor GC
    UT->>UT: Stop The World
    GC->>GC: Serial GC工作
    Note over GC: 单线程复制存活对象
    GC->>UT: GC完成
    UT->>UT: 恢复运行

尽管存在Stop The World,但Serial收集器由于没有线程切换开销,在单核环境或小内存应用中依然有其价值。

ParNew收集器

ParNew是Serial收集器的多线程并行版本,除了使用多线程进行垃圾收集外,其他行为与Serial完全一致。

核心特点:

  • 算法: 标记-复制算法
  • 工作模式: 多线程并行
  • 应用场景: Server模式,配合CMS使用
  • 特殊性: 是唯一能与CMS配合工作的新生代收集器

并行与并发的区别:

  • 并行(Parallel): 多条GC线程同时工作,但用户线程处于等待状态
  • 并发(Concurrent): GC线程与用户线程同时执行(可能交替执行)

Parallel Scavenge收集器

Parallel Scavenge同样是基于标记-复制算法的多线程收集器,但它的关注点与其他收集器不同。

核心特点:

  • 算法: 标记-复制算法
  • 工作模式: 多线程并行
  • 关注点: 吞吐量优先
  • 自适应调节: 支持GC自适应调节策略

吞吐量 = 用户代码运行时间 / (用户代码运行时间 + GC时间)

JVM参数配置:

bash
# 使用Parallel收集器 + 老年代串行
-XX:+UseParallelGC

# 使用Parallel收集器 + 老年代并行
-XX:+UseParallelOldGC

JDK 8默认使用Parallel Scavenge + Parallel Old组合。验证方式:

bash
$ java -XX:+PrintCommandLineFlags -version
-XX:InitialHeapSize=262921408 
-XX:MaxHeapSize=4206742528 
-XX:+PrintCommandLineFlags 
-XX:+UseCompressedClassPointers 
-XX:+UseCompressedOops 
-XX:+UseParallelGC
java version "1.8.0_211"
Java(TM) SE Runtime Environment (build 1.8.0_211-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.211-b12, mixed mode)

INFO

指定-XX:+UseParallelGC会自动启用-XX:+UseParallelOldGC,可用-XX:-UseParallelOldGC禁用。

适用场景:

Parallel Scavenge适合后台计算任务,不需要太多交互。通过提高吞吐量可以最高效地利用CPU时间,尽快完成计算任务。

老年代收集器

Serial Old收集器

Serial Old是Serial收集器的老年代版本,同样是单线程收集器。

核心特点:

  • 算法: 标记-整理算法
  • 工作模式: 单线程串行
  • 主要用途:
    • JDK 1.5及之前与Parallel Scavenge搭配使用
    • 作为CMS收集器的后备方案

Parallel Old收集器

Parallel Old是Parallel Scavenge的老年代版本,在JDK 1.6中才开始提供。

核心特点:

  • 算法: 标记-整理算法
  • 工作模式: 多线程并行
  • 应用场景: 与Parallel Scavenge组合,注重吞吐量和CPU资源利用
mermaid
graph TB
    A[堆内存] --> B[新生代<br/>Parallel Scavenge]
    A --> C[老年代<br/>Parallel Old]
    
    B --> D[Eden区]
    B --> E[Survivor 0]
    B --> F[Survivor 1]
    
    style A fill:#E8E8E8,stroke:#999,stroke-width:2px,rx:10,ry:10
    style B fill:#4A90E2,stroke:#2E5C8A,stroke-width:2px,rx:10,ry:10
    style C fill:#E94B3C,stroke:#B8341F,stroke-width:2px,rx:10,ry:10
    style D fill:#A8D8EA,stroke:#5A8CAA,stroke-width:1px,rx:8,ry:8
    style E fill:#A8D8EA,stroke:#5A8CAA,stroke-width:1px,rx:8,ry:8
    style F fill:#A8D8EA,stroke:#5A8CAA,stroke-width:1px,rx:8,ry:8

CMS收集器

CMS(Concurrent Mark Sweep)是一款以获取最短停顿时间为目标的收集器,非常符合注重用户体验的应用需求。

核心特点:

  • 算法: 标记-清除算法
  • 工作模式: 并发收集
  • 设计目标: 最短停顿时间
  • 版本支持: JDK 9标记为过时,JDK 14移除

CMS工作流程:

CMS的垃圾回收过程比前面的收集器更复杂,主要分为四个阶段:

mermaid
graph LR
    A[初始标记<br/>STW] --> B[并发标记<br/>Concurrent]
    B --> C[重新标记<br/>STW]
    C --> D[并发清除<br/>Concurrent]
    
    style A fill:#E94B3C,stroke:#B8341F,stroke-width:2px,rx:10,ry:10
    style B fill:#50C878,stroke:#2E7D4E,stroke-width:2px,rx:10,ry:10
    style C fill:#E94B3C,stroke:#B8341F,stroke-width:2px,rx:10,ry:10
    style D fill:#50C878,stroke:#2E7D4E,stroke-width:2px,rx:10,ry:10

各阶段详解:

1. 初始标记(Initial Mark):

  • 短暂停顿,标记GC Roots直接关联的对象
  • 速度很快

2. 并发标记(Concurrent Mark):

  • 从GC Roots直接关联对象开始遍历整个对象图
  • 耗时较长,但与用户线程并发执行
  • 不保证实时性,因为用户线程可能持续更新引用

3. 重新标记(Remark):

  • 修正并发标记期间因用户程序运行导致的标记变动
  • 停顿时间通常比初始标记稍长,但远小于并发标记
  • 需要STW

4. 并发清除(Concurrent Sweep):

  • 清理标记为死亡的对象
  • 与用户线程并发执行

CMS的优缺点:

优点:

  • 并发收集,降低停顿时间
  • 用户体验好

缺点:

  • CPU资源敏感: 并发阶段占用部分CPU资源,降低应用吞吐量
  • 无法处理浮动垃圾: 并发清除阶段产生的新垃圾只能等下次GC清理
  • 内存碎片问题: 标记-清除算法会产生大量碎片,可能导致频繁Full GC

INFO

CMS在Java 9中被标记为过时(deprecated),在Java 14中被正式移除。

收集器组合关系

不同的垃圾收集器可以组合使用,但并非任意组合:

mermaid
graph TB
    subgraph 新生代收集器
    A[Serial]
    B[ParNew]
    C[Parallel<br/>Scavenge]
    end
    
    subgraph 老年代收集器
    D[Serial Old]
    E[CMS]
    F[Parallel Old]
    end
    
    A -.-> D
    A -.-> E
    B -.-> E
    C -.-> D
    C -.-> F
    
    style A fill:#4A90E2,stroke:#2E5C8A,stroke-width:2px,rx:10,ry:10
    style B fill:#4A90E2,stroke:#2E5C8A,stroke-width:2px,rx:10,ry:10
    style C fill:#4A90E2,stroke:#2E5C8A,stroke-width:2px,rx:10,ry:10
    style D fill:#E94B3C,stroke:#B8341F,stroke-width:2px,rx:10,ry:10
    style E fill:#E94B3C,stroke:#B8341F,stroke-width:2px,rx:10,ry:10
    style F fill:#E94B3C,stroke:#B8341F,stroke-width:2px,rx:10,ry:10

INFO

实线连接表示推荐组合,虚线表示可组合但不推荐。

新生代与老年代收集器差异

新生代和老年代的收集器在设计上存在本质差异,这些差异源于两个区域对象的不同特征。

核心差异对比

维度新生代收集器老年代收集器
回收频率频繁(Minor GC)较少(Major GC/Full GC)
对象存活率低(大部分朝生夕死)高(经过筛选的长期存活对象)
主流算法标记-复制标记-整理/标记-清除
空间利用牺牲部分空间(Survivor)追求高空间利用率
性能重点回收速度减少碎片、控制停顿

为什么新生代使用复制算法?

新生代对象特点是"朝生夕死",每次GC都有大量对象死亡,只需复制少量存活对象。典型的新生代回收场景:

java
// 电商订单处理示例
public class OrderProcessor {
    public void processOrders() {
        for (int i = 0; i &lt; 10000; i++) {
            // 创建临时对象处理订单
            OrderRequest request = new OrderRequest();
            OrderValidator validator = new OrderValidator();
            
            // 处理完立即变成垃圾
            if (validator.validate(request)) {
                saveOrder(request);
            }
            // request和validator在循环外就可以回收
        }
    }
}

在上述代码中,每次循环创建的临时对象在循环结束后就成为垃圾,存活率极低。

为什么老年代使用整理算法?

老年代对象存活率高,如果使用复制算法需要复制大量对象,效率低下。而且老年代没有额外空间进行分配担保。

java
// 缓存对象 - 长期存活在老年代
public class UserCacheManager {
    // 这些对象会长期存活
    private static Map&lt;String, UserProfile&gt; userCache = new ConcurrentHashMap&lt;&gt;();
    private static RedisClient redisClient = new RedisClient();
    private static DatabasePool dbPool = new DatabasePool(50);
    
    // 系统配置对象 - 几乎不会被回收
    private static SystemConfig config = SystemConfig.load();
}

这类对象会长期驻留在老年代,每次GC存活率都很高,使用标记-整理算法更合适。

算法选择的权衡

mermaid
graph TB
    A[内存区域] --> B{对象存活率?}
    B -->|低<br/>新生代| C[标记-复制算法]
    B -->|高<br/>老年代| D[标记-整理算法]
    
    C --> E[优点:<br/>简单高效<br/>无碎片]
    C --> F[代价:<br/>需要额外空间]
    
    D --> G[优点:<br/>无需额外空间<br/>高空间利用率]
    D --> H[代价:<br/>移动对象<br/>复杂度高]
    
    style A fill:#E8E8E8,stroke:#999,stroke-width:2px,rx:10,ry:10
    style C fill:#4A90E2,stroke:#2E5C8A,stroke-width:2px,rx:10,ry:10
    style D fill:#E94B3C,stroke:#B8341F,stroke-width:2px,rx:10,ry:10
    style E fill:#D4F1D4,stroke:#7CB87C,stroke-width:1px,rx:8,ry:8
    style F fill:#FFD4D4,stroke:#C87C7C,stroke-width:1px,rx:8,ry:8
    style G fill:#D4F1D4,stroke:#7CB87C,stroke-width:1px,rx:8,ry:8
    style H fill:#FFD4D4,stroke:#C87C7C,stroke-width:1px,rx:8,ry:8

选择合适的垃圾收集器

不同的应用场景应该选择不同的垃圾收集器:

客户端应用

  • 推荐: Serial + Serial Old
  • 原因: 简单高效,内存占用小

服务端应用(吞吐量优先)

  • 推荐: Parallel Scavenge + Parallel Old
  • 适用: 后台批处理、科学计算、大数据分析
  • 特点: 最大化CPU利用率

服务端应用(响应时间优先)

  • 推荐: ParNew + CMS(JDK 8)或G1(JDK 9+)
  • 适用: 互联网应用、Web服务
  • 特点: 减少停顿时间,提升用户体验

大内存应用

  • 推荐: G1或ZGC
  • 适用: 堆内存>4GB的应用
  • 特点: 可预测停顿,内存整理

小结

本文详细介绍了HotSpot虚拟机中的主要垃圾收集器,包括它们的算法、特点和适用场景。关键要点:

  • Serial/Serial Old: 单线程,适合客户端
  • ParNew: Serial的多线程版本,配合CMS使用
  • Parallel Scavenge/Old: 吞吐量优先,JDK 8默认
  • CMS: 低停顿,已在JDK 14移除
  • 新生代和老年代收集器采用不同算法,源于对象存活率差异

随着Java版本演进,G1已成为JDK 9+的默认收集器,而ZGC等新一代收集器也在不断发展。下一篇文章我们将深入探讨G1收集器的原理和优化策略。

参考资料

  • 《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)》- 周志明
  • The Java Virtual Machine Specification - Java SE 8 Edition
  • OpenJDK官方文档

更新: 2025-12-04 17:35:28
原文: https://www.yuque.com/u22210564/zoxfmt/doc-05-jvm-03-08

Java 后端面试知识库