深蓝黑背景品牌封面:左侧大标题「四种基础 GC 算法」,副标题「标记-清除、复制、标记-整理、分代:各付什么代价」;右侧原创装饰——四个小方块排成一列,各自用一个图形暗喻:第一块里有几个空洞(碎片)、第二块被竖线劈成两半(空间减半)、第三块里的方块被压实到一侧(移动整理)、第四块分成上下两层(分代);左上标注「经典题解析 · Java 后端」,右上标注「Day 04 · 10-12」 图 1:本文封面,四个方块的暗喻对应四种算法各自的代价。

先说明属性:这是一篇经典题解析,题目出自本地历史面试资料,与任何公司的当前招聘无关。「说说垃圾回收算法」是 JVM 面试的保留曲目,平庸的答案是背四个名字,加分的答案是讲清每种算法用什么东西换了什么东西。

想象面试现场:「垃圾回收有哪些基础算法?各自的实现原理和优缺点是什么?」

第一步,先立共同前提:怎么判断对象死了。

四种算法共享同一个地基:可达性分析。从一组根对象(栈上的局部变量、静态引用等)出发沿引用链遍历,走不到的对象就是垃圾。差异不在「找垃圾」,而在「找到之后怎么处理」——处理方式的四种选择,就是四种算法。

第二步,标记-清除:最简单,代价是碎片。

做法分两步:标记阶段从根出发给所有活对象打上标记,清除阶段把没标记的直接回收。优点是简单、对象原地不动;代价藏在清除之后——活对象之间留下一个个空洞,内存变成筛子。空洞总量也许很大,但没有一个洞装得下新的大对象,于是「总空闲充足却分配失败」。碎片是它甩不掉的代价。

第三步,复制:分配极快,代价是空间减半。

把可用内存平分成两半,平时只用其中一半;回收时把这半里的活对象整体搬到另一半,然后这半整体清空。优点是双份的:没有碎片——搬过去的对象紧挨着排;分配极快——空闲区永远连续,新对象挪动指针就完成分配。代价同样醒目:任何时刻都有一半内存在闲置,可用空间永远打五折。它赌的是「活对象很少」,赌赢了搬运成本极低,赌输了(存活率高)就要搬大量数据。

第四步,标记-整理:不浪费空间,代价是移动与停顿。

标记阶段与标记-清除相同,之后多一步整理:把所有活对象向内存的一端压实,再一次性清掉边界之外的区域。碎片没了,空间也百分百可用,两全其美——但代价换成了新的形态:移动对象意味着更新所有指向它们的引用,整个过程的停顿明显更重。它适合对象存活率高、值得花时间整理的场合。

第五步,分代收集:不是第五种算法,是组合拳。

分代收集的依据是官方虚拟机调优文档里的弱分代假说:绝大多数对象朝生暮死。既然不同年龄的对象死亡概率悬殊,就没必要用同一套算法伺候:新生代放刚出生的对象,死得快、存活率低,用复制——正好发挥它「活对象越少越赚」的特性;熬过几轮回收的幸存者晋升老年代,这里存活率高、复制不划算,用标记-清除或标记-整理。新生代满了触发只收新生代的 minor collection,老年代满了才触发收全堆的 major collection,后者因为涉及对象多,停顿长得多。分代不是新算法,而是把前三种算法按对象年龄错开部署的策略。

四列对比图:标记-清除——从根出发标记活对象,其余直接清除,优点是简单不动对象,代价是留下内存碎片,大对象可能无连续空间可用;复制——把活对象整体搬到另一半空间,优点是分配极快无碎片,代价是可用的内存永远只有一半;标记-整理——标记后把活对象向一端压实再清除,优点是无碎片也不浪费空间,代价是移动对象、更新引用,停顿更重;分代收集——按对象年龄分区,新生代朝生暮死用复制,老年代存活久用标记-整理,用组合拳把三种算法的代价错开。底部是取舍坐标系:回收效率、空间利用率、停顿成本三者不可兼得,所有收集器都是在这三个轴上选位置 图 2:四种算法的原理、代价与取舍坐标系。

把四种算法放进同一个坐标系。

回收效率、空间利用率、停顿成本,三者不可兼得:标记-清除省停顿但牺牲空间连续性;复制用一半空间换效率;标记-整理用停顿换两全;分代用分区设计把三种代价错开摊薄。市面上所有具体收集器,都是在这个坐标系里选位置——看懂坐标系,再看任何收集器都只是「它选了哪几个点」。

可能的追问与易错点。

「为什么新生代敢用复制?」因为弱分代假说:新生代每次回收后活对象极少,搬运成本极低,空间浪费被高频率的回收摊薄。「标记-清除的碎片怎么救?」要么退化成定期整理,要么靠分配策略绕开,但根本上是它被替代的原因。易错点主要有三:一是把四种算法背成四个名字,讲不出各自的代价形态;二是把分代当成与三者并列的第五种算法;三是以为碎片是「实现没写好」,而不是算法选择本身的代价。

回到面试现场,建议的答题节奏:先立可达性分析的共同地基,再按「原理一句话、代价一句话」的节奏讲完前三种,然后抬出弱分代假说解释分代为什么是组合拳,最后落到三轴坐标系收尾。「没有银弹,只有取舍」是这套算法谱系教给工程师的核心思维——推理服务的延迟抖动治理、缓存分层设计,复用的是同一套权衡框架。

你被 GC 停顿在线上咬过吗?最后是靠换算法、调分区还是改代码解决的?欢迎分享。