概述
还记得标记清除
和复制算法
的问题么? 堆使用效率低和碎片化问题. 那么有没有能够利用整个堆, 有没有内存碎片化问题的算法呢? 这就是标记压缩算法
了.
简单来说, 标记压缩算法就是将堆中的所有活动对象整体向左移, 将对象间的空隙消除.
在GC执行前的内存:
GC执行后的内存:
恩, 就是这么个意思.
实现
如何实现上面的操作呢? 首先, 要将所有活动对象标记出来. 这是标记阶段, 跳过了, 跟标记清除
一样操作就行. (这里每个对象都有一个mark
属性, true为活动对象)
标记完了, 那就剩下压缩操作了. 如何进行呢?
-
遍历堆, 将所有活动对象挪到左边. 但是, 后面有对象引用了前边的对象, 你就找不到新的指针了, 因为那块地址很可能已经被覆盖了.
-
....
最后想了想, 还是得老老实实地三步走:
-
遍历堆, 将所有对象通过计算得到新的地址并保存
-
遍历堆, 将所有子对象的地址更新为新的地址, 同时更新根集合中的指针.
-
遍历堆, 将对象集体迁移. 指针的问题都解决了, 可以将对象搬到新家了.
步骤一: 计算所有对象的新地址
// HEAP_START 是堆的开始位置, HEAP_END 是堆得结束位置 obj = HEAP_START newAddr = HEAP_START // 遍历所有活动对象 while(obj < HEAP_END){ // 非活动对象, 跳过 if(obj.mark != true){ obj += obj.size; continue; } // 记录新的地址 obj.newAddr = newAddr newAddr += obj.size // 继续遍历 obj += obj.size }
这遍完后, 所有活动对象都保存了自己的新地址, 然后就可以将所有指针的地址进行更新了.
步骤二: 更新所有指针
// 更新根集合中的指针 for(obj in roots){ obj = obj.newAddr } /* 更新所有活动对象的指针 当然, 这里也可以修改为遍历所有活动对象, 并将指针进行更新. 但是会出现各种重复处理、指针覆盖等问题, 就直接遍历堆了. */ obj = HEAP_START while(obj < HEAP_END){ if(obj.mark != true){ obj += obj.size; continue; } // 更新子对象 for(child in children){ child = child.newAddr } obj += obj.size }
至此, 所有指针都已经更新完毕, 但是, 对象还没有移动. 只剩下最后一步了, 将对象按照步骤一的规律, 向左排排坐就好啦.
步骤三: 迁移对象
obj = newAddr = HEAP_START while(obj < HEAP_END){ if(obj.mark != true) { obj += obj.size; continue; } // 将obj的数据复制到newAddr处 copyData(newAddr, obj, obj.size); // 清空数据, 为下一次GC做准本 newAddr.mark = false; newAddr.newAddr = null; // 遍历下一个对象 obj += obj.size newAddr += obj.size }
至此, 实现基本完成. 创建对象分配内存的操作与复制算法
一样. 这个算法简直是融合了标记清除
和复制算法
的优点, 解决了他们的问题, 不光堆的使用效率变高了, 而且也没有内存碎片的问题了. 但是, 就是, 只不过要对堆进行三次遍历而已. 不过没关系啦, 毕竟有失才有得嘛. 不过是时间换空间了.
而这, 也是标记压缩算法
最大的问题了, 执行时间太久了, 标记清除
对堆进行一次遍历, 而标记压缩
要进行三次. 三倍的时间. 可想而知.
不过也有伟人说了, 算法没有好不好, 只有是否适合. 这几种可达性
的算法各有优劣吧.
标记压缩的衍生
Two-Finger算法
将堆的遍历次数减少到两次.
如上图所示, 在第一次遍历的时候, 指针1从前向后寻找空闲地址, 指针2从后向前寻找活动对象, 找到后在原地址中记录新地址, 并将对象进行复制.
第二次遍历就可以将所有对象中的指针进行更新了.
你也发现了, 这个算法如果不想发生内存碎片化, 那就只能令每个对象的空间都是相同的. 而事实上也确实是这样. 强行规定每个对象都占用相同大小的空间, 我不知道这算法有什么应用场景. (原谅我的无知)
其他
还有一些其他的表格算法、lmmixGC算法等, 因为这两个我看的似懂非懂, 就不细说了.
标记压缩算法差不多就这么些. 告辞~~~