Files
autumn-recruitment/01.Java/jvm/垃圾回收算法与收集器.md

202 lines
8.5 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [java/lang, gc-algorithm, cms, g1, zgc, young-gen]
create time: 2026-08-08 18:00
update time: 2026-08-08 18:00
---
# 垃圾回收算法与收集器
## 概述
垃圾回收是 JVM 自动管理内存的核心机制。理解 GC 算法的优缺点和不同收集器的设计哲学,能帮助你在生产环境中做出正确的选型决策——没有最好的收集器,只有最适合业务场景的收集器。
## 核心原理
### 标记-清除 / 标记-复制 / 标记-整理
三种经典 GC 算法各有侧重,对应不同的内存碎片和空间利用率权衡。
```mermaid
flowchart LR
subgraph 标记清除["标记-清除算法"]
direction TB
MS1["1. 标记存活对象"]
MS2["2. 清除未标记区域"]
end
subgraph 标记复制["标记-复制算法"]
direction TB
MC1["1. 标记存活对象"]
MC2["2. 复制到半区"]
MC3["3. 清理整块原区"]
end
subgraph 标记整理["标记-整理算法"]
direction TB
MI1["1. 标记存活对象"]
MI2["2. 存活对象向一端移动"]
MI3["3. 清理边界外内存"]
end
```
#### 标记-清除(Mark-Sweep)
最直接的方案:先标记所有存活对象,然后统一清除未被标记的对象。
| 优点 | 缺点 |
|------|------|
| 实现简单,不需要额外空间 | 产生大量内存碎片,大对象分配可能提前触发 GC |
| 适合对象存活率高的场景 | 两次扫描(标记 + 清除),STW 时间较长 |
#### 标记-复制(Mark-Copy)
将可用内存分为大小相等的两块,每次只用其中一块。GC 时把存活对象复制到另一块,然后清空已用区域。新生代 Eden + Survivor 就是基于此思想。
| 优点 | 缺点 |
|------|------|
| 不会产生内存碎片 | 可用内存减半,浪费严重 |
| 复制即整理,天然紧凑 | 对象在 Survivor 间来回复制,增加 CPU 开销 |
> [!TIP] HotSpot 通过 Eden:S0:S1 = 8:1:1 的比例设计,让绝大多数新对象直接分配到 Eden,只有少量"长寿"对象才进入 Survivor,大大缓解了空间浪费问题。
#### 标记-整理(Mark-Compact)
标记阶段同标记-清除,但后续步骤是让存活对象向内存一端移动,然后清理掉边界外的内存。老年代主要采用此算法。
| 优点 | 缺点 |
|------|------|
| 无内存碎片 | 移动对象需要更新所有引用指针,代价高 |
| STW 时间短于标记-清除 | 涉及对象拷贝,CPU 占用较高 |
### 分代理论的依据
现代 JVM 采用分代收集策略:**收集器选择不同的算法作用于不同的代**。这基于一个经验结论——**对象的生命周期呈现明显的分层分布**:
- **朝生夕死**:绝大部分对象在 Eden 区出生后即死亡,存活率极低。
- **中途夭折**:部分对象经过几次 Minor GC 后仍然存活,但很快会死亡。
- **长生不老**:少数对象经历多次 Minor GC 后依然存活,最终进入老年代。
基于此,JVM 将堆划分为新生代和老年代,分别使用标记-复制和标记-整理算法,达到整体最优。
### CMS 收集器(Concurrent Mark Sweep)
CMS 的目标是最小化 STW 时间,适用于对响应时间敏感的业务场景。它是 JDK 7 时代默认的低延迟收集器。
**四个阶段**:
```mermaid
stateDiagram-v2
[*] --> 初始标记: 触发 CMSCollectionBeginning
初始标记 --> 并发标记: STW 极短
并发标记 --> 预标记: 用户线程同时运行
预标记 --> 并发清除: STW 较短
并发清除 --> 并发重置
并发重置 --> [*]: 回收完成
```
| 阶段 | 说明 | STW |
|------|------|-----|
| 初始标记(Initial Mark) | 标记 Direct GC Roots,需要停顿 | 短 |
| 并发标记(Concurrent Mark) | 从 GC Roots 开始遍历标记树 | 无 |
| 预标记(Re-Mark) | 修正并发标记期间因用户程序操作导致的变化 | 中 |
| 并发清除(Concurrent Sweep) | 清除标记信息为空的区间 | 无 |
> [!WARNING] CMS 有三个显著缺陷:
> 1. **浮动垃圾**:并发清扫阶段又有新对象产生,这些"浮动垃圾"只能等下一次 GC 处理。
> 2. **内存碎片**:基于标记-清除算法,容易产生碎片,可能导致大对象无法分配而提前触发 Full GC。
> 3. **CPU 资源敏感**:并发阶段和用户代码共享 CPU,负载过高时会导致平均响应时间变长。
CMS 在 JDK 9 中被标记为废弃,JDK 14 中被彻底移除。
### G1 收集器(Garbage-First)
G1 是 JDK 9 默认收集器,面向多核处理器和大容量堆(通常 ≥ 6GB)。它将堆划分为多个大小相等的 Region,不再物理分隔新生代和老年代。
**关键概念**:
| 概念 | 说明 |
|------|------|
| Region | G1 将堆划分为最多 2048 个 Region(每个大小 1~32MB),每个 Region 可以扮演 Eden、Survivor、Old 或 Humongous 角色 |
| Humongous Region | 超过半个 Region 大小的超大对象,直接分配到 Humongous Region,避免碎片问题 |
| RSet(Remembered Set) | 记录跨 Region 引用,使 G1 能精确知道哪些 Region 引用了其他 Region 的对象 |
| Mixed GC | G1 特有的回收模式,一次性回收多个 Region(包括 Young + Old) |
```mermaid
flowchart TD
A["分配对象到 Eden Region"] --> B{"是否够分配?"}
B -->|否| C["Minor GC<br/>回收年轻代 Regions"]
C --> D{是否仍有空间?}
D -->|是| E["分配成功"]
D -->|否| F["Full GC"]
B -->|是| E
G["Major / Mixed GC"] --> H["根据Region ROI排序"]
H --> I["优先回收价值最大的Region"]
```
G1 的优势在于:
- **可预测的暂停时间**:通过 `-XX:MaxGCPauseMillis` 设定目标,G1 内部会自动调整各区域回收策略。
- **无碎片**:Mixed GC 后会进行整理。
- **并行与并发**:充分利用多核能力。
### ZGC(Z Garbage Collector)
ZGC 从 JDK 11 起实验性引入,JDK 15 成为生产级收集器。它的核心创新在于将原本沉重的写屏障移到加载时机,借助两个黑科技:**染色指针**和**加载屏障**。
**染色指针(Colored Pointers)**:在指针的高位 bit 上编码颜色信息(指向、已访问、重定位、预留),这样在读取对象引用时就能快速判断状态。
**加载屏障(Load Barrier)**:当线程读取一个引用时,如果发现有重定位标记,就执行重定位操作。这一步发生在读引用之前而非写引用之后,因此无需在所有写入口处插入屏障。
```mermaid
sequenceDiagram
participant T as 线程
participant M as 内存对象
participant LB as 加载屏障
participant RC as 重定位
T->>+LB: 读取引用地址
alt 有重定位标记
LB-->>RC: 发现需要重定位
RC->>M: 更新对象位置
RC-->>LB: 清除重定位标记
end
LB-->>T: 返回新地址
LB->>-T: 继续执行
```
ZGC 的特点:
- **暂停时间不超过 10ms**,无论堆大小(JDK 11 支持最大 320GB,JDK 15+ 支持 TB 级)。
- **与应用程序并发执行**,几乎不停顿用户线程。
- **不支持优先级队列**,不适合对延迟极度敏感的场景(如游戏服务器)。
> [!NOTE] ZGC 的染色指针依赖于平台指针压缩。x86_64 有足够高位可用,但 ARM 架构可能需要不同实现。目前仅支持 x86_64、AArch64 和 SPARC64。
### 收集器对比选型表
| 维度 | CMS | G1 | ZGC | Shenandoah |
|------|-----|----|-----|-----------|
| 首次引入 | JDK 1.4.1 | JDK 7u4 (JDK 9默认) | JDK 11 (JDK 15正式) | JDK 12 |
| 最大堆 | ~16GB | ~64GB+ | ~320GB (JDK 11) | ~320GB |
| 最大暂停 | 100-500ms | 目标可配 | < 10ms | < 10ms |
| 吞吐量 | 中等 | 较高 | 略低 | 略低 |
| 内存开销 | 低 | 中(RSet) | 低(染色指针) | 中高 |
| 堆碎片 | 有 | 无 | 无 | 无 |
| 适用场景 | 遗留系统 | 通用后端服务 | 超大堆、超低延迟 | 同 ZGC |
| 推荐度 | 不推荐 | **首选** | 需求匹配时首选 | 备选 |
## 实践场景
**选型决策树**:
1. 堆 < 4GB → Parallel GC(吞吐优先)或 CMS(如果你还在 JDK 8 且必须低延迟)
2. 4GB ≤ 堆 ≤ 32GB → G1(大多数生产环境的默认选择)
3. 堆 > 32GB 且要求暂停 < 10ms → ZGC
4. 对延迟极其敏感且不在意吞吐量 → ZGC 或 Shenandoah
5. 离线批处理、追求最高吞吐 → Parallel Old
> [!TIP] 面试常考点:为什么不建议在生产环境使用 Parallel GC?因为它的所有 GC 动作都在一个线程内串行完成,STW 时间与堆大小呈正相关,不适合交互式应用。
## 关联笔记
- [[JVM 内存模型]]