垃圾回收算法介绍

183 阅读5分钟

JVM垃圾回收算法包括标记-清除(碎片多)、复制(高效但耗空间)、标记-整理(碎片少)。分代理论将堆分为年轻代(复制算法)和老年代(标记清除/整理)。现代回收器如CMS(低延迟)、G1(平衡)、ZGC(亚毫秒停顿)针对不同场景优化,选型需考虑吞吐、延迟及堆大小。


一、基础垃圾回收算法

1. 标记-清除(Mark-Sweep)

  • 原理

    1. 标记阶段:从 GC Roots 出发,遍历所有可达对象并标记。
    2. 清除阶段:扫描堆内存,回收未被标记的对象。
  • 优点:实现简单,无需移动对象。

  • 缺点

    • 内存碎片:回收后内存不连续,可能导致大对象分配失败。
    • 两次遍历:效率较低,需两次全堆扫描。
  • 适用场景:老年代(如 CMS 回收器的初始设计)。

2. 复制算法(Copying)

  • 原理

    1. 将内存分为两块(如 Eden 区和 Survivor 区)。
    2. 仅使用其中一块,存活对象复制到另一块,清空原内存。
  • 优点

    • 无内存碎片:复制后内存连续。
    • 高效回收:仅处理存活对象,适合短生命周期对象。
  • 缺点

    • 空间浪费:需预留 50% 空闲内存。
    • 复制开销:对象存活率高时效率下降。
  • 适用场景:年轻代(如 Serial、ParNew 回收器)。

3. 标记-整理(Mark-Compact)

  • 原理

    1. 标记阶段:同标记-清除,标记所有可达对象。
    2. 整理阶段:将存活对象向内存一端移动,清理边界外内存。
  • 优点

    • 避免碎片:内存连续,适合长期存活对象。
    • 空间利用率高:无需预留额外内存。
  • 缺点

    • 移动开销:对象移动增加回收时间。
    • 两次遍历:标记和整理均需全堆扫描。
  • 适用场景:老年代(如 Serial Old、Parallel Old 回收器)。

4. 引用计数(Reference Counting)

  • 原理

    • 每个对象维护一个引用计数器,引用增减时更新计数器。
    • 计数器为 0 时立即回收。
  • 优点

    • 实时回收:无需等待 GC 触发。
  • 缺点

    • 循环引用:无法回收相互引用的对象(如 A→B→A)。
    • 性能开销:频繁更新计数器影响效率。
  • 适用场景:非 JVM 系统(如 Python、Objective-C)。


二、分代收集算法

基于分代理论,将堆内存划分为年轻代和老年代,针对不同代使用不同算法:

1. 年轻代:复制算法

  • 流程

    1. 新对象分配至 Eden 区
    2. Minor GC 触发时,将 Eden 和 Survivor 区存活对象复制到另一块 Survivor 区。
    3. 年龄计数器达到阈值(默认 15)的对象晋升至老年代。
  • 优化

    • Survivor 区设计:通过两个 Survivor 区(S0/S1)减少内存浪费。
    • 分配担保:当 Survivor 区空间不足时,直接晋升至老年代。

2. 老年代:标记-清除或标记-整理

  • 标记-清除:用于 CMS 回收器,减少停顿但产生碎片。
  • 标记-整理:用于 Parallel Old 回收器,避免碎片但增加停顿时间。

三、现代高级回收算法

1. 并发标记清除(CMS, Concurrent Mark-Sweep)

  • 目标:减少老年代回收的停顿时间。

  • 流程

    1. 初始标记(Stop-The-World):标记 GC Roots 直接关联的对象。
    2. 并发标记:遍历对象图,标记所有可达对象。
    3. 重新标记(Stop-The-World):修正并发标记期间变动的引用。
    4. 并发清除:回收不可达对象。
  • 优点:并发执行,降低停顿时间。

  • 缺点

    • 内存碎片:需定期 Full GC 整理内存。
    • CPU 敏感:并发阶段占用 CPU 资源。
  • 适用场景:对延迟敏感的 Web 服务。

2. G1(Garbage-First)

  • 目标:平衡吞吐量和延迟,适合大堆内存。

  • 核心思想

    • 将堆划分为多个 Region(大小相同,1MB~32MB)。
    • 优先回收垃圾最多的 Region(Garbage-First)。
  • 流程

    1. 初始标记(Stop-The-World):标记 GC Roots 直接关联的对象。
    2. 并发标记:遍历对象图,标记存活对象。
    3. 最终标记(Stop-The-World):处理 SATB(Snapshot-At-The-Beginning)记录。
    4. 筛选回收(Stop-The-World):选择垃圾比例高的 Region 回收。
  • 优点

    • 可预测停顿:通过设置 -XX:MaxGCPauseMillis 控制目标停顿时间。
    • 内存整理:部分 Region 回收时执行整理。
  • 缺点:内存占用较高(维护 Region 元数据)。

  • 适用场景:大内存、对延迟和吞吐均有要求的应用。

3. ZGC(Z Garbage Collector)

  • 目标:亚毫秒级停顿,支持 TB 级堆内存。

  • 核心技术

    • 染色指针(Colored Pointers):在指针中存储元数据(如标记、重定位信息)。
    • 读屏障(Read Barrier):拦截指针访问,处理并发回收时的对象移动。
  • 流程

    1. 并发标记:遍历对象图,标记存活对象。
    2. 并发预备重分配:确定需回收的 Region。
    3. 并发重分配:移动对象并更新引用。
    4. 并发重映射:修正旧地址的引用。
  • 优点

    • 极低停顿:停顿时间不随堆大小增长。
    • 全堆并发:几乎所有阶段均可并发执行。
  • 缺点:JDK 15+ 才正式生产可用,兼容性要求高。

  • 适用场景:超大规模内存、严格低延迟的应用(如金融交易系统)。


四、算法对比与选型建议

算法/回收器优点缺点适用场景
Serial简单,单线程低开销停顿时间长客户端应用、小内存
Parallel高吞吐量停顿时间较长后端批处理、计算密集型
CMS低延迟内存碎片、CPU 敏感Web 服务、响应优先
G1平衡吞吐和延迟,大堆友好内存占用较高通用服务器应用
ZGC亚毫秒级停顿,超大堆支持需最新 JDK,兼容性限制超低延迟、TB 级内存

五、总结

  1. 基础算法

    • 标记-清除简单但碎片多,复制算法高效但浪费空间,标记-整理平衡碎片与效率。
  2. 分代理论

    • 年轻代用复制算法快速回收,老年代用标记-清除/整理处理长生命周期对象。
  3. 现代回收器

    • CMS 以低延迟为目标,G1 平衡吞吐与延迟,ZGC 追求极致低停顿。
  4. 选型关键

    • 根据应用场景(吞吐、延迟、堆大小)和 JDK 版本选择合适的回收器。