跳到主要内容
EN
返回

垃圾回收算法四大门派:标记-清除、复制、标记-整理、分代

Zhou Xunyou 6 分钟阅读 jvm
分享

上一篇文章介绍了 JVM 内存分区,本篇深入讲解 GC 的核心算法。理解算法原理才能真正掌握调优技巧。

标记-清除(Mark-Sweep)— 最基础但有缺陷

流程

  1. 标记(Mark):从 GC Roots 出发,遍历所有存活对象,标记为”可达”
  2. 清除(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 GCParallel 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)的适用场景与选型原则。

评论