标记-清除算法不整理碎片,仅标记存活对象并清除死亡对象,导致外部碎片(空闲块散乱)和放大内部碎片(若配合固定块分配器),解决路径是换算法、加合并逻辑或换架构。

标记-清除算法本身不进行碎片整理,它只完成“标记存活对象”和“清除死亡对象”两步,既不移动存活对象,也不合并空闲区域。因此,它无法主动解决外部碎片或内部碎片问题——所谓“碎片整理方法”,其实是通过更换策略、引入辅助机制或切换到其他算法来规避或缓解碎片,而非在标记-清除框架内直接整理。
外部碎片:空闲块散乱,大对象分不了
外部碎片是标记-清除最典型的副作用。每次清除后,死亡对象留下的空闲空间位置随机、大小不一,堆内存逐渐变成“蜂窝状”。高频分配小对象(如循环中创建的临时String、HashMap.Entry)会反复切分和填充这些间隙,最终导致总空闲量充足,却凑不出一个连续的128KB空间。
- 首次适应策略下,低地址频繁被拆碎,高地址大块长期闲置
- 最佳适应策略虽选最匹配块,但常留下无法复用的极小残片
- 相邻空闲块默认不自动合并,两个64KB空闲区仍被视作独立单元
内部碎片:不是标记-清除造成,但会被放大
标记-清除算法本身不引入内部碎片,因为它按对象实际大小回收空间。但若上层分配器配合固定块内存池(如C语言中block_size=256B的池),就会产生内部碎片:请求12B的对象仍占256B,多余244B无法利用。这种碎片与标记-清除无关,但当该池使用标记-清除管理底层内存时,其内部碎片会叠加外部碎片,进一步降低有效利用率。
- 内部碎片根源在于分配粒度粗(如对齐到8/16/64字节)或预设块大小
- 它存在于已分配内存内部,程序无法主动释放,只能靠设计阶段控制块大小
- 标记-清除对此无能为力,也无法检测或修正
真正可行的“整理”路径:绕开或升级
想解决标记-清除带来的外部碎片,不能指望它自己整理,而要从三个方向入手:
- 换算法:改用标记-整理(Mark-Compact),将存活对象向一端滑动并更新所有引用,直接消除外部碎片;代价是STW时间更长
- 加合并逻辑:在空闲链表管理层面实现邻接块自动合并(释放时检查前后是否空闲,是则合并为一块),缓解但不能根治碎片累积
- 换架构:采用ZGC或Shenandoah这类支持并发压缩的收集器,靠读屏障+着色指针,在不停顿前提下完成对象移动与指针重写
小结:关键不在“清”,而在“不整”
标记-清除的清除动作只是把死亡对象空间挂入空闲列表,真正导致碎片不可逆的是——存活对象原地不动,地址固化,间隙永久保留。高频分配相当于持续往这个静态骨架里插拔积木,结构越用越松散。所以,与其纠结如何给标记-清除“打补丁”,不如明确它的适用边界:适合对象生命周期长、分配频率低、且能容忍碎片的场景;一旦出现大对象分配失败或GC频繁,就该考虑切换策略了。

















