垃圾回收算法四大门派:标记-清除、复制、标记-整理、分代
上一篇文章介绍了 JVM 内存分区,本篇深入讲解 GC 的核心算法。理解算法原理才能真正掌握调优技巧。
标记-清除(Mark-Sweep)— 最基础但有缺陷
流程
- 标记(Mark):从 GC Roots 出发,遍历所有存活对象,标记为”可达”
- 清除(Sweep):扫描整个区域,将未被标记的对象回收,释放内存
标记前: 标记(可达对象):
████ ██ ████ █ ████ ██ ████ █ ← 灰色为可达
(存活) (死亡) (存活) (死亡) (存活) (死亡)
清除后:
████████████ ← 留下存活对象,死亡对象被清除
↑ 清除后产生内存碎片
空闲内存
优缺点
| 优点 | 缺点 |
|---|---|
| 实现简单,不需要移动对象 | 内存碎片化:清除后空闲内存不连续 |
| 不需要额外空间 | Stop-the-World 时间长:需要扫描全部对象 |
适用场景
CMS GC 的并发清除阶段使用了改进版标记-清除算法。
复制(Copying)— 解决碎片化问题
流程
将内存划分为两块等大小的区域(From 和 To,也叫 S0/S1)。每次只使用一块。GC 时将存活对象复制到另一块,然后整体清除原区域。
Minor GC 之前(Eden + S0):
┌─────────────────────────┬────────────┐
│ Eden │ S0 │ S1 │
│ ████████ │ ████ │ (空) │
└─────────────────────────┴────────────┘
↑
使用中
Minor GC 之后:存活对象复制到 S1
┌─────────────────────────┬────────────┐
│ Eden │ S0 │ S1 │
│ ████ ████ │ (清空) │ ████ ████ │
└─────────────────────────┴────────────┘
优缺点
| 优点 | 缺点 |
|---|---|
| 无内存碎片:对象紧凑排列 | 可用内存减半:始终只有一半可用 |
| 分配效率高:通过指针碰撞即可分配 | 不适合老年代:老年代对象存活率高,复制开销大 |
适用场景
Young 代的 Minor GC。因为 Young 代对象生命周期短,存活对象少(通常 <10%),复制开销低。
标记-整理(Mark-Compact)— 兼顾效率与完整内存
流程
在标记-清除基础上增加整理(Compact)阶段:将存活对象向一端移动,然后清除边界以外的内存。
标记后: 整理(Compact)后:
存活对象 存活对象连续排列
↓ ↓
██ █ ███ █ ██████████
(有碎片) → (无碎片)
↑
移动到一端
优缺点
| 优点 | 缺点 |
|---|---|
| 无内存碎片 | Stop-the-World 时间最长 |
| 内存利用率高(100%可用) | 需要移动大量对象,开销大 |
| 适合老年代 |
适用场景
老年代的垃圾回收。经典实现如 Serial Old GC、Parallel Old GC。
分代收集(Generational Collection)— 实战主流
核心思想
不同生命周期的对象放在不同区域,采用最适合的收集策略
- Young 代:对象朝生夕灭,存活率低 → 用复制算法(成本低)
- Old 代:对象存活时间长 → 用标记-整理或标记-清除算法(成本高但频率低)
GC 频率
↑
│
┌───────────┴───────────┐
│ Old Generation │ ← 低频(几天一次)
│ (标记-整理/清除) │
└───────────┬───────────┘
│
┌───────────┴───────────┐
│ Young Generation │ ← 高频(可能每秒多次)
│ (复制算法) │
└───────────┬───────────┘
│
┌─────┴─────┐
│ Eden + S0/S1 │
└─────────────┘
Hotspot JVM 分代详情
Young Generation(通常占堆的 1/3):
- Eden 区:约占 80%,新对象分配地
- Survivor S0 / S1:各占 10%,交替使用
复制算法在两个 Survivor 区之间复制存活对象
Old Generation(通常占堆的 2/3):
- 长期存活的大对象直接进入老年代
- 经历多次 Minor GC 后仍存活的对象晋升
- 使用标记-整理算法(Parallel Old)或标记-清除(CMS)
对象晋升规则
对象年龄计数器:
新对象在 Eden 分配 → Age = 1
Minor GC 后存活且 S0/S1 有空间 → 复制到 S0 → Age++
Age >= MaxTenuringThreshold(默认 15)→ 晋升 Old Gen
JDK 8 默认:
-XX:MaxTenuringThreshold=15
-XX:TargetSurvivorRatio=50 (Survivor 区使用率达 50% 时提前晋升)
算法对比一览
| 算法 | 内存碎片 | 停顿时间 | 内存利用率 | 适用区域 |
|---|---|---|---|---|
| 标记-清除 | 有 | 长 | 100% | 老年代 |
| 复制 | 无 | 短 | 50%(Young) | Young 代 |
| 标记-整理 | 无 | 最长 | 100% | 老年代 |
| 分代收集 | 取决于子算法 | 综合最优 | 综合最优 | 全部 |
实战验证
# 查看分代信息
java -XX:+PrintGCDetails -Xms100m -Xmx100m -XX:+UseSerialGC \
-version 2>&1 | grep -i "generation"
# 对比不同 GC 的停顿时间
java -Xms512m -Xmx512m -XX:+UseSerialGC \
-XX:+PrintGCApplicationStoppedTime \
-XX:+PrintGCDateStamps \
your-app.jar
java -Xms512m -Xmx512m -XX:+UseG1GC \
-XX:+PrintGCApplicationStoppedTime \
your-app.jar
总结
- 复制算法最适合 Young 代:存活少、复制成本低
- 标记-整理适合 Old 代:无碎片、内存利用率高
- 分代收集是商用 JVM 的主流:不同区域用不同策略
- 理解算法本质才能判断何时该调整哪块区域的参数
下篇将介绍主流垃圾收集器(Serial、Parallel、CMS、G1、ZGC)的适用场景与选型原则。
评论