标记-清除算法先标记可达对象再回收未标记对象内存,不移动对象导致外部碎片;现代JVM已不裸用该算法,而是通过分代隔离、G1/ZGC等组合策略治理碎片。

标记-清除算法分两步走:先从 GC Roots(比如栈帧变量、静态字段、JNI 引用)出发,遍历并标记所有可达对象;再扫描整个堆,把没被标记的对象所占内存直接归还给空闲列表。它不移动对象,只做逻辑回收,因此实现简单、不需额外空间,但代价是留下大量离散的小块空闲内存——也就是外部碎片。
为什么会产生内存碎片
清除操作只是释放死亡对象的空间,并不整理位置。例如 A、B、C 三个对象在内存中连续排列,若 B 被回收,A 和 C 之间就出现空隙。多次 GC 后,空隙越碎越多。哪怕老年代总空闲量充足,一旦需要分配一个大对象(如 byte[2MB]),也会因找不到连续地址空间而失败,最终触发 OutOfMemoryError: Java heap space,甚至被迫退化为 STW 全堆整理。
现代 JVM 不裸用标记-清除
纯标记-清除在 HotSpot 8u292+ 及以后版本中已无默认启用的收集器。它更多作为底层组件嵌入更复杂流程:
- CMS 曾用其并发清理老年代,但因碎片失控频繁触发 concurrent mode failure,最终被 G1 取代
- G1 不直接使用标记-清除,而是用 Remembered Set 精准识别跨代引用,在 Mixed GC 阶段结合 Evacuation(迁移+整理) 消除碎片
- ZGC 和 Shenandoah 在标记后通过 重映射(remap)+ 并发转移 实现低延迟下的碎片控制
真正有效的碎片治理路径
不是靠单个算法“解决”,而是靠组合策略和运行时干预:
立即学习“Java免费学习笔记(深入)”;
- 分代隔离 + 算法分工:年轻代用复制算法(ParNew / G1 Eden/Survivor),天然防碎片;老年代避免纯标记-清除,优先选 G1 或 ZGC
- 主动压缩与整理:对 CMS 可启用 -XX:+UseCMSCompactAtFullCollection;对 Parallel Old,确保 -XX:+UseParallelOldGC 开启默认 Compact
- 监控与调优落地点:用 jstat -gc <pid> 观察老年代使用率(O)是否持续接近已用容量(OC);G1 日志中出现 to-space exhausted 或 evacuation failure 就是碎片恶化的明确信号
该换收集器还是该调参数
如果频繁 Full GC 且老年代碎片严重,大概率是还在用 Serial Old 或 CMS 这类依赖标记-清除逻辑的老收集器。建议:
- JDK 8/11 应用可切换到 G1,配 -XX:G1HeapRegionSize=1M 减少 Region 数量,降低碎片粒度
- JDK 17+ 生产环境优先评估 ZGC,开启 -XX:+ZUncommit 自动归还未用堆内存给 OS,缓解虚拟地址碎片
- 禁用 -XX:-UseAdaptiveSizePolicy,防止 JVM 盲目扩容堆加剧碎片分布


















