标记-清除算法因内存碎片化导致频繁Full GC;复制算法实际浪费约10%内存而非50%;标记-整理适用于高存活率场景,分代收集是策略框架而非独立算法。

Java里常见的垃圾回收算法就四种:标记-清除、复制、标记-整理、分代收集——其中分代收集不是独立算法,而是策略框架,底层仍靠前三种实现。
标记-清除算法为什么会导致频繁的 Full GC?
它只做两件事:标记所有从 GC Roots 可达的对象,再 清除 所有未标记对象。问题出在“清除”之后:内存块被零散释放,形成大量不连续空洞。
- 当新对象(比如一个大数组)需要分配时,JVM 找不到足够大的连续空间,就会触发
Full GC来强行整理——哪怕老年代其实还有很多空闲字节 - 典型现象:
ParNew回收后Old Gen使用率缓慢爬升,但没明显对象晋升,却突然卡顿数秒,日志里出现Full GC (Ergonomics) - HotSpot 中,
Serial Old和早期CMS的初始标记/清除阶段就用这个逻辑,所以 CMS 在并发失败(concurrent mode failure)时会退化成 Serial Old,直接卡死
复制算法真只浪费 50% 内存?别被教科书骗了
教科书说“把内存一分为二”,但 HotSpot 实际用的是 Eden:S0:S1 = 8:1:1 比例,也就是年轻代总共 10 份,只预留 1 份作“备份区”。
- 每次
Minor GC把Eden+ 当前Survivor(比如 S0)中存活对象,复制到另一个空Survivor(S1),然后清空 Eden+S0 - 真正浪费的是那 10% 的 Survivor 空间——前提是对象能活过一次 GC;如果 Eden 区 98% 对象都死掉,复制成本极低
- 陷阱:把
-XX:SurvivorRatio调得太小(比如设成 2),会导致 Survivor 不够用,大量对象提前晋升到老年代,反而加剧老年代压力
标记-整理 vs 复制:什么时候该选哪个?
关键看对象存活率。复制算法怕“活得多”,标记-整理怕“动得慢”。
立即学习“Java免费学习笔记(深入)”;
- 年轻代用复制:因为
98% 对象朝生夕死,复制少量存活对象比移动全部对象快得多 - 老年代不用复制:假设一次 GC 后 70% 对象还活着,复制就得搬走 70% 数据,指针重定向开销巨大,STW 时间不可控
- 所以
Serial Old、Parallel Old、G1的混合回收(mixed GC)后期都会用标记-整理逻辑——把存活对象往一端压,腾出大片连续空间 - 注意:
G1名义上是“分区复制”,但它对跨 Region 引用的处理、以及 Evacuation 失败后的退化行为,本质上仍是标记-整理思路的变体
分代收集不是算法,是 JVM 的默认生存规则
它不解决“怎么回收”,而是决定“在哪回收、何时回收、用什么算法回收”。你改不了它,但必须理解它的默认契约:
- 新对象一律进
Eden;经历一次 Minor GC 还活着,就进Survivor;熬过-XX:MaxTenuringThreshold(默认 15)次,或 Survivor 放不下,就晋升到Old Gen - 老年代满了才触发
Full GC(CMS 除外);但年轻代 GC 频繁且老年代增长快,往往意味着有隐性内存泄漏(比如缓存没设上限、监听器未注销) - 别盲目调大
-Xmn:年轻代太大 → Minor GC 停顿变长;太小 → 晋升加速 → 老年代更快填满 → 更多 Full GC
真正难的从来不是记住算法名字,而是看懂 GC 日志里那一行 PSYoungGen: 123456K->7890K(131072K) 背后,到底是对象在合理流转,还是正在悄悄滑向 OOM 边缘。










