在 JVM 的内存管理体系中,垃圾收集(GC)算法就是 "回收兵法"------ 不同算法有不同的 "战术特点",有的追求效率,有的追求无碎片,有的兼顾两者。我早年做电商库存系统时,因对算法选型一知半解,盲目使用标记 - 清除算法,导致老年代内存碎片堆积,大订单对象分配失败,触发频繁 Full GC;后来改用标记 - 整理算法,碎片问题彻底解决,系统稳定性提升一个量级。垃圾收集算法没有 "最优",只有 "最适配"------ 新生代的复制算法、老年代的标记 - 清除 / 整理算法,以及整合它们的分代收集策略,都是 JVM 基于 "对象朝生夕死" 规律的精准选择。读懂这些算法,你就能明白 GC 的底层逻辑,也能为不同业务场景选择合适的收集器。

一、标记 - 清除算法
标记 - 清除(Mark-Sweep)是最基础、最古老的垃圾收集算法,它的核心逻辑简单到 "两步走",却也是新手最易踩坑的算法。
1. 核心执行流程
标记 - 清除算法分为两个阶段,就像清洁工 "先标记垃圾,再清理垃圾":
- 标记阶段:从 GC Roots 出发,遍历所有对象,标记出可达的存活对象;
- 清除阶段:遍历整个堆内存,清除所有未被标记的垃圾对象,释放内存空间。
这个算法的优点是实现简单------ 无需移动对象,只需标记和清除,早期 JVM 都采用这种算法。但它的两个致命缺点,直接决定了它只能 "屈居" 老年代:
- 效率低:标记和清除都需要遍历整个堆,若堆内存大、对象多,两次遍历的耗时会很长,导致 STW(Stop The World)时间增加;
- 产生内存碎片:清除垃圾对象后,释放的内存是零散的 "碎片"------ 比如堆中有 100M 空闲内存,但由多个 10M、20M 的碎片组成,此时要分配一个 50M 的大对象,明明总空闲内存足够,却因没有连续空间而分配失败,最终触发 Full GC。
2. 实战踩坑
我早年维护的电商库存系统,使用基于标记 - 清除的 CMS 收集器,运行一段时间后出现诡异现象:堆内存使用率仅 60%,但创建大订单对象(约 30M)时,频繁抛出OutOfMemoryError: Java heap space。
通过jmap -heap <pid>分析堆内存,发现老年代有大量零散的内存碎片,最大的连续空闲空间只有 25M------ 这就是标记 - 清除算法的 "后遗症"。解决方法是:改用 G1 收集器(老年代用标记 - 整理算法),碎片问题立刻消失,大对象分配正常。
3. 适用场景
标记 - 清除算法适合对象存活率高的区域 ------ 老年代的对象存活时间长,每次 GC 只回收少量垃圾,标记阶段的耗时远小于清除阶段,效率相对可接受。但为了避免碎片,通常会和标记 - 整理算法结合使用(比如 CMS 收集器的 "并发标记 - 清除"+"串行标记 - 整理",定期整理碎片)。
二、复制算法
复制(Copying)算法是为解决标记 - 清除的痛点而生,它以 "牺牲部分内存" 为代价,换来了高效和无碎片的优势,也是新生代 GC 的 "标配算法"。
1. 核心执行流程
复制算法的核心思路是 "空间换时间",它将堆内存分为大小相等的两块区域,每次只使用其中一块:
- 标记阶段:从 GC Roots 出发,标记出当前区域的存活对象;
- 复制阶段:将所有存活对象复制到另一块未使用的区域,按顺序排列,保证内存连续;
- 清空阶段:清空当前区域的所有对象,完成一次 GC;
- 交换阶段:两块区域的角色互换,下一次 GC 时使用另一块区域。
这个算法的优点非常明显:
- 效率高:无需遍历整个堆清除垃圾,只需复制存活对象,而新生代的对象存活率极低(90% 以上的对象都是 "朝生夕死"),复制的开销很小;
- 无内存碎片:存活对象按顺序复制到新区域,内存是连续的,大对象分配毫无压力。
但它的缺点也很突出:内存利用率低------ 只有 50% 的内存能被使用,另一块区域始终闲置,这对内存资源是极大的浪费。
2. 新生代的优化
为了解决内存利用率低的问题,JVM 对复制算法做了 "改良"------ 将新生代分为一个 Eden 区和两个大小相等的 Survivor 区(S0、S1) ,默认比例为Eden:S0:S1=8:1:1。
改良后的执行流程(Minor GC 的核心逻辑):
- 新对象优先分配到 Eden 区,S0 和 S1 初始为空;
- Eden 区满触发 Minor GC,标记 Eden 区的存活对象,复制到空闲的 S0 区;
- 清空 Eden 区,完成一次 GC;
- 下次 Minor GC 时,标记 Eden+S0 区的存活对象,复制到空闲的 S1 区;
- 清空 Eden+S0 区,交换 S0 和 S1 的角色(S0 变空闲,S1 变使用中)。
这种设计的核心优势是:内存利用率提升至 90%(8+1 的可用区域,1 的闲置区域),既保留了复制算法高效无碎片的优点,又解决了内存浪费的问题。
3. 实战调优
新生代的SurvivorRatio参数(默认 8)决定了 Eden 和 Survivor 的比例,这个参数直接影响对象 "晋升" 到老年代的频率 ------ 调大 SurvivorRatio,Eden 区变大,Minor GC 频率降低;调小 SurvivorRatio,Eden 区变小,Minor GC 频率升高。
我曾将电商系统的SurvivorRatio从 8 调整为 10(Eden:S0:S1=10:1:1),Eden 区容量增加,Minor GC 频率从每分钟 1 次降到每 3 分钟 1 次,接口响应时间减少 20%。但要注意:Survivor 区不能太小,否则存活对象无法容纳,会直接进入老年代,导致老年代内存占用飙升,触发 Major GC。
4. 适用场景
复制算法是新生代的 "专属算法"------ 新生代对象存活率低,复制开销小,90% 的内存利用率完全可接受。几乎所有新生代收集器(Parallel Scavenge、G1 的新生代收集)都采用复制算法。
三、标记 - 整理算法
标记 - 整理(Mark-Compact)算法是标记 - 清除的 "升级版",它解决了标记 - 清除的碎片问题,也避免了复制算法的内存浪费,是老年代 GC 的 "终极方案"。
1. 核心执行流程
标记 - 整理算法分为三个阶段,兼顾了标记 - 清除的低内存消耗和复制算法的无碎片优势:
- 标记阶段:和标记 - 清除一样,从 GC Roots 出发,标记存活对象;
- 整理阶段 :将所有存活对象移动到堆内存的一端,按顺序排列,保证内存连续;
- 清除阶段:清除堆内存另一端的所有垃圾对象,释放内存空间。
这个算法的优点是:
- 无内存碎片:存活对象连续排列,大对象分配无忧;
- 内存利用率高:无需额外的闲置区域,100% 的堆内存都能被使用。
缺点是:效率中等------ 整理阶段需要移动存活对象,而老年代的对象存活率高,移动开销大,导致 STW 时间比标记 - 清除长。
2. 实战价值
标记 - 整理算法是老年代的 "标配算法"------G1 收集器的老年代收集、ZGC 的整理阶段,都采用标记 - 整理算法。我曾将电商系统的 CMS 收集器换成 G1 收集器,老年代的内存碎片问题彻底解决,Full GC 频率从每小时 1 次降到每天 1 次,STW 时间从 500ms 降到 50ms 以内。
3. 适用场景
标记 - 整理算法适合对象存活率高的区域 ------ 老年代的对象存活时间长,整理阶段的移动开销虽然大,但一次整理能保证长时间的无碎片内存,整体性价比更高。
四、分代收集
分代收集不是一种独立的算法,而是结合复制、标记 - 清除、标记 - 整理三种算法的 "组合策略" ------ 它的核心设计逻辑是:基于对象的生命周期,将堆分为新生代和老年代,为不同代选择最适配的收集算法。
1. 分代收集的核心依据
JVM 通过大量实践发现:Java 程序中的对象具有 "朝生夕死" 的特点 ------90% 以上的对象在创建后很快就会变成垃圾,只有少数对象能存活较长时间。
基于这个规律,分代收集的策略如下:
| 内存区域 | 对象特点 | 适配算法 | 核心优势 | 对应 GC 类型 |
|---|---|---|---|---|
| 新生代 | 存活率低、生命周期短 | 复制算法 | 高效无碎片 | Minor GC(年轻代 GC) |
| 老年代 | 存活率高、生命周期长 | 标记 - 清除 / 标记 - 整理 | 低内存消耗、无碎片 | Major GC/Full GC(老年代 GC) |
2. 分代收集的执行流程
分代收集的完整执行流程,就像 "清洁工分区域打扫":
- 新生代 Minor GC:Eden 区满触发,采用复制算法,将存活对象复制到 Survivor 区,清空 Eden 区;
- 对象晋升老年代 :存活对象在 Survivor 区经历多次 Minor GC 后,若仍存活(默认 15 次,可通过
-XX:MaxTenuringThreshold调整),则晋升到老年代; - 老年代 Major GC:老年代内存占用达到阈值触发,采用标记 - 清除 / 标记 - 整理算法,回收老年代的垃圾对象;
- Full GC:新生代 + 老年代的全堆 GC,通常是 Major GC 的 "升级版",STW 时间最长,应尽量避免。
3. 实战调优
分代收集的调优核心是 "调整代际比例",让内存分配贴合业务的对象生命周期:
-XX:NewRatio:新生代和老年代的比例(默认 2,即新生代:老年代 = 1:2);- 电商、支付等短生命周期对象多的场景:调小
NewRatio(如设为 1,新生代:老年代 = 1:1),增加新生代容量,减少 Minor GC 频率; - 大数据、缓存等长生命周期对象多的场景:调大
NewRatio(如设为 3,新生代:老年代 = 1:3),增加老年代容量,减少 Major GC 频率。
- 电商、支付等短生命周期对象多的场景:调小
-XX:SurvivorRatio:Eden 和 Survivor 的比例(默认 8,即 Eden:S0:S1=8:1:1);- 大对象多的场景:调小
SurvivorRatio(如设为 4,Eden:S0:S1=4:1:1),增加 Survivor 区容量,避免对象直接晋升老年代。
- 大对象多的场景:调小
五、算法选型的适配原则
总结出垃圾收集算法的选型原则:没有最好的算法,只有最适配的场景。
- 看对象存活率:存活率低选复制算法,存活率高选标记 - 整理算法;
- 看内存利用率:内存紧张选标记 - 清除 / 标记 - 整理,内存充足选复制算法;
- 看业务需求:低延迟场景选 G1(混合算法),高吞吐量场景选 Parallel Scavenge+Parallel Old(复制 + 标记 - 整理)。
最后小结
核心回顾
- 三大基础 GC 算法:标记 - 清除(简单但有碎片)、复制(高效无碎片但浪费内存)、标记 - 整理(无碎片但效率中等);
- 分代收集是 "组合策略":新生代用复制算法(Minor GC),老年代用标记 - 清除 / 整理算法(Major GC),基于对象 "朝生夕死" 的规律;
- 实战调优核心:调整代际比例(
NewRatio、SurvivorRatio),让算法适配业务场景。