清除不会移动和整理内存空间,一般都是通过空闲链表(双向链表)来标记哪一块内存空闲可用,因此会导致一个情况:空间碎片。
这会使得明明总的内存是够的,但是申请内存就是不足。
而且在申请内存的时候也有点麻烦,需要遍历链表查找合适的内存块,会比较耗时。
所以会有多个空闲链表的实现,也就是根据内存分块大小组成不同的链表,比如分为大分块链表和小分块链表,这样根据申请的内存分块大小遍历不同的链表,加快申请的效率。
当然还可以分更多个链表。
还有标记,标记的话一般我们会觉得应该是标记在对象身上,比如标记位放在对象头中,但是这对写时复制不兼容。
等于每一次 GC 都需要修改对象,假设是 fork 出来的,其实是共享一块内存,那修改必然导致复制。
所以有一种位图标记法,其实就是将堆的内存某个块用一个位来标记。就像我们的内存是一页一页的,堆中的内存可以分成一块一块,而对象就是在一块,或者多块内存上。
根据对象所在的地址和堆的起始地址就可以算出对象是在第几块上,然后用一个位图中的第几位在置为 1 ,表明这块地址上的对象被标记了。
而且用位图表格法不仅可以利用写时复制,清除也更加高效,如果标记在对象头上,那么需要遍历整个堆来扫描对象,现在有了位图,可以快速遍历清除对象。
但是不论是标记对象头还是利用位图,标记-清除的碎片问题还是处理不了。
因此就引出了标记-复制和标记-整理。
标记-复制首先这个算法会把堆分为两块,一块是 From、一块是 To。
对象只会在 From 上生成,发生 GC 之后会找到所有存活对象,然后将其复制到 To 区,之后整体回收 From 区。
再将 To 区和 From 区身份对调,即 To 变成 From , From 变成 To,我再用图来解释一波。
可以看到内存的分配是紧凑的,不会有内存碎片的产生。
不需要空闲链表的存在,直接移动指针分配内存,效率很高。
对 CPU缓存亲和性高,因为从根开始遍历一个节点,是深度优先遍历,把关联的对象都找到,然后内存分配在相近的地方。
这样根据局部性原理,一个对象被加载了那它所引用的对象也同时被加载,因此访问缓存直接命中。、
当然它也是有缺点的,因为对象的分配只能在 From 区,而 From 区只有堆一半大小,因此内存的利用率是 50%。
其次如果存活的对象很多,那么复制的压力还是很大的,会比较慢。
然后由于需要移动对象,因此不适用于上文提到的保守式 GC。
当然我上面描述的是深度优先就是递归调用,有栈溢出风险,还有一种 Cheney 的 GC 复制算法,是采用迭代的广度优先遍历,具体不做分析了,有兴趣自行搜索。
标记-整理标记-整理其实和标记-复制差不多,区别在于复制算法是分为两个区来回复制,而整理不分区,直接整理。
算法思路还是很清晰的,将存活的对象往边界整理,也没有内存碎片,也不需要复制算法那样腾出一半的空间,所以内存利用率也高。