常见的垃圾回收算法有哪些?
简化版
三种基础算法:标记-清除(标出垃圾直接清,会产生内存碎片)、复制(经典半区模型把存活对象复制到另一块空间,能获得连续空间但需要预留复制目标)、标记-整理(标记后把存活对象向一端挪、清掉边界外,无碎片但慢)。实际的分代收集把它们组合用:年轻代用复制(存活少、复制快),老年代用标记-清除或标记-整理(存活多、不能浪费一半空间)。
详细版
① 标记-清除(Mark-Sweep) 分两步:先标记所有存活对象(从 GC Root 可达性分析),再清除没标记的。
- 优点:简单,不用移动对象。
- 缺点:产生大量内存碎片(清掉的对象散落各处),后续大对象可能因没有连续空间而提前触发 GC;且标记、清除两阶段效率都不高。
② 复制(Copying) 把内存分成两半,只用一半。GC 时把存活对象复制到另一半,然后把原来那半整个清空。
- 优点:无碎片(复制后紧凑排列)、分配快(只需移动指针)。
- 缺点:内存利用率只有一半,浪费严重;存活对象多时复制成本高。
③ 标记-整理(Mark-Compact) 标记阶段同标记-清除,但之后不是直接清,而是把所有存活对象向内存一端移动,再清掉边界外的所有空间。
- 优点:无碎片,且不浪费一半内存。
- 缺点:要移动对象、更新引用,性能开销大,且移动时通常要 STW。
完整版教学
一、一切的起点:先判断谁是垃圾
三种算法都先要回答「哪些对象是垃圾」。JVM 用可达性分析:从 GC Roots(栈里的局部变量、静态变量、常量等)出发,能引用到的对象是存活的,引用不到的就是垃圾(详见「JVM 如何判断对象可回收」那道题)。标记算法标的就是这些「可达的存活对象」。例如对象图共有 1,000 个对象,从根只能走到 120 个,那么其余 880 个才是回收候选;互相引用但不连到根的环也属于候选。这里得到的是逻辑存活集合,空间何时重用还取决于后续清除、复制或整理阶段。
二、三种算法的取舍本质是「碎片 vs 空间 vs 速度」
把三者放一起对比,就能看清它们是在三个维度上做权衡:
| 算法 | 碎片 | 空间利用率 | 是否移动对象 | 适合 |
|---|---|---|---|---|
| 标记-清除 | 有碎片 | 较高 | 否 | 并发清扫、组合收集阶段 |
| 复制 | 无碎片 | 需预留目标空间 | 是 | 存活对象少的区域 |
| 标记-整理 | ✅ 无碎片 | ✅ 高 | 是 | 存活对象多的区域 |
没有一种算法全能:标记-清除省空间但有碎片,复制无碎片但费空间,标记-整理两者兼顾但慢。所以现代 JVM 不单用某一种,而是分代组合。
三、分代收集:为什么年轻代用复制、老年代用标整
这是这道题的落脚点。基于「大多数对象朝生夕灭」的假设,堆分年轻代和老年代,各用最适合的算法:
- 年轻代:对象绝大多数很快就死,存活的极少。复制算法只需复制这一小撮存活对象,成本极低,无碎片——完美契合。因此传统分代收集器常在年轻代采用复制。Eden 与两个 Survivor 的常见初始经验值是 8:1:1,但比例可配置或被自适应策略调整,不能把“只浪费 10%”当作固定结论。
- 老年代:对象存活率高、数量大。用复制算法要复制一大堆存活对象、还得留一半空间,太亏。所以老年代用标记-清除或标记-整理(如 CMS 常规周期以标记-清除为主,Serial Old / Parallel Old 采用标记-整理)。
这就是「复制算法配存活少的年轻代、标记整理配存活多的老年代」的道理——按存活率选算法。
四、算法和收集器的关系
要区分「算法」和「收集器」:算法是理论方法,收集器是具体实现。比如:
- Serial / ParNew / Parallel Scavenge(年轻代)→ 复制算法;
- Serial Old / Parallel Old(老年代)→ 标记-整理;
- CMS(老年代)→ 标记-清除(所以它有碎片问题);
- G1 → 整体看是标记-整理(Region 之间复制),避免碎片。
(收集器细节见「JVM 有哪几种垃圾回收器」那道题。)
五、并发标记为什么需要写屏障
并发收集器允许业务线程在标记期间继续改引用,单靠三色标记可能漏掉仍存活的对象:
白色:尚未发现
灰色:已发现,但子引用未扫描完
黑色:对象及其当时的子引用已扫描完
若黑对象删除对白对象的引用,同时灰对象又新增对白对象的引用却未被记录,收集器就可能错误回收白对象。实现会借助写屏障记录引用变化,并采用 SATB 或增量更新等策略维持正确性。
| 策略思路 | 重点记录 | 典型理解 |
|---|---|---|
| SATB | 并发标记开始时的旧引用关系 | 保存被覆盖的旧引用,近似处理“开始时存活” |
| 增量更新 | 黑对象新指向的白对象 | 重新关注被修改的黑对象或新引用 |
这些机制会产生额外 CPU 和内存开销,所以“并发 GC”不是免费地消除暂停,而是在暂停、吞吐和额外屏障成本之间交换。
六、怎样按存活率估算算法成本
假设某区域容量 1 GB,回收时只有 50 MB 存活:复制存活对象约处理 50 MB,并可一次获得连续空间;若有 900 MB 存活,复制成本就明显升高。
复制量近似 ∝ 存活对象大小
可回收量 = 区域已用量 - 存活量
标记-清除不移动存活对象,但空闲块可能零散;标记-整理需要移动对象和修正引用,却能得到连续空间。实际收集器通常组合多种算法,Region 化收集器还会选择部分区域做疏散,不能简单贴一个算法标签概括全部阶段。
选算法时真正要问的是对象存活率、是否允许碎片、可用额外空间、暂停目标以及并发修改成本。
七、常见误区与追问
- 误区:复制算法固定浪费一半堆空间。 半区复制只是经典模型,实际年轻代比例和 Region 疏散策略由收集器决定。
- 误区:标记-清除完成后一定能分配任意大小对象。 总空闲量足够时仍可能因碎片缺少连续空间。
- 误区:并发标记期间业务线程完全不受影响。 写屏障、并发线程抢占 CPU 和最终处理阶段都会带来成本。
- 追问:标记-整理和复制算法有什么共同点? 两者都会移动存活对象并更新引用,区别主要在目标空间组织和使用场景。
- 追问:为什么年轻代常用复制或疏散? 年轻代通常存活率低,复制少量存活对象能快速回收大片连续空间。
- 追问:CMS 为什么容易产生碎片? 它的老年代主要采用并发标记清除,不在常规清除阶段持续压缩所有存活对象。
- 追问:三色标记本身能解决并发修改吗? 不能,还需要写屏障和 SATB、增量更新等并发标记协议。
八、加强记忆
把三种基础算法记成“清会碎、复制看存活、整理换连续”:标记-清除少移动但留下碎片,复制只搬存活对象却需要目标空间,标记-整理用移动与引用修正换取连续内存。分代和 Region 化不是第四种孤立算法,而是依据存活率把这些方法组合到不同区域。并发标记还必须用写屏障配合 SATB 或增量更新,避免业务线程改引用造成漏标。答题时始终围绕存活率、额外空间、碎片、暂停和吞吐五个代价展开。