GC演算法實踐(二) 物件標記、複製演算法
上一篇文章中,我們實現了自定義分配記憶體,有了這個基礎,我們可以開發垃圾回收演算法了。GC演算法有很多種,如引用計數法、標記-清除演算法、複製演算法、分代回收演算法等,也有綜合運用幾種演算法的。PHP用到了引用計數演算法,Java用到了複製演算法和分代回收演算法。由於引用計數演算法需要頻繁更新引用計數,目前暫不研究;標記-清除演算法則因為清除後造成大量記憶體碎片不好管理,目前只研究標記(標記出活動物件);複製演算法是本篇研究的重點。
1.標記-清除演算法、複製演算法簡介
1.1 根物件
首先需要理解一個基本概念:根物件。根物件是程式中可以直接訪問到的物件,比如:
有兩個物件
a
,a
有成員b
,訪問物件a
直接用a
就行,而訪問物件b需要通過a->b
,那麼物件a
就是根物件,物件b
由於只能通過a->b
來訪問,所以不是根物件。
根物件可以是全域性變數、函式呼叫棧上的變數等。
1.2 標記-清除演算法
標記-清除演算法的大致思想如下:
標記階段:遍歷根物件及其引用的物件。假設每個物件都有個標記位
flag
,對根物件集合中的每個根物件,從根物件出發,對可以訪問到的每個物件的標記位flag
設為1(活動物件)。清除階段:遍歷堆,將非活動物件所佔空間設為可用。遍歷堆,將標記位
flag
等於0的物件(即垃圾)所佔據的空間設為可用。
標記-清除演算法的清除階段 後,產生很多記憶體碎片,管理比較麻煩。
1.3 複製演算法
複製演算法的大致思想如下:
把給物件分配記憶體的堆(heap)分成大小相等的兩部分,或者申請2塊大小相同的堆,其中一個堆稱為
From空間
,另一個稱為To空間
。首次給物件分配記憶體時,活動堆為
From空間
,從From空間
分配;觸發GC時,採用標記-清除演算法中的標記演算法遍歷活動物件,把活動物件複製到To空間
,然後就把To空間
當做當前活動堆。To空間
滿觸發GC時,把活動物件複製到From空間
,如此交替進行。
複製演算法的缺點是記憶體空間利用效率低,只有50%。
2.標記物件
先構建一個測試場景,程式碼如下:
Object *root[2 ]; // simulate root objects collection
void test_alloc_memory() {
Object *objects[6];
int obj_len[6] = {3,2,4,2,3,2};
int i;
for(i=0; i<6; i++) {
objects[i] = new_object(obj_len[i]);
OBJ_SET_INT(objects[i], 0, i);
}
OBJ_SET_OBJ(objects[1], 1, objects[4]); // objects[1]->objects[4]
OBJ_SET_OBJ(objects[0], 1, objects[1]); // objects[0]->objects[1]
OBJ_SET_OBJ(objects[0], 2, objects[5]); // objects[0]->objects[5]
root[0] = objects[0];
root[1] = objects[2]; // objects[3] is garbage
}
為了簡化問題,當前用一個物件陣列root
來表示根物件集合。
objects[0]
會引用到objects[1]
、objects[4]
和、objects[5]
。我們只把objects[0]
和objects[2]
新增到根物件集合中,因為objects[3]
無法從根物件中訪問到,因此,objects[3]
是“垃圾”(不可達的物件就是垃圾)。
以上程式碼形成的引用關係示意圖如下:
上圖中,object[0]
中的方括號沒有陣列的含義,僅僅是一個符號而已,它指的就是alloc_memory
函式中的objects[0]
。objref[0]
是指向object[0]
的引用,objref[2]
是指向`object[2]
的引用。
標記演算法可實現為如下:
void mark_object(Object *obj) {
Object *sub_obj = NULL;
int index;
obj->flag = 1; // active
for(index=1; index<obj->length; index++) {
sub_obj = OBJ_GET_OBJ(obj, index);
if (NULL != sub_obj) {
mark_object(sub_obj);
}
}
}
void mark_objects(Object *root[], int len) {
int i;
for(i=0; i<len; i++) {
mark_object(root[i]);
}
}
本篇文章中的程式碼是基於上一篇文章的,所以,程式碼方面有疑問的話需要先閱讀上篇文章。
關鍵點如下:
- 物件的結構模型仍然是按上篇文章中簡化後的模型,即第一個欄位的資料型別為
int
,後續的欄位型別為Object
。 - 函式
mark_object
用於標記單個根物件,以及該根物件所引用到的物件,其中用到了遞迴。將物件的flag
欄位設為1,表示該物件為活動物件。未標記的物件的flag
為0。 - 函式
mark_objects
中,遍歷根物件集合,堆每個跟物件依次呼叫函式mark_object
,即完成所有物件的標記。
組織測試:
int main() {
init_heap();
printf("after alloc...");
test_alloc_memory();
dump_heaps();
printf("after mark...");
mark_objects(root, 2);
dump_heaps();
return 0;
}
執行結果如下:
分配記憶體後堆的列印結果:
標記後堆的列印結果:
對比上面的 “引用關係示意圖”,可知“垃圾物件” object[3]
識別出來了。
3.複製演算法
複製演算法可以採用深度優先搜尋(dfs),也可以用廣度優先搜尋(bfs)。這兩種方法複製後物件的物理位置順序不一樣。以object[0]
的複製為例:
深度優先搜尋的複製演算法的思路如下:
複製一個根物件:
- 從
To空間
中找到空閒空間的起始地址,- 複製根物件到該起始地址
- 設定原物件的
flag
為2,表示已複製,避免重複複製。設定複製後物件的flag
為0,表示初始狀態- 遍歷該物件所直接引用的物件,如果沒有複製的話就遞迴呼叫該函式;關鍵:複製後要更新新物件的引用。
複製所有根物件:
遍歷根物件集合,對每個根物件呼叫“複製根物件”的函式,然後更新根物件集合的引用。
寫成程式碼就是:
// copy a root object and the objects referenced by it
Object* copy_object_dfs(Object *obj) {
Object *sub_obj;
Object *new_obj;
int index;
uint size = OBJ_SIZE(obj);
char* new_addr = alloc_memory(free_heap, size);
memcpy(new_addr, obj, size);
new_obj = (Object*)new_addr;
OBJ_SET_FIELDS(new_obj, new_addr);
obj->flag = 2; // copied
new_obj->flag = 0;
// copy each direct referenced object
for(index=1; index<obj->length; index++) {
sub_obj = OBJ_GET_OBJ(obj, index);
if (NULL != sub_obj && sub_obj->flag != 2) {
// update reference
OBJ_SET_OBJ(new_obj, index, copy_object_dfs(sub_obj));
OBJ_SET_OBJ(obj, index, sub_obj);
}
}
return new_obj;
}
// copy all root objects
void copy_objects_dfs(Object *root[], int len) {
int i;
for(i=0; i<len; i++) {
root[i] = copy_object_dfs(root[i]);
}
}
其中,巨集OBJ_SET_FIELDS
定如下:
#define OBJ_SET_FIELDS(obj,new_addr) (obj)->fields = new_addr + sizeof(Object)
物件複製過程示意圖如下:
複製完成後記憶體中個物件的引用關係示意圖如下:
箭頭顏色說明:
- 藍色,表示原有的引用關係
- 紅色,表示未更新的引用關係
- 綠色,表示更新後的引用關係
4.測試
如何驗證複製是OK的呢?在複製的實現程式碼中,我們用到了memcpy
函式,該函式會忠實地拷貝記憶體。檢驗複製成功考慮以下三個指標:
- 基本資料型別的值保持不變
- 指標的指向關係保持不變
- 最後,該複製的物件都要複製,不能有遺漏,或者複製了“垃圾物件”。
組織測試:
int main() {
init_heap();
printf("after alloc...");
test_alloc_memory();
dump_heaps();
printf("after copy...");
copy_objects_dfs(root, 2);
dump_heaps();
dump_active_objects(root, 2);
return 0;
}
執行結果:
複製前活動堆(From空間)的情況:
複製後空閒堆(To空間)的情況:
因為object[3]
是“垃圾物件”,所以沒有複製。
仔細比較複製前後堆的資料,以及物件之間的引用關係,可知複製是OK的。
把活動物件打印出來看下:
也是OK的。
5.總結
在基於上篇自定義記憶體分配的基礎上,本文實現了:
- 正確標記物件(活動物件與“垃圾物件”)
- GC中的複製演算法(用深度優先搜尋)。當然可以用廣度優先搜尋,限於篇幅就不寫出來了。