跳至正文
来两杯美式
返回

垃圾收集算法三板斧:标记-清除、复制、标记-整理

By 来两杯美式
发布于

这是「一文拿下 GC」系列第三篇。判定对象「该死」后,下一步是怎么回收。三大经典算法——标记-清除、复制、标记-整理——各有取舍,现代收集器都是它们的组合或变种。

一、标记-清除算法(Mark-Sweep)

1.1 核心思想

算法分为「标记」和「清除」两个阶段:

  1. 标记阶段:从 GC Roots 出发,标记所有可达对象为「存活」
  2. 清除阶段统一回收所有未被标记的对象(即不可达对象)

这是最「朴素」的算法——其他两个算法都是它的改进版。

1.2 缺点

效率问题

空间问题

后果:明明堆还有 30% 空闲,但放不下一个 20% 的连续大对象——只能再 GC 一次。

1.3 适用场景

二、复制算法(Copying)

2.1 核心思想

将内存分为大小相等的两块,每次只使用其中一块:

  1. 这块内存用完时,将存活的对象复制到另一块
  2. 然后将已使用过的内存空间一次性清理
  3. 内存分配时也无需考虑内存空间碎片等复杂情况
  4. 只需要移动堆顶指针按顺序分配内存即可
  5. 实现简单,运行高效

2.2 缺点

2.3 HotSpot 的优化:Eden + Survivor

IBM 公司专门研究表明,新生代对象 98% 是「朝生夕死」——所以无需按照 1:1 分配内存空间。

HotSpot 把内存分为:

每次使用 Eden 和其中一块 Survivor

  1. GC 时把存活对象从 Eden + 当前 Survivor 复制到另一块 Survivor
  2. 然后清空 Eden + 刚用过的 Survivor
  3. 角色互换

HotSpot 默认的 Eden 和 Survivor 大小比例为 8:1——也就是整个新生代可用容量为新生代总容量的 90%(80% Eden + 10% Survivor),只浪费 10%。

2.4 分配担保(Handle Promotion)

问题:没办法保证每次回收都有不多于 10% 的对象存活。

解决:当 Survivor 空间不够用时,需要依赖老年代内存进行分配担保(Handle Promotion)——多余的存活对象直接通过分配担保机制进入老年代。

2.5 适用场景

三、标记-整理算法(Mark-Compact)

3.1 核心思想

适合存活率较高的老年代

  1. 标记的过程和标记-清除的标记过程一样
  2. 后续步骤不是直接对可回收对象进行清理,而是让所有存活对象向一端移动
  3. 然后直接清理掉端边界以外的内存

标记-整理算法:优化前碎片化 vs 优化后连续化

3.2 关键细节

3.3 适用场景

四、三大算法对比

维度标记-清除复制标记-整理
速度中等(标记+清除都需遍历)最快(只复制存活对象)较慢(标记+移动)
空间开销少(只需标记位)(浪费 50% 或 10%)少(原地整理)
碎片严重
移动对象不移动移动移动
适用区域老年代(CMS)新生代老年代
STW较短较短较长(需要更新引用)

五、现代收集器的算法组合

实际生产中,没有收集器只用一种算法——都是混合:

2017 年冷知识:当年 G1 已经是 HotSpot 的「未来之星」,但生产环境大量跑的还是 Parallel Old(吞吐量优先)或 CMS(停顿优先)。2017 年 G1 还在收集器架构革命期,很多参数和 region 策略都在变动。

小结

下一篇讲这些算法在生产中的实际载体——七大垃圾收集器(Serial/ParNew/Parallel Scavenge/Serial Old/Parallel Old/CMS/G1)的工作原理和适用场景。


分享这篇文章:
通过邮件分享这篇文章✓ 链接已复制
所属专题
Java GC
第 3 / 5 篇
查看系列全部文章
  1. 01.GC 调优入门:参数、工具与参考值
  2. 02.GC 怎么判断对象「该死了」:引用计数 vs 可达性分析
  3. 03.垃圾收集算法三板斧:标记-清除、复制、标记-整理
  4. 04.垃圾收集器大阅兵:Serial/ParNew/Parallel/CMS/G1
  5. 05.堆内存区域全景:Eden/Survivor/Old + PermGen/Metaspace 演进

上一篇
垃圾收集器大阅兵:Serial/ParNew/Parallel/CMS/G1
下一篇
GC 怎么判断对象「该死了」:引用计数 vs 可达性分析