标记-清除算法必然导致内存碎片化,因其不移动存活对象,仅标记并释放零散空间,无法拼出连续大块内存,故即使总空闲足够仍会因最大连续块不足而OOM。

标记-清除算法的内存碎片化问题,本质不是“内存不够”,而是“空闲散装、拼不出整块”。它不移动对象,只打标+释放,久而久之堆内存就像被蛀空的奶酪——空洞密布、大小不一、彼此隔离。
为什么一用就碎:算法机制决定碎片必然产生
标记-清除分两步:先从 GC Roots 出发标记所有存活对象;再遍历堆,把未标记的对象所占空间直接标为“空闲”。关键在于——它从不挪动任何一个存活对象。对象原地不动,释放的只是零散的“缝隙”,这些缝隙无法自动合并,也无法拼接成大块连续空间。
- 一个
new byte[1024*1024](1MB)对象,需要的是连续1MB,不是“总共加起来有1MB” - 哪怕老年代还剩 300MB 空闲,但最大连续块只有 512KB,照样抛
OutOfMemoryError: Java heap space - 日志里常见 Old Gen 使用率仅 60%~70%,却频繁触发 Full GC,正是碎片在“卡脖子”
老年代最容易“积碎成疾”
新生代靠复制算法(Eden+S0+S1,8:1:1)天然防碎片:每次 Minor GC 把存活对象集中搬走,旧区整块清空。老年代没这个条件——对象多、体积大、晋升频繁、又没备用空间供复制,只能就地清理。
- CMS 收集器就是纯标记-清除,长期运行后碎片像雪球一样越滚越大
- CMS 并发清除阶段若遇到大对象分配失败,会直接退化为 Serial Old(标记-整理),引发长停顿
- Parallel Old 默认 Full GC 带整理,但整理本身耗时,可能反过来加剧延迟
参数不当=主动往老年代倒碎玻璃
碎片问题不只出在算法,更藏在配置里。错误调参会加速中等寿命对象涌入老年代,大幅抬高碎片风险:
你接手一个需求时,是不是经常这样:"改一个小字段 → 结果把不相关的代码也改了"、"需求说得很模糊 → 做着做着发现跑偏了"、"数据库加个字段 → 上线才发现漏了数据兼容"、"做了个新功能 → 一个月后自己都忘了当时怎么设计的"、"写到一半被中断 → 再回来不知道从哪继续";这个 Skill 就是专门解决这些问题的。 它会自动探测你的项目环境(JDK、Spring Boot、ORM、数据库、前端框架),记在
立即学习“Java免费学习笔记(深入)”;
-
-XX:SurvivorRatio=2:Survivor 区太小,极易塞满,大量本该短命的对象提前 tenuring 到老年代 -
-XX:MaxTenuringThreshold=1:对象活过一次 Minor GC 就进老年代,等于把碎片源头直接搬进最难收拾的区域 - 缓存类服务若长期持有中生命周期对象(活几轮但未晋升),Survivor 区反复复制反而低效,此时更需考虑 G1 或调高晋升阈值
它不是错,是明确的取舍
标记-清除胜在实现简单、STW 时间相对可控、适合高吞吐场景。但它把“空间利用率”让渡给了“暂停时间”。一旦碎片影响到大对象分配或触发兜底整理(如 CMS fallback),代价就从“快”变成“卡”甚至“崩”。
现代 JVM(HotSpot 8u292+ / 11+ / 17+)已无默认启用纯 Mark-Sweep 的收集器,但它仍是 CMS、G1 等回收器内部环节的基础逻辑——理解它的碎片代价,才能避开调优陷阱。

















