Follow me on GitHub

垃圾回收算法

垃圾回收有 3 个基本问题:

  1. 哪些对象需要回收?
  2. 何时回收?
  3. 如何回收?

垃圾回收算法解决第 3 个问题,本文仅介绍其思想,以及 JVM 中对应的垃圾收集器,不涉及具体实现:

  • 新生代:复制
    • Serial
    • ParNew
    • Parallel Scavenge
  • 老年代:标记-清除 & 标记-整理

标记-清除(Mark-Sweep)

标记清除算法分为标记、清除两步:

  1. 标记:标记所有需要回收的对象;
  2. 清除:统一回收所有被标记的对象;

标记清除是 最基础 的回收算法,因为后续的回收算法都是针对其不足进行 改进 得到的,它有两个不足:

  1. 效率问题:标记、清除两个过程的效率都不高;
  2. 空间问题:标记清除后会产生大量 不连续的内存碎片,空间碎片太多可能会导致随后需要分配 大对象 时,无法找到足够大的连续内存,从而 提前触发 下一次垃圾收集动作;

复制

复制算法解决了标记-清除的 效率 问题:

  1. 将可用内存划分为 容量相等 的两块;
  2. 每次只使用其中一块,当这块内存用完,就当 还存活的对象 复制到另一块上,然后把这块内存一次性清理掉;

相比标记-清除高效在:

  1. 每次仅对 50% 的内存进行回收;
  2. 内存分配时无需考虑 内存碎片,移动堆顶指针按顺序分配即可;

复制算法的不足:

  • 可用内存仅为实际内存的 50%,浪费严重(可通过 Eden + Survivor + 分配担保解决);
  • 对象 存活率高 时,需要很多 复制操作,效率降低;

由于这两个不足,复制算法不用于 老年代

  1. 没有其他内存为老年代做分配担保;
  2. 老年代对象存活率很高;

现代商业虚拟机都采用复制算法回收 新生代,因为新生代中的对象 98% 都是“朝生夕死”的,所以不需要按 1:1 来划分内存,而是将新生代划分为:

  • 一块较大的 Eden 区;
  • 两块较小的 Survivor 区,分别称为 from、to 区;

使用方式为:

  1. 每次使用 Eden + from 两块内存;
  2. 垃圾回收时,将 Eden + from 中的存活对象一次性 复制 到 to 区;
  3. 清理 Eden + from;
  4. to 变身为 from,from 变身为 to;

HotSpot 默认 Eden/Survivor 比例为 8:1,即 Eden 占新生代 80%,from、to 各占 10%,每次只有 to 空间被浪费,从 50% 下降到了 10%。

分配担保

新生代 98% 对象朝生夕死只是概率统计结果,极端情况下可能 100% 的对象都不会被回收,复制时 to 区无法容纳这么多对象,此时需要 老年代 进行分配担保,即 to 无法容纳时,新生代存活对象 直接进入老年代

标记-整理(Mark-Compact)

标记-整理与标记清除类似,只不过第二步不是 直接清理待回收对象,而是:

  1. 存活对象 移动到另一端;
  2. 直接清理端边界以外的内存;

该算法解决了标记-清除的空间问题,一次内存回收后,不会产生 内存碎片