标记-清除算法导致oom的根本原因是内存碎片化:回收后产生大量不连续小空闲块,无法满足大对象的连续内存分配需求,即使总空闲空间充足。

标记-清除算法为什么会导致 OOM,即使堆内存还很空?
根本原因不是内存不够,而是“够不着”——Mark-Sweep 回收后留下大量不连续的小块空闲内存,当新对象(比如一个 byte[1024*1024] 数组)需要分配连续空间时,哪怕总空闲量远超 1MB,也因找不到足够大的连续块而触发 OutOfMemoryError。
- 典型现象:
java.lang.OutOfMemoryError: Java heap space频发,但jstat -gc显示老年代使用率仅 60%~70% - 触发条件:频繁创建/销毁中等大小对象(如缓存
HashMap、短生命周期 DTO),尤其在老年代长期运行后 - 关键指标:用
jmap -histo查看对象分布 +jstat -gccapacity观察YGC/FGC后的CCSU(压缩空间使用率)是否持续走低
复制算法真能彻底解决碎片?它在哪儿被实际用到?
能,但代价是“砍半”——Copying 算法靠把存活对象集中拷贝到另一半空间来消除碎片,所以它只敢用在天然适合“高死亡率+小容量”的区域。
- 真实场景:JVM 年轻代的
Survivor区(S0/S1)就是标准复制算法落地;每次 Minor GC 把 Eden 和一个 Survivor 中的存活对象复制到另一个 Survivor - 不能乱搬:老年代不用复制算法,因为对象存活率高(>95%),复制成本爆炸;强行用会导致 STW 时间飙升,响应延迟不可控
- 参数影响:
-XX:SurvivorRatio=8控制 Eden:Survivor 比例,比值过大会让 Survivor 容易溢出,触发tenuring threshold提前晋升,加剧老年代碎片
标记-整理算法是不是“万能解药”?它卡在哪?
它确实不产生碎片,但“整理”本质是移动所有存活对象——这要求整个堆暂停写操作,且移动过程本身耗时,所以它只在“必须保全空间利用率”的场合才启用。
- 典型实现:CMS 的 fallback 机制、Serial Old、Parallel Old 默认使用
Mark-Compact;G1 在 Mixed GC 阶段也会局部整理 - 性能陷阱:对象越多、存活率越高,整理耗时越长;若老年代有 2GB 存活对象,一次 Full GC 可能 STW 超过 1 秒
- 容易误配:
-XX:+UseParallelOldGC开启后,Parallel Old 默认 Compact,但若应用对延迟敏感(如金融交易),反而应换用 G1 或 ZGC
现在还有谁在用纯标记-清除?别踩这个坑
现代生产 JVM(HotSpot 8u292+ / 11+ / 17+)已无默认启用纯 Mark-Sweep 的收集器;但某些定制化或嵌入式 JVM(如 J9 的早期版本、部分 IoT Java 运行时)仍保留该选项,极易被误选。
立即学习“Java免费学习笔记(深入)”;
- 危险信号:启动参数含
-XX:+UseSerialGC且未显式指定-XX:+UseSerialOldGC(旧版默认 Serial Old 是 Mark-Sweep);或日志中出现[DefNew: ... [Tenured: ...]且 Tenured 区 GC 类型为PSMarkSweep - 替代方案:哪怕最小资源环境,也优先用
-XX:+UseG1GC(JDK9+ 默认)或-XX:+UseZGC(JDK15+),它们底层都规避了纯 Mark-Sweep - 检查方法:加
-Xlog:gc*:file=gc.log:time,观察 GC 日志中老年代回收器名称,确认不含MarkSweep字样
碎片问题从来不是孤立存在的——它和对象生命周期分布、晋升阈值、GC 停顿容忍度强耦合。调优时盯着 Fragmentation 数值没用,真正要盯的是每次 GC 后的 free space contiguity 和后续大对象分配失败率。










