垃圾收集器详解
前言
在Java虚拟机的内存管理体系中,如果说垃圾收集算法是理论基础,那么垃圾收集器就是这些理论的具体落地实现。每种收集器都有其独特的设计目标和适用场景。
理解各种垃圾收集器的工作原理和特性,对于我们选择合适的GC策略、优化应用性能至关重要。本文将深入剖析HotSpot虚拟机中的主要垃圾收集器。
INFO
没有"最好"的垃圾收集器,只有最适合特定场景的收集器。正因如此,HotSpot虚拟机才会提供多种垃圾收集器供开发者选择。
JDK默认垃圾收集器
不同JDK版本使用的默认垃圾收集器是不同的,可以通过以下命令查看:
java -XX:+PrintCommandLineFlags -version主要JDK版本的默认配置:
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模式下的虚拟机
- 优势: 简单高效,无线程交互开销
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参数配置:
# 使用Parallel收集器 + 老年代串行
-XX:+UseParallelGC
# 使用Parallel收集器 + 老年代并行
-XX:+UseParallelOldGCJDK 8默认使用Parallel Scavenge + Parallel Old组合。验证方式:
$ 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资源利用
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:8CMS收集器
CMS(Concurrent Mark Sweep)是一款以获取最短停顿时间为目标的收集器,非常符合注重用户体验的应用需求。
核心特点:
- 算法: 标记-清除算法
- 工作模式: 并发收集
- 设计目标: 最短停顿时间
- 版本支持: JDK 9标记为过时,JDK 14移除
CMS工作流程:
CMS的垃圾回收过程比前面的收集器更复杂,主要分为四个阶段:
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中被正式移除。
收集器组合关系
不同的垃圾收集器可以组合使用,但并非任意组合:
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:10INFO
实线连接表示推荐组合,虚线表示可组合但不推荐。
新生代与老年代收集器差异
新生代和老年代的收集器在设计上存在本质差异,这些差异源于两个区域对象的不同特征。
核心差异对比
| 维度 | 新生代收集器 | 老年代收集器 |
|---|---|---|
| 回收频率 | 频繁(Minor GC) | 较少(Major GC/Full GC) |
| 对象存活率 | 低(大部分朝生夕死) | 高(经过筛选的长期存活对象) |
| 主流算法 | 标记-复制 | 标记-整理/标记-清除 |
| 空间利用 | 牺牲部分空间(Survivor) | 追求高空间利用率 |
| 性能重点 | 回收速度 | 减少碎片、控制停顿 |
为什么新生代使用复制算法?
新生代对象特点是"朝生夕死",每次GC都有大量对象死亡,只需复制少量存活对象。典型的新生代回收场景:
// 电商订单处理示例
public class OrderProcessor {
public void processOrders() {
for (int i = 0; i < 10000; i++) {
// 创建临时对象处理订单
OrderRequest request = new OrderRequest();
OrderValidator validator = new OrderValidator();
// 处理完立即变成垃圾
if (validator.validate(request)) {
saveOrder(request);
}
// request和validator在循环外就可以回收
}
}
}在上述代码中,每次循环创建的临时对象在循环结束后就成为垃圾,存活率极低。
为什么老年代使用整理算法?
老年代对象存活率高,如果使用复制算法需要复制大量对象,效率低下。而且老年代没有额外空间进行分配担保。
// 缓存对象 - 长期存活在老年代
public class UserCacheManager {
// 这些对象会长期存活
private static Map<String, UserProfile> userCache = new ConcurrentHashMap<>();
private static RedisClient redisClient = new RedisClient();
private static DatabasePool dbPool = new DatabasePool(50);
// 系统配置对象 - 几乎不会被回收
private static SystemConfig config = SystemConfig.load();
}这类对象会长期驻留在老年代,每次GC存活率都很高,使用标记-整理算法更合适。
算法选择的权衡
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