世界上首個值得記念的GC 算法是GC 標記 - 清除算法(Mark-Sweep GC)。自其問世以來,一直到半個世紀後的今天,它依然是各類處理程序所用的偉大的算法。node
GC 標記 - 清除算法由標記階段和清除階段構成。git
標記階段是把全部活動對象(可達對象,reachable)都作上標記的階段。清除階段是把那些沒有標記的對象,也就是非活動對象回收的階段。經過這兩個階段,就能夠複用已釋放的空間。github
本文代碼使用C語言實現算法
對象在GC的世界裏,表明的是數據集合,是垃圾回收的基本單位。segmentfault
能夠理解爲就是C語言中的指針(又或許是handle),GC是根據指針來搜索對象的。數據結構
這個詞有些地方翻譯爲賦值器,但仍是比較奇怪,不如不翻譯……spa
mutator 是 Edsger Dijkstra 琢磨出來的詞,有「改變某物」的意思。說到要改變什麼,那就是 GC 對象間的引用關係。不過光這麼說可能你們仍是不能理解,其實用一句話歸納的話,它的實體就是「應用程序」。
mutatar的工做有如下兩種:翻譯
mutator 在進行這些操做時,會同時爲應用程序的用戶進行一些處理(數值計算、瀏覽網頁、編輯文章等)。隨着這些處理的逐步推動,對象間的引用關係也會「改變」。伴隨這些變化會產生垃圾,而負責回收這些垃圾的機制就是 GC。
GC ROOTS就是引用的起始點,好比棧,全局變量設計
堆就是進程中的一段動態內存,在GC的世界裏,通常會先申請一大段堆內存,而後mutatar在這一大段內存中進行分配指針
活動對象就是能經過mutatar(GC ROOTS)引用的對象,反之訪問不到的就是非活動對象。
在標記清除算法中,使用空閒鏈表(free-list)的內存分配策略
空閒鏈表分配使用某種數據結構(通常是鏈表)來記錄空閒內存單元的位置和大小,該數據結構即爲空閒內存單元的集合。
在須要分配內存時,順序遍歷每個內存單元,找到第一個空閒的內存單元使用。
在本文中,爲了下降複雜度,只使用了最基本的free-list分配法,free-list數據結構以下圖所示:
爲了實現簡單,在本文代碼中,每一個單元只存儲一個對象,不考慮單元拆分合並等問題。
首先是對象類型的結構:
爲了動態訪問「對象」的屬性,此處使用屬性偏移量來記錄屬性的位置,而後經過指針的計算得到屬性
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;//標記對象是否可達(reachable) } object; //繼承 //"繼承對象"需和父對象object基本屬性保持一致,在基本屬性以後,能夠定義其餘的屬性 typedef struct emp { class_descriptor *class;//對象對應的類型 byte marked;//標記對象是否可達(reachable) int id; dept *dept; } emp;
free-list結構設計
struct _node { node *next; byte used;//是否使用 int size;//單元大小 object *data;//單元中的數據 };
有了基本的數據結構,下面就能夠進行算法的實現了,如下執行GC前堆的狀態圖:
根據前面介紹的free-list內存分配策略,在新建對象時只須要搜索出空閒內存單元便可:
node *find_idle_node() { for (next_free = head; next_free && next_free->used; next_free = next_free->next) {} //還找不到就觸發回收 if (!next_free) { gc(); } for (next_free = head->next; next_free && next_free->used; next_free = next_free->next) {} //再找不到真的沒了…… if (!next_free) { printf("Allocation Failed!OutOfMemory...\n"); abort(); } }
在找到的空閒內存單元中分配新對象,並初始化
object *gc_alloc(class_descriptor *class) { if (!next_free || next_free->used) { find_idle_node(); } //賦值當前freePoint node *_node = next_free; //新分配的對象指針 //將新對象分配在free-list的節點數據以後,node單元的空間內除了sizeof(node),剩下的地址空間都用於存儲對象 object *new_obj = (void *) _node + sizeof(node); new_obj->class = class; new_obj->marked = FALSE; _node->used = TRUE; _node->data = new_obj; _node->size = class->size; 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; } next_free = next_free->next; return new_obj; }
GC代碼,當分配新對象而且可用內存不足時調用該方法
void gc() { for (int i = 0; i < _rp; ++i) { mark(_roots[i]); } sweep(); }
標記階段,要從GC ROOTS開始,遍歷對象圖(graph),對全部可達(reachable)的對象打上標記
for (int i = 0; i < _rp; ++i) { mark(_roots[i]); }
標記的代碼邏輯很簡單,就是遞歸查找對象並標記
void mark(object *obj) { //避免重複標記,由於一個對象可能被引用屢次 if (!obj || obj->marked) { return; } //給對象打上標記 obj->marked = TRUE; //遞歸標記對象的引用 //經過對象的field_offsets訪問對象的引用對象 for (int i = 0; i < obj->class->num_fields; ++i) { mark(*((object **) ((void *) obj + obj->class->field_offsets[i]))); } }
從上面的代碼邏輯能夠得出,標記階段的耗時和堆大小無關,耗時和存活對象的數量成正比
清除階段須要遍歷全堆(這裏是遍歷free-list),清除全部沒有標記的對象並回收對應的內存單元
void sweep() { for (node *_cur = head; _cur && _cur; _cur = _cur->next) { if (!_cur->used)continue; object *obj = _cur->data; if (obj->marked) { obj->marked = FALSE; } else { //回收對象所屬的node memset(obj, 0, obj->class->size); //經過地址計算出,對象所在的node node *_node = (node *) ((void *) obj - sizeof(node)); _node->used = FALSE; _node->data = NULL; _node->size = 0; //將next_free更新爲當前回收的node next_free = _node; } } }
因爲本文沒有實現free-list中空閒單元的拆分與合併,因此沒有涉及內存碎片化(fragmentation)問題.
若是實現空閒單元拆分合並的話,可能會致使不斷的拆分後,出現無數的小分散單元遍及整個堆,形成極大的內存浪費,而且增長free-list的掃描時間。
https://github.com/kongwu-/gc_impl/tree/master/mark-sweep