垃圾回收算法實現之 - 複製算法(完整可運行C語言代碼)

GC 複製算法(Copying GC)是 Marvin L. Minsky 在 1963 年研究出來的算法。說得簡單點,就是隻把某個空間裏的活動對象複製到其餘空間,把原空間裏的全部對象都回收掉。這是一個至關大膽的算法。在此,咱們將複製活動對象的原空間稱爲 From 空間,將粘貼活動對象的新空間稱爲 To 空間。

本文實現的是 Robert R. Fenichel 與 Jerome C. Yochelson 研究出來的 GC 複製算法,使用C語言實現git

名詞解釋

對象

對象在GC的世界裏,表明的是數據集合,是垃圾回收的基本單位。github

指針

能夠理解爲就是C語言中的指針(又或許是handle),GC是根據指針來搜索對象的。算法

mutatar

這個詞有些地方翻譯爲賦值器,但仍是比較奇怪,不如不翻譯……segmentfault

mutator 是 Edsger Dijkstra 琢磨出來的詞,有「改變某物」的意思。說到要改變什麼,那就是 GC 對象間的引用關係。不過光這麼說可能你們仍是不能理解,其實用一句話歸納的話,它的實體就是「應用程序」。

mutatar的工做有如下兩種:數據結構

  • 生成對象
  • 更新指針
mutator 在進行這些操做時,會同時爲應用程序的用戶進行一些處理(數值計算、瀏覽網頁、編輯文章等)。隨着這些處理的逐步推動,對象間的引用關係也會「改變」。伴隨這些變化會產生垃圾,而負責回收這些垃圾的機制就是 GC。

GC ROOTS

GC ROOTS就是引用的起始點,好比棧,全局變量spa

堆(Heap)

堆就是進程中的一段動態內存,在GC的世界裏,通常會先申請一大段堆內存,而後mutatar在這一大段內存中進行分配翻譯

活動對象和非活動對象

活動對象就是能經過mutatar(GC ROOTS)引用的對象,反之訪問不到的就是非活動對象。設計

準備工做

在複製算法中,使用順序內存分配(sequential allocation)策略,順序分配流程以下圖所示3d

維護一個free pointer,每次分配內存後移動該指針,limit-free的就是當前堆中可用內存的大小指針

數據結構設計

首先是對象類型的結構:

爲了動態訪問「對象」的屬性,此處使用屬性偏移量來記錄屬性的位置,而後經過指針的計算得到屬性

typedef struct class_descriptor {
    char *name;//類名稱
    int size;//類大小,即對應sizeof(struct)
    int num_fields;//屬性數量
    int *field_offsets;//類中的屬性偏移,即全部屬性在struct中的偏移量
} class_descriptor;

而後是對象的結構,雖然C語言中沒有繼承的概念,可是能夠經過共同屬性的struct來實現:

typedef struct _object {
    class_descriptor *class;//對象對應的類型
    byte forwarded;//對象已經移動的標記,防止被重複複製
    object *forwarding;//目標位置
} object;

//繼承
//"繼承對象"需和父對象object基本屬性保持一致,在基本屬性以後,能夠定義其餘的屬性
typedef struct emp {
    class_descriptor *class;//對象對應的類型
    byte forwarded;//對象已經移動的標記
    object *forwarding;//目標位置
    int id;
    dept *dept;
} emp;

有了基本的數據結構,下面就能夠進行算法的實現了

算法實現

複製算法利用From空間進行分配。當From空間被徹底佔滿沒法分配時,GC會將活動對象所有複製到To空間。當複製完成後,會將From/To空間互換,爲下次GC作準備。在本算法中,爲了確保To空間能夠容納全部From空間的活動對象,須要From和To空間容量保持一致。

複製算法的流程以下圖所示:

初始化堆

複製算法中,須要將堆一分爲二,一半做爲from,一半做爲to

void gc_init(int size) {
    heap_size = resolve_heap_size(size);
    heap_half_size = heap_size / 2;
    heap = (void *) malloc(heap_size);
    from = heap;
    to = (void *) (heap_half_size + from);
    _rp = 0;
}

建立對象&內存分配

新建立對象分配內存時,只須要移動free pointer便可

next_free_offset就是圖中的free pointer

object *gc_alloc(class_descriptor *class) {

