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

GC 標記 - 壓縮算法(Mark Compact GC)是將 GC 標記 - 清除算法與 GC 複製算法相結合的產物。

本文實現的是Donald E. Knuth研究出來的 Lisp2 算法,基於C語言git

在標記 - 整理算法中,標記階段和標記 - 清除算法中的的標記階段徹底同樣;而後對堆進行幾回搜索來整理活動對象。github

整理算法也是移動式的算法,不會有碎片化的問題,而且和複製算法相比不用犧牲半個堆的空間算法

名詞解釋

對象

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

指針

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

mutatar

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

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

mutatar的工做有如下兩種:翻譯

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

GC ROOTS

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

堆(Heap)

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

活動對象和非活動對象

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

準備工做

在標記-整理算法中,使用順序內存分配(sequential allocation)策略,順序分配流程以下圖所示

維護一個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 marked;//是否可達
object *forwarding;//目標位置

} object;

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

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

算法實現(Lisp2)

Lisp2 算法在對象頭裏爲 forwarding 指針留出了空間。這裏的forwarding 指針跟 GC 複製算法中的用法同樣。

假設咱們要在下面這種狀況下執行 GC

標記

首先是標記階段,標記-整理中的標記算法和標記-清除中一致;標記階段結束後的堆狀態以下圖:

mark代碼:

void mark(object *obj) {
    if (!obj || obj->marked) { return; }

    obj->marked = TRUE;
    printf("marking...\n");
    //遞歸標記對象的引用
    for (int i = 0; i < obj->class->num_fields; ++i) {
        mark(*((object **) ((void *) obj + obj->class->field_offsets[i])));
    }
}

整理

整理代碼:

void compact() {
    set_forwarding();
    adjust_ref();
    move_obj();
}

整理階段分爲三個步驟:

計算並設置整理後的對象forwarding指針

void set_forwarding() {
    int p = 0;
    int forwarding_offset = 0;

    //遍歷堆的已使用部分,這裏不用遍歷全堆
    //由於是順序分配法,因此只須要遍歷到已分配的終點便可
    while (p < next_free_offset) {
        object *obj = (object *) (p + heap);

        //爲可達的對象設置forwarding
        if (obj->marked) {

            obj->forwarding = (object *) (forwarding_offset + heap);

            forwarding_offset = forwarding_offset + obj->class->size;
        }
        p = p + obj->class->size;
    }
}

調整對象的引用爲移動後的地址

如上圖所示,調整引用後,gc roots和其餘對象的引用都已經更新爲了預先計算的forwarding指針

void adjust_ref() {
    int p = 0;

    //先將roots的引用更新
    for (int i = 0; i < _rp; ++i) {
        object *r_obj = _roots[i];
        _roots[i] = r_obj->forwarding;
    }

    //再遍歷堆,更新存活對象的引用
    while (p < next_free_offset) {
        object *obj = (object *) (p + heap);

        if (obj->marked) {
            //更新引用爲forwarding
            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;
    }
}

移動對象

void move_obj() {
    int p = 0;
    int new_next_free_offset = 0;
    while (p < next_free_offset) {
        object *obj = (object *) (p + heap);

        if (obj->marked) {
            //移動對象至forwarding
            obj->marked = FALSE;
            memcpy(obj->forwarding, obj, obj->class->size);
            new_next_free_offset = new_next_free_offset + obj->class->size;
        }
        p = p + obj->class->size;
    }

    //清空移動後的間隙
    memset((void *)(new_next_free_offset+heap),0,next_free_offset-new_next_free_offset);

    //移動完成後,更新free pointer爲新的邊界指針
    next_free_offset = new_next_free_offset;

}

經過上圖咱們可以確認,整理後,活動對象 B、C、D、F 分別對應整理後的BꞋ 、CꞋ、DꞋ 、FꞋ 。在 Lisp2 算法中,整理階段並不會改變對象的排列順序,只是縮小了它們之間的空隙,把它們彙集到了堆的一端。

以上就是對標記-整理算法的說明

優勢

沒有碎片化問題,並且能夠利用整個堆,不用像複製算法那樣將堆一分爲二

缺點

整理成本太高,在上述實現中,對堆進行了3次搜索。也就是說該算法的時間花費是和堆大小成正比的,和存活對象數量無關

完整代碼

https://github.com/kongwu-/gc_impl/tree/master/mark-compact

相關文章

參考

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