标记-清除算法因只标记释放而不移动对象,导致堆内存碎片化;典型表现为full gc频繁但老年代使用率低、大对象分配失败却内存充足。

标记-清除算法为什么一用就出内存碎片?
因为它的“清除”只是把对象占用的内存块打上空闲标记,不挪动任何存活对象。结果就是:堆里全是散落的小空洞,像被老鼠啃过的奶酪。
典型现象:Full GC (Ergonomics) 频繁触发,但老年代实际使用率并不高;日志里 Old Gen 使用率缓慢爬升,却突然卡顿数秒;分配一个 new byte[1024*1024] 就直接 OutOfMemoryError: Java heap space——不是没内存,是没“连续”内存。
- 它不整理、不压缩、不移动,只标记+释放地址段
- 碎片积累到一定程度,JVM 无法满足大对象的连续分配需求,只能被迫启动 Full GC 来整理(比如 Serial Old 或 CMS fallback)
- CMS 垃圾收集器在并发失败时退化成 Serial Old,本质就是靠标记-清除 + 标记-整理兜底,这时候 STW 时间会明显拉长
复制算法真能彻底避免碎片?别信“一半内存”的老说法
HotSpot 实际用的是 Eden:S0:S1 = 8:1:1,不是教科书里“对半分”。真正浪费的不是 50%,而是那 10% 的 Survivor 空间——而且仅当对象活过一次 GC 才会占用它。
问题出在调参上:-XX:SurvivorRatio 设太小(比如 2),Survivor 区瞬间塞满,大量本该短命的对象提前晋升到老年代,反而加剧老年代的碎片压力。
立即学习“Java免费学习笔记(深入)”;
- 年轻代用复制算法,是因为
98%对象朝生夕死,复制成本极低 - 如果误调
-XX:MaxTenuringThreshold过低,或让对象频繁跨代晋升,等于主动往老年代“撒碎玻璃” - 注意:G1 的
Evacuation看似是复制,但跨 Region 引用处理失败时会退化为标记-整理,本质仍是防碎片的兜底逻辑
什么时候必须上标记-整理?看存活率,不是看年龄
老年代默认用标记-整理,不是因为它“老”,而是因为一次 GC 后通常有 70% 以上对象还活着——复制这 70% 的数据,指针重定向开销太大,STW 时间不可控。
但别以为只有老年代才需要它。比如长期运行的缓存服务,如果用了弱引用+大量中生命周期对象(活几轮 Minor GC 但又没到晋升阈值),Survivor 区反复复制效率反降,这时更应考虑调大 -XX:MaxTenuringThreshold 或启用 -XX:+UseG1GC 让混合回收自动调度整理逻辑。
- 标记-整理核心代价是“移动所有存活对象”,所以适合高存活率 + 低分配频率场景
-
Serial Old、Parallel Old都用它;CMS 不用,所以怕碎片;ZGC/Shenandoah 用读屏障+着色指针绕过移动,属于另一条技术路径 - 别指望靠加大堆来掩盖碎片——更大的堆只会让碎片更隐蔽、Full GC 更致命
怎么验证你是不是被碎片坑了?看 GC 日志里的关键信号
光看 GC logs 里有没有 Full GC 不够,得盯住三件事:PSYoungGen 回收后 ParOldGen 使用率是否异常跳升、available 字段是否持续缩小、以及每次 Allocation Failure 是否伴随大对象(>1MB)分配失败。
一个快速验证方式:加 JVM 参数 -XX:+PrintGCDetails -XX:+PrintGCTimeStamps,然后观察日志中类似这样的行:
ParNew: 123456K->1234K(2097152K), 0.0234567 secs
如果箭头右边数字(即幸存对象大小)长期稳定在几千 KB,但 ParOldGen 使用量每月涨 5%,基本可以确定是碎片导致对象提前晋升。
- 别只依赖
jstat -gc看百分比——它不反映连续性 -
jmap -histo可查大对象分布,但无法定位碎片位置;真正诊断得用jhsdb jmap --heap或 GCEasy 工具解析日志 - 最易被忽略的一点:CMS 的
concurrent mode failure日志里不会写“碎片”,但根本原因十有八九就是它










