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