标记-清除算法不处理内存碎片,仅标记存活对象并清除死亡对象空间,导致空闲块散乱隔离、无法合并,高频分配下加剧“蜂窝状”外部碎片,迫使系统依赖分代设计、增量合并或并发压缩等外围机制兜底。

标记-清除算法本身不处理碎片,也不主动优化内存分配——它只做两件事:标记存活对象、清除未标记对象所占空间。所谓“碎片处理”和“分配机制”,其实是它暴露问题后,系统必须额外补上的环节。
碎片不是它产生的,而是它放任的结果
标记-清除不移动对象、不合并空闲块、不重排地址。一次清除后,死亡对象留下的空闲空间散落在各处,大小不一、彼此隔离。这些空闲块被挂入空闲链表,但相邻的两个64KB空闲区默认仍算作两个独立单元,无法满足一个128KB对象的连续分配需求。
- 外部碎片是直接后果:堆内存变成“蜂窝状”,总空闲充足却凑不出大块连续空间
- 内部碎片并非该算法造成,但若上层用固定块分配器(如256B池),小对象仍占整块,浪费空间被叠加放大
- 高频分配会加速恶化:循环中反复创建销毁String、Map.Entry等短命对象,持续切分与填充间隙,碎片越积越密
它怎么给对象分配内存?靠空闲链表+简单策略
分配时,JVM从空闲链表中查找合适大小的连续块,不涉及整理或压缩。具体选哪一块,取决于配置的分配策略:
- 首次适应:从低地址开始找,容易让高地址大块长期闲置,低地址碎成“筛子”
- 最佳适应:选最接近请求大小的块,常留下无法复用的极小残片
- 无论哪种,都默认不检查前后是否空闲,更不会自动合并——合并需额外逻辑支持
真正起作用的“碎片缓解”,都在它外面
现代运行时从不裸用标记-清除。它只是基础组件,实际稳定性靠外围机制兜底:
- 分代设计:年轻代用复制算法天然防碎片;老年代保留标记逻辑,但清除后立即触发整理(如G1的Evacuation)或混合回收
- 增量合并:CMS可配-XX:+UseCMSCompactAtFullCollection,在Full GC时强制整理;G1靠Remembered Set控制标记范围,再借Region复制完成紧致
- 并发压缩架构:ZGC/Shenandoah用读屏障+着色指针,在应用线程运行时移动对象并更新引用,不依赖STW
你该盯住的三个实操信号
不是调算法,而是看现象、调参数、控行为:
- 监控jstat -gc <pid>:老年代使用率(O)持续接近100%但OC未满,同时出现to-space exhausted或evacuation failure,就是碎片恶化征兆
- 限制盲目扩容:-XX:-UseAdaptiveSizePolicy关掉自适应堆调整,避免JVM扩堆反而拉大碎片分布跨度
- 主动释放大块资源:对Direct Buffer等场景,在低峰期谨慎调用System.gc();更推荐ZGC的-XX:+ZUncommit,归还未用内存给OS

















