标记-清除算法之所以会导致内存不连续,答案其实很直接:它只会“清空位置”,却从不主动“挪动对象”。

这句简短的话背后,隐藏着内存碎片化的核心逻辑,值得我们深入剖析。
存活对象原地不动,间隙永久保留
标记-清除算法在清除阶段压根不做任何移动操作。所有存活对象都老老实实待在原来的内存地址上,算法只是把死亡对象占用的空间标记为“空闲”。这些空闲空间就像散落在固定骨架之间的缝隙,彼此孤立、大小不一。时间一长,堆空间就变成了蜂窝状——空洞密布,但始终无法将它们拼接起来。
举个例子,假设堆里有一些长期存活的对象,它们像固定的支撑柱一样杵在那里。算法每次清理,只是把柱子之间的垃圾扫走,柱子本身纹丝不动。久而久之,柱子之间的空隙越来越小、越来越碎,再也容纳不下一个大块对象——这正是内存碎片(memory fragmentation)的典型表现。
清除动作不合并,空闲块各自为政
算法本身还有一个“盲区”:它从不检查相邻空闲区域是否可以合并。两个紧挨着的64KB空闲块,在空闲链表里仍然是两条独立的记录,不会自动合并成128KB的大块。分配器遍历时,只能一个一个地匹配,完全看不到“连续性”的存在。
这就好比一个书架,你把两本薄书抽走,中间留出了空隙,但算法不会主动把这两段空隙合并成一个完整的大格子。下次想放一本厚书时,却发现怎么也塞不进去——这种因不合并导致的碎片化,正是JVM(Java虚拟机)性能调优中常遇到的痛点。
高频分配加速碎片淤积
在实际运行中,如果循环里反复创建小对象——比如临时String、HashMap.Entry——这些对象会不断在存活对象之间切分、填充、再释放,导致小碎片快速累积。就像往一个固定格子的抽屉里,不停塞进不同尺寸的纸片,越塞越乱,最后连一张完整的A4纸都铺不开了。
这类碎片累积的速度,往往比我们想象的要快得多。尤其在频繁分配和释放的场景下,几轮操作下来,原本还算规整的空间就开始变得支离破碎,进而影响GC(垃圾回收)算法的整体效率。
老年代尤其容易“积碎成疾”
年轻代用的是复制算法,每轮Minor GC都会把存活对象集中搬运到另一个区域,天然就能实现内存规整。但老年代就不一样了:对象多、存活率高、又没有备用空间可以复制,只能就地清理。CMS这类纯标记-清除收集器,长期运行下来,碎片雪球就会越滚越大,大对象一来就被卡住,最终触发Full GC甚至直接抛出内存分配失败的错误。
所以,标记-清除算法导致内存不连续的根本原因,在于它只做“清扫”,不做“整理”。内存碎片化是其先天设计上的代价,而老年代则是这个代价最终兑现的地方——理解这一点,对JVM性能优化和GC调优至关重要。
