JVM 垃圾回收算法有哪些?
结论先行:垃圾回收只有三种基础算法——标记-清除(mark-sweep)、复制(copying)、标记-整理(mark-compact),其余都是它们的组合与并发化。HotSpot 在此基础上用分代收集(generational collection)把堆切成新生代与老年代,分别套用最合适的算法;并发收集器再叠加三色标记 + 写屏障,把标记过程摊到应用运行期间。
一、三种基础算法
| 算法 | 步骤 | 优点 | 缺点 | 典型使用者 |
|---|---|---|---|---|
| 标记-清除 | 标记存活 → 清除垃圾 | 实现简单,不需要移动对象 | 产生内存碎片,标记与清除效率随堆增大而下降 | CMS |
| 复制 | 把存活对象复制到另一半空间 | 无碎片,分配只需移动指针 | 空间利用率低,存活率高时复制成本大 | 新生代(Serial/ParNew/Parallel Scavenge) |
| 标记-整理 | 标记存活 → 把存活对象向一端移动 | 无碎片,空间利用率高 | 移动对象要更新所有引用,停顿长 | Serial Old、Parallel Old |
标记-清除:
[A][ ][B][ ][ ][C] → [A][ ][B][ ][ ][C] 空隙留在原地,大对象可能分配失败
复制(Eden + 两个 Survivor):
[A][B][C][ 垃圾 ][ ] → 存活对象整体搬到 to 区,from 区整块清空
标记-整理:
[A][ ][B][ ][C][ ] → [A][B][C][ ][ ][ ] 存活对象左移,尾部整块回收
二、分代收集:按对象寿命分区
| 区域 | 特点 | 采用的算法 | 原因 |
|---|---|---|---|
| 新生代 | 朝生夕死,存活率低于 10% | 复制算法 | 存活少,复制成本低;分代假说成立 |
| 老年代 | 存活率高,对象大 | 标记-清除或标记-整理 | 存活率高时复制不划算,需要碎片控制 |
跨代引用用**卡表(card table)记录,老年代对象指向新生代的引用被标记为脏卡,Young GC 时只扫脏卡而不必遍历整个老年代,这就是记忆集(remembered set)**的用途。
三、并发标记的两种屏障
| 方案 | 屏障类型 | 记录内容 | 使用者 |
|---|---|---|---|
| 增量更新 | 写后屏障 | 黑色对象新增的引用 | CMS |
| 原始快照 SATB | 写前屏障 | 被删除的旧引用 | G1、Shenandoah |
两者的差别在于「重新标记阶段要重新扫描谁」:增量更新扫新增引用,SATB 扫删除引用所指向的对象。SATB 的额外收益是新对象一律视为存活,直接分配到标记结束。
四、用参数验证算法差异
# 新生代复制算法:Eden 与 Survivor 的比例、晋升年龄阈值
java -XX:+UseSerialGC -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15 -jar app.jar
# 老年代标记-整理:Serial Old / Parallel Old
java -XX:+UseParallelGC -XX:+UseParallelOldGC -jar app.jar
# 标记-清除 + 定期整理:CMS 的碎片治理
java -XX:+UseConcMarkSweepGC \
-XX:CMSFullGCsBeforeCompaction=5 \
-XX:+UseCMSCompactAtFullCollection \
-jar app.jar
CMSFullGCsBeforeCompaction=5 表示执行 5 次不压缩的 Full GC 后做一次带整理的 Full GC,用停顿换碎片空间。
五、常见追问
| 追问 | 回答要点 |
|---|---|
| 为什么新生代用复制算法 | 存活率低,复制量小;代价是浪费一个 Survivor 空间,但整体收益更高 |
| 复制算法一定要两倍空间吗 | 不需要,HotSpot 用 Eden + 两个 Survivor(默认 8:1:1),只浪费 10% |
| 标记-清除的碎片怎么解决 | 定期做标记-整理,或换用 G1 这类按 Region 回收、自带整理的收集器 |
| 三色标记属于哪种算法 | 它是标记阶段的实现手段,服务于标记-清除/整理,不是独立的回收算法 |
| 分代收集会被淘汰吗 | 会淡化。ZGC、Shenandoah 不分代(ZGC 在 JDK 21 引入分代),说明分代只是当前性价比最高的选择 |
六、并发带来的两个副产品
| 现象 | 产生原因 | 影响 |
|---|---|---|
| 浮动垃圾(floating garbage) | 标记完成后、清除进行时,用户线程又产生了新垃圾 | 本轮收不掉,只能留到下一轮,所以收集器要预留空间 |
| 并发模式失败(concurrent mode failure) | 预留空间被耗尽,应用分配速率超过回收速率 | 收集器退化为单线程 Full GC,停顿从毫秒级变成秒级 |
对策是给收集器留出余量:CMS 用 -XX:CMSInitiatingOccupancyFraction=70 提前启动,G1 用 -XX:G1ReservePercent=10 预留 Region。余量越大越安全,但可用堆越小,这本身就是吞吐与停顿的权衡。
小结
三种算法各有取舍:清除省空间但留碎片,复制快但费空间,整理干净但停顿长。分代是策略层面的组合,三色标记与写屏障是并发化的手段,答题时把这几层分清就不会乱。