1. 程式人生 > >GC演算法實踐(二) 物件標記、複製演算法

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]的複製為例:

這裡寫圖片描述

深度優先搜尋的複製演算法的思路如下:

複製一個根物件

  1. To空間中找到空閒空間的起始地址,
  2. 複製根物件到該起始地址
  3. 設定原物件的flag為2,表示已複製,避免重複複製。設定複製後物件的flag為0,表示初始狀態
  4. 遍歷該物件所直接引用的物件,如果沒有複製的話就遞迴呼叫該函式;關鍵:複製後要更新新物件的引用。

複製所有根物件

​ 遍歷根物件集合,對每個根物件呼叫“複製根物件”的函式,然後更新根物件集合的引用。

寫成程式碼就是:

// 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中的複製演算法(用深度優先搜尋)。當然可以用廣度優先搜尋,限於篇幅就不寫出來了。