    //檢查是否能夠分配
    if (next_free_offset + class->size > heap_half_size) {
        printf("Allocation Failed. execute gc...\n");
        gc();
        if (next_free_offset + class->size > heap_half_size) {
            printf("Allocation Failed! OutOfMemory...\n");
            abort();
        }
    }

    int old_offset = next_free_offset;

    //分配後,free移動至下一個可分配位置
    next_free_offset = next_free_offset + class->size;

    //分配
    object *new_obj = (object *) (old_offset + heap);

    //初始化
    new_obj->class = class;
    new_obj->forwarded = FALSE;
    new_obj->forwarding = NULL;

    for (int i = 0; i < new_obj->class->num_fields; ++i) {
        //*(data **)是一個dereference操做,拿到field的pointer
        //(void *)o是強轉爲void* pointer,void*進行加法運算的時候就不會按類型增長地址
        *(object **) ((void *) new_obj + new_obj->class->field_offsets[i]) = NULL;
    }

    return new_obj;
}

複製

複製時,需從GC ROOTS開始遍歷對象圖,對每個存活的對象進行復制;複製後對象地址改變,還須要更新GC ROOTS引用的地址;

void copying() {
    next_forwarding_offset = 0;
    //遍歷GC ROOTS
    for (int i = 0; i < _rp; ++i) {
        object *forwarded = copy(_roots[i]);

        //先將GC ROOTS引用的對象更新到to空間的新對象
        _roots[i] = forwarded;
    }

    //更新引用
    adjust_ref();

    //清空from,並交換from/to
    swap(&from,&to);
}

複製算法流程以下:
image

image

copy方法:

object *copy(object *obj) {

    if (!obj) { return NULL; }

    //因爲一個對象可能會被多個對象引用,因此此處判斷,避免重複複製
    if (!obj->forwarded) {

        //計算複製後的指針
        object *forwarding = (object *) (next_forwarding_offset + to);

        //賦值
        memcpy(forwarding, obj, obj->class->size);

        obj->forwarded = TRUE;

        //將複製後的指針,寫入原對象的forwarding pointer,爲最後更新引用作準備
        obj->forwarding = forwarding;

        //複製後,移動to區forwarding偏移
        next_forwarding_offset += obj->class->size;

        //遞歸複製引用對象,遞歸是深度優先
        for (int i = 0; i < obj->class->num_fields; i++) {
            copy(*(object **) ((void *) obj + obj->class->field_offsets[i]));
        }
        return forwarding;
    }

    return obj->forwarding;
}

Forwarding pointer

我的以爲「轉發指針(Forwarding Pointer)」在複製算法中仍是一個比較重要的概念

轉發指針,指的時複製時,在原對象裏保留新對象的指針。爲何要保留這個指針呢?

由於須要複製的不僅是對象,對象的引用關係也須要複製。好比下圖,對象ACD都須要複製,且只複製了對象A時,實際上覆制的對象A'(一撇)引用的CD仍是未複製的

調整引用

在全部活動對象都複製完畢後,須要將引用的地址調整爲複製後的對象地址;只須要遍歷一邊to空間,找到引用對象的forwarding pointer更新便可

void adjust_ref() {

int p = 0;
//遍歷to,即複製的目標空間
while (p < next_forwarding_offset) {
    object *obj = (object *) (p + to);
    //將還指向from的引用更新爲forwarding pointer,即to中的pointer
    for (int i = 0; i < obj->class->num_fields; i++) {
        object **field = (object **) ((void *) obj + obj->class->field_offsets[i]);
        if ((*field) && (*field)->forwarding) {
            *field = (*field)->forwarding;
        }
    }

    //順序訪問下一個對象
    p = p + obj->class->size;
}

}

以上就是對複製算法的說明

優勢

  • 吞吐量高,不須要遍歷全堆,只須要處理活動對象
  • 分配速度快,和free-list分配法相比,順序分配不須要搜索free-list,只須要移動free pointer便可
  • 不會有碎片化的問題,由於每次複製都將存活對象從from複製到to的一端

缺點

堆利用率較低,由於在複製算法下,只有一半的內存用來存儲對象

完整代碼

https://github.com/kongwu-/gc_impl/tree/master/copying

相關文章

參考

  • 《垃圾回收的算法與實現》 中村成洋 , 相川光 , 竹內鬱雄 (做者) 丁靈 (譯者)
  • 《垃圾回收算法手冊 自動內存管理的藝術》 理查德·瓊斯 著,王雅光 譯
相關文章
相關標籤/搜索