這篇文章寫的非常好,因此將它轉載到這裏:
編寫你的第一個垃圾收集器
伯樂在線補充:本文作者 Bob Nystrom 是 Google Dart 團隊的一名工程師,所以下文中”處理一些工作上的事情 “中的鏈接是指向了 Dart 官網。Bob 之前(曾在 EA 公司)做過遊戲開發,UI 設計。更多信息,請看他的簡歷 。
每當我倍感壓力以及有很多事情要做的時候,我總是有這樣一種反常的反應,那就是希望做一些其他的事情來擺脫這種狀況。通常情況下,這些事情都是些我能夠編寫並實現的獨立的小程序。
一天早上,我幾乎要被一堆事情給整瘋了——我得寫我那本《遊戲編程模式》 、處理一些工作上的事情 、還要準備一場Strange Loop的演講 ,然後這時我突然想到:“我該寫一個垃圾收集器了”。
是的,我知道那一刻讓我看上去有多瘋狂。不過我的神經故障卻是你實現一段基礎的程序語言設計的免費教程!在100行左右毫無新意的c代碼中,我設法實現一個基本的標記和掃描模塊。
有人認爲,垃圾收集好比是有更多鯊魚出沒的危險水域,但在這篇文章中,我會給你一個漂亮的兒童游泳池去玩耍。可能這裏面仍然會有一些坑,但至少這是一個淺水區。
精簡、複用、再複用
垃圾收集背後有這樣一個基本的觀念:編程語言(大多數的)似乎總能訪問無限的內存。而開發者可以一直分配、分配再分配——像魔法一樣,取之不盡用之不竭。
當然,我們從來都沒有無限的內存。所以計算機實現收集的方式就是當機器需要分配一些內存,而內存又不足時,讓它收集垃圾。
“垃圾(Garbage)”在這裏表示那些事先分配過但後來不再被使用的內存。而基於對無限內存的幻想,我們需要確保“不再被使用”對於編程語言來說是非常安全的。要知道在你的程序試圖訪問一些隨機的對象時它們卻剛好正在得到回收,這可不是一件好玩的事情。
爲了實現收集,編程語言需要確保程序不再使用那個對象。如果該程序不能得到一個對象的引用,那麼顯然它也不會再去使用它。所以關於”in use”的定義事實上非常簡單:
- 任何被一個變量引用的對象,仍然在作用域內,就屬於”in use”狀態。
- 任何被另一個對象引用的對象,仍在使用中,就是”in use”狀態。
如果對象A被一個變量引用,而它又有一些地方引用了對象B,那麼B就是在使用中(“in use”),因爲你能夠通過A來訪問到它。
這樣到最後的結果就是得到一張可訪問的對象圖——以一個變量爲起點並能夠遍歷到的所有對象。任何不在圖中的對象對於程序來說都是死的,而它的內存也是時候被回收了。
標記並清理
有很多不同的方法可以實現關於查找和回收所有未被使用的對象的操作,但是最簡單也是第一個被提出的算法就是”標記-清除”算法。它由John McCarthy——Lisp(列表處理語言)的發明者提出,所以你現在做的事情就像是與一個古老的神在交流,但希望你別用一些洛夫克拉夫特 式的方法——最後以你的大腦和視網膜的完全枯萎而結束。
該算法的工作原理幾乎與我們對”可訪問性(reachability)”的定義完全一樣:
- 從根節點開始,依次遍歷整個對象圖。每當你訪問到一個對象,在上面設置一個”標記(mark)”位,置爲true。
- 一旦搞定,找出所有標記位爲”not”的對象集,然後刪除它們。
對,就是這樣。我猜你可能已經想到了,對吧?如果是,那你可能就成爲了一位被引用了數百次的文章的作者。所以這件事情的教訓就是,想要在CS(計算機科學)領域中出名,你不必開始就搞出一個很牛的東西,你只需要第一個整出來即可,哪怕這玩意看上去很搓。
對象對
在我們落實這兩個步驟之前,讓我們先做些不相關的準備工作。我們不會爲一種語言真正實現一個解釋器——沒有分析器,字節碼、或任何這種愚蠢的東西。但我們確實需要一些少量的代碼來創建一些垃圾去收集。
讓我們假裝我們正在爲一種簡單的語言編寫一個解釋器。它是動態類型,並且有兩種類型的變量:int 和 pair。 下面是用枚舉來標示一個對象的類型:
typedef enum {
OBJ_INT,
OBJ_PAIR
} ObjectType;
typedef struct sObject {
ObjectType type;
union {
/* OBJ_INT */
int value;
/* OBJ_PAIR */
struct {
struct sObject* head;
struct sObject* tail;
};
};
} Object;
這個Object結構擁有一個type字段表示它是哪種類型的值——要麼是int要麼是pair。接下來用一個union來持有這個int或是pair的數據。如果你對c語言很生疏,一個union就是一個結構體,它將字段重疊在內存中。由於一個給定的對象只能是int或是pair,我們沒有任何理在一個單獨的對象中同時爲所有這3個字段分配內存。一個union就搞定。帥吧。
小虛擬機
現在我們可以將其包裝在一個小的虛擬機結構中了。它(指虛擬機)在這裏的角色是用一個棧來存儲在當前作用域內的變量。大多數語言虛擬機要麼是基於棧(如JVM和CLR)的,要麼是基於寄存器(如Lua)的。但是不管哪種情況,實際上仍然存在這樣一個棧。它用來存放在一個表達式中間需要用到的臨時變量和局部變量。
我們來簡潔明瞭地建立這個模型,如下:
#define STACK_MAX 256
typedef struct {
Object* stack[STACK_MAX];
int stackSize;
} VM;
現在我們得到了一個合適的基本數據結構,接下來我們一起敲些代碼來創建些東西。首先,我們來寫一個方法創建並初始化一個虛擬機:
VM* newVM() {
VM* vm = malloc(sizeof(VM));
vm->stackSize = 0;
return vm;
}
一旦我們得到了虛擬機,我們需要能夠操作它的堆棧:
void push(VM* vm, Object* value) {
assert(vm->stackSize < STACK_MAX, "Stack overflow!");
vm->stack[vm->stackSize++] = value;
}
Object* pop(VM* vm) {
assert(vm->stackSize > 0, "Stack underflow!");
return vm->stack[--vm->stackSize];
}
好了,現在我們能敲些玩意到”變量”中了,我們需要能夠實際的創建對象。首先來一些輔助函數:
Object* newObject(VM* vm, ObjectType type) {
Object* object = malloc(sizeof(Object));
object->type = type;
return object;
}
void pushInt(VM* vm, int intValue) {
Object* object = newObject(vm, OBJ_INT);
object->value = intValue;
push(vm, object);
}
Object* pushPair(VM* vm) {
Object* object = newObject(vm, OBJ_PAIR);
object->tail = pop(vm);
object->head = pop(vm);
push(vm, object);
return object;
}
標記
第一個階段就是標記(marking)。我們需要掃遍所有可以訪問到的對象,並設置其標誌位。現在我們需要做的第一件事就是爲對象添加一個標誌位(mark bit):
typedef struct sObject {
unsigned char marked;
/* Previous stuff... */
} Object;
void markAll(VM* vm)
{
for (int i = 0; i < vm->stackSize; i++) {
mark(vm->stack[i]);
}
}
void mark(Object* object) {
object->marked = 1;
}
void mark(Object* object) {
object->marked = 1;
if (object->type == OBJ_PAIR) {
mark(object->head);
mark(object->tail);
}
}
爲了解決這個情況,我們僅需要做的是在訪問到了一個已經處理過的對象時,退出即可。所以完整的mark()方法應該是:
void mark(Object* object) {
/* If already marked, we're done. Check this first
to avoid recursing on cycles in the object graph. */
if (object->marked) return;
object->marked = 1;
if (object->type == OBJ_PAIR) {
mark(object->head);
mark(object->tail);
}
}
清理
下一個階段就是清理一遍所有我們已經分配過(內存)的對象並釋放那些沒有被標記過的(對象)。但這裏有一個問題:所有未被標記的對象——我們所定義的——都不可達!我們都不能訪問到它們!
虛擬機已經實現了對象引用的語義:所以我們只在變量和pair元素中儲存指向對象的指針。當一個對象不再被任何指針指向時,那我們就完全失去它了,而這也實際上造成了內存泄露。
解決這個問題的訣竅是:虛擬機可以有它自己的對象引用,而這不同於對語言使用者可讀的那種語義。換句話說,我們自己可以保留它們的痕跡。
這麼做最簡單的方法是僅維持一張由所有分配過(內存)的對象(組成)的鏈表。我們在這個鏈表中將對象自身擴展爲一個節點:
typedef struct sObject {
/* The next object in the list of all objects. */
struct sObject* next;
/* Previous stuff... */
} Object;
typedef struct {
/* The first object in the list of all objects. */
Object* firstObject;
/* Previous stuff... */
} VM;
Object* newObject(VM* vm, ObjectType type) {
Object* object = malloc(sizeof(Object));
object->type = type;
object->marked = 0;
/* Insert it into the list of allocated objects. */
object->next = vm->firstObject;
vm->firstObject = object;
return object;
}
void sweep(VM* vm)
{
Object** object = &vm->firstObject;
while (*object) {
if (!(*object)->marked) {
/* This object wasn't reached, so remove it from the list
and free it. */
Object* unreached = *object;
*object = unreached->next;
free(unreached);
} else {
/* This object was reached, so unmark it (for the next GC)
and move on to the next. */
(*object)->marked = 0;
object = &(*object)->next;
}
}
}
祝賀你!我們已經有了一個垃圾收集器!現在只剩下一點工作了:實際調用它!首先我們將這兩個階段整合在一起:
void gc(VM* vm) {
markAll(vm);
sweep(vm);
}
事實證明,我們沒有完全正確或錯誤的答案。這真的取決於你使用虛擬機的目的以及讓它運行在什麼樣的硬件上。爲了讓這個例子看上去很簡單,我們僅在進行了一定數量的內存分配之後開始收集。事實上一些語言的實現就是這麼做的,而這也很容易。
我們將邀請虛擬機來追蹤我們到底創建了多少(對象):
typedef struct {
/* The total number of currently allocated objects. */
int numObjects;
/* The number of objects required to trigger a GC. */
int maxObjects;
/* Previous stuff... */
} VM;
VM* newVM() {
/* Previous stuff... */
vm->numObjects = 0;
vm->maxObjects = INITIAL_GC_THRESHOLD;
return vm;
}
每當我們創建一個對象,我們增加numObjects,如果它達到最大值就啓動一次收集:
Object* newObject(VM* vm, ObjectType type) {
if (vm->numObjects == vm->maxObjects) gc(vm);
/* Create object... */
vm->numObjects++;
return object;
}
void gc(VM* vm) {
int numObjects = vm->numObjects;
markAll(vm);
sweep(vm);
vm->maxObjects = vm->numObjects * 2;
}
最後
你成功了!如果你全部照做了,那你現在已經得到了一個簡單的垃圾收集算法的句柄。如果你想看完整的代碼,在這裏 。我再強調一點,儘管這個收集器很簡單,但它可不是一個玩具。
你可以在這上面做一大堆的優化(像在GC和程序設計語言這些事情中,90%的努力都在優化上),但它的核心代碼可是真正的GC。它與目前Ruby和Lua中的收集器非常的相似。你可以使用一些類似的代碼到你的項目中。去做些很酷的事情吧!
譯文鏈接: http://blog.jobbole.com/53376/
[ 轉載必須在正文中標註並保留原文鏈接、譯文鏈接和譯者等信息。]