标记-清除算法导致内存碎片,因只标记死亡对象而不移动存活对象,使空闲空间零散分布;即使总空闲足够,大对象仍因缺乏连续空间而分配失败,最终触发oom。

因为它只“清空位置”,从不“挪动东西”。标记-清除算法在回收内存时,只是把死亡对象占的那块空间打上“空闲”标签,其他所有存活对象原地不动。久而久之,堆内存就像一块被虫蛀过的木板——空洞密布、大小不一、彼此隔离。
它不整理,也不合并,只做最轻量的释放
清除阶段不移动任何对象,也不调整地址布局。每次回收后,空闲空间被切割成零散小块,分布在存活对象之间。这些小块无法拼接,也无法自动合并。JVM分配器只能靠遍历空闲链表或位图去找合适大小的连续区域,效率随碎片增多而下降。
- 一个 new byte[1024*1024](1MB)对象,需要的是连续1MB空间,不是“总共加起来有1MB”
- 哪怕老年代还剩 300MB 空闲,但最大连续块只有 512KB,照样触发 OutOfMemoryError: Java heap space
- 日志里常看到 Old Gen 使用率仅 60%~70%,却频繁 Full GC,就是这个原因
老年代尤其容易“积碎成疾”
新生代用复制算法,每次 Minor GC 都会把存活对象集中搬到一边,旧区整块清空,天然防碎片。老年代没这个条件:对象多、体积大、晋升频繁、又没备用空间供复制,只能就地清理。CMS 收集器就采用纯标记-清除,长期运行后碎片像雪球一样越滚越大。
- CMS 在并发清除阶段若遇到大对象分配失败,会直接退化为 Serial Old(标记-整理),引发长停顿
- Parallel Old 默认 Full GC 就带整理,但整理本身耗时,可能反过来加剧延迟和分配压力
- 碎片还会拖慢后续分配速度——JVM 要花更多时间在空闲列表里“找块合适的地”
参数不当会加速碎片向老年代转移
问题不只出在算法本身,更藏在配置里。Survivor 区太小、晋升阈值太低、对象过早进入老年代,等于主动往老年代“倒碎玻璃”。
- -XX:SurvivorRatio=2 会让 Survivor 区极容易塞满,大量本该短命的对象提前 tenuring 到老年代
- -XX:MaxTenuringThreshold 设得太小(如 1),对象活过一次 Minor GC 就进老年代,大幅提升老年代碎片风险
- 缓存类服务若长期持有中生命周期对象(活几轮 GC 但未晋升),Survivor 区反复复制反而低效,此时更需考虑 G1 或调高晋升阈值
它不是错,而是取舍
标记-清除胜在实现简单、STW 时间相对可控、适合高吞吐场景。但它把“空间利用率”让渡给了“暂停时间”。一旦碎片影响到大对象分配或触发兜底整理,代价就从“快”变成“卡”甚至“崩”。所以现代收集器往往组合使用:CMS 主打并发清除,靠 fallback 整理兜底;G1 在 Mixed GC 中局部整理;ZGC/Shenandoah 则绕开移动,用读屏障解决碎片——路径不同,目标一致:既要低延迟,也要能持续分配。










