还记得标记清除
和复制算法
的问题么? 堆使用效率低和碎片化问题. 那么有没有能够利用整个堆, 有没有内存碎片化问题的算法呢? 这就是标记压缩算法
了.
简单来说, 标记压缩算法就是将堆中的所有活动对象整体向左移, 将对象间的空隙消除.
在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算法等, 因为这两个我看的似懂非懂, 就不细说了.
标记压缩算法差不多就这么些. 告辞~~~