标记-清除算法能正常回收大对象,但因不整理内存导致外部碎片,使后续无法分配连续空间;空闲块彼此隔离、不自动合并,总空闲量充足也无法满足大对象请求,进而频繁触发gc,加剧stw压力。

标记-清除算法在处理大对象时表现较弱,核心问题不是“回收不了”,而是“分配不出”——它本身能正常回收大对象,但因不整理内存,导致后续无法为新大对象找到连续空间。
外部碎片直接阻塞大对象分配
每次清除后,死亡对象留下大小不一、位置随机的空闲块。这些块彼此隔离,即使总空闲量足够,也无法拼出一个连续的大块。例如:堆中剩余三个 64KB 空闲区,但被存活对象隔开,就无法满足一个 128KB 的数组分配请求。
- 首次适应分配策略下,低地址区域反复被切碎,大对象只能向高地址寻找,而高地址往往长期闲置却难以利用
- 相邻空闲块默认不合并,两个紧挨着的 64KB 空闲区仍被当作两个独立单元,无法响应 128KB 请求
- 频繁分配小对象(如循环中的临时 Map.Entry)会加速蜂窝状碎片形成,进一步压缩大对象可用窗口
触发额外 GC 增加延迟与开销
当分配器发现无足够连续空间时,JVM 不会尝试整理,而是立即触发下一轮 GC——哪怕此时堆中仍有大量分散空闲内存。这造成恶性循环:
- 一次 GC 未解决碎片,下次分配又失败,再次停顿,加剧 STW(Stop-The-World)压力
- 尤其在老年代使用标记-清除时,对象存活率高,清除后碎片更顽固,GC 频次上升明显
- 大对象(如大 byte[]、长字符串)常直接进入老年代,使问题集中在本就回收代价高的区域
内部碎片虽非直接成因,但会叠加恶化
标记-清除本身不产生内部碎片,但如果上层采用固定块分配器(如按 256B 对齐分配),小对象也会占用整块,浪费空间。这种浪费在碎片化堆中更难被复用:
- 一个请求 12B 的对象占了 256B,这 244B 内部碎片无法拆分给其他对象使用
- 当外部碎片已使大块稀缺,内部碎片进一步降低有效内存利用率
- 问题在混合使用多种分配策略(如 TLAB + 全局空闲链表)时尤为突出
实际应对思路不在算法内部,而在架构层面
标记-清除本身不提供整理能力,所谓“优化”其实是绕过它的局限:
- 改用标记-整理算法:让存活对象向一端滑动,腾出连续大块,适合老年代
- 引入空闲块合并逻辑:在清除阶段扫描相邻空闲区并合并,需维护双向链表或位图辅助
- 分代设计隔离影响:把大对象导向专门区域(如 G1 的大对象区 Humongous Region),避免污染常规分区
- 配合压缩式 GC(如 ZGC、Shenandoah):并发整理,减少 STW 时间,但底层已超出纯标记-清除范畴










