概述
還記得標記清除和復制算法的問題么? 堆使用效率低和碎片化問題. 那么有沒有能夠利用整個堆, 有沒有內存碎片化問題的算法呢? 這就是標記壓縮算法了.
簡單來說, 標記壓縮算法就是將堆中的所有活動對象整體向左移, 將對象間的空隙消除.
在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算法等, 因為這兩個我看的似懂非懂, 就不細說了.
標記壓縮算法差不多就這么些. 告辭~~~
