vault backup: 2026-08-08 19:01:04
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
---
|
||||
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 内存模型]]
|
||||
Reference in New Issue
Block a user