Lua GC算法
1.概覽
垃圾回收算法裏,有兩大類別:一是引用計數法,二是標記清除法(Mark & Sweep)。Lua 採用的是第二種,標記清除法。
在不同的版本中,Lua的GC有不一樣的做法。
5.0版本的雙色標記算法
在早期的5.0版本使用的是雙色標記清除法(Two-Color Mark and Sweep),它的大概原理是:每一個對象的狀態非黑即白,要麼被引用,要麼沒有被引用,系統在清理過程中會把沒有引用的回收掉。但是它個算法其中有一個很大的缺陷是:在回收的過程中不可以被打斷,程序必須停下來爲GC服務,直到GC執行完畢爲止。
5.1版本的三色增量標記清除算法
由於雙色標記算法對程序的影響很大,從5.1開始優化了GC算法,採用三色增量標記清除算法(全稱: Dijkstra’s three-color incremental mark & sweep)。對比舊版本的算法,這個算法的好處是:GC過程可以分步執行,增量回收,被中斷之後可以繼續執行。具體三種顏色爲:
白色:新建對象的初始狀態。如果在一輪GC掃描結束後,對象還是爲白色,該對象可以被進行回收了。
灰色:表示對象已經被GC掃描過,但該對象引用的對象還沒被掃描到。
黑色:表示對象已經被GC掃描過,同時該對象引用的對象也被掃描過了。
三色增量標記清除算法算法過程
簡單的說是:每一個對象創建後會賦予爲白色,經常各種操作運算賦值等操作,被GC掃描完後如果是還是白色,就把它回收了。
它的大概過程是:
//對象創建時
把新創建的對象標記爲白色
//初始化對象
遍歷rootgc下所有引用的對象,從白色標記爲灰色,並放到g->gray節點列表中
//標記階段
當灰色鏈表中還有未掃描的對象:(g->gray會指向GCObject,通過gclist串起來,當最後一個節點指向的gclist爲空的,就停止標記)
---- 從鏈表中取出對象並標記爲黑色;
---- 遍歷這個對象關聯的其它所有對象:
-------- 如果是白色:
-------- 標記爲灰色,並加入到灰色鏈表中
//回收階段
遍歷所有對象:
---- 如果爲白色:
-------- 這些對象都是沒有被其它對象引用的,則把它回收了
---- 否則:
-------- 重新加入對象鏈表中等待下一輪的GC檢查
新建對象會存放在g->rootgc鏈表上,同時sweepgc指向rootgc:
每一個對象通過不同的字段指向不同鏈表。
例如某個GCObject o通過gco2th(o)->gclist鏈接到gray上,o->gch.next鏈接到rootgc上。
2.關鍵代碼
GC相關的數據主要存放在global_State:
//lstate.h, line:65
/*
** `global state', shared by all threads of this state
*/
typedef struct global_State {
stringtable strt; /* hash table for strings */
lua_Alloc frealloc; /* function to reallocate memory */
void *ud; /* auxiliary data to `frealloc' */
lu_byte currentwhite;
lu_byte gcstate; /* state of garbage collector */
int sweepstrgc; /* position of sweep in `strt' */
GCObject *rootgc; /* list of all collectable objects */
GCObject **sweepgc; /* position of sweep in `rootgc' */
GCObject *gray; /* list of gray objects */
GCObject *grayagain; /* list of objects to be traversed atomically */
GCObject *weak; /* list of weak tables (to be cleared) */
GCObject *tmudata; /* last element of list of userdata to be GC */
Mbuffer buff; /* temporary buffer for string concatentation */
lu_mem GCthreshold;
lu_mem totalbytes; /* number of bytes currently allocated */
lu_mem estimate; /* an estimate of number of bytes actually in use */
lu_mem gcdept; /* how much GC is `behind schedule' */
int gcpause; /* size of pause between successive GCs */
int gcstepmul; /* GC `granularity' */
lua_CFunction panic; /* to be called in unprotected errors */
TValue l_registry;
struct lua_State *mainthread;
UpVal uvhead; /* head of double-linked list of all open upvalues */
struct Table *mt[NUM_TAGS]; /* metatables for basic types */
TString *tmname[TM_N]; /* array with tag-method names */
} global_State;
與GC緊密聯繫的幾個重要參數爲(個別參數不影響整體流程,暫時不細說):
lu_byte gcstate :當前GC的狀態(主要有5個狀態:GCSpause(GC暫停狀態,還沒開始GC), GCSpropagate(遍歷灰色節點階段),GCSsweepstring(字符串回收階段),GCSsweep(除字符串以外的回收階段),GCSfinalize(終止階段)
GCObject *rootgc : 待GC回收對象的鏈表,所有新建的GC對象會放在這裏
GCObject *gray : 灰色節點的鏈表
GCObject *grayagain :將被進行原子性回的所有對象的鏈表(原子性意即不能被打斷)
3.垃圾回收過程
在Lua GC的源碼裏,GC過程劃分爲5個不同階段,以下根據5種狀態來分析記錄。
階段1:GCSpause(暫停階段)
//lgc.c, line:560
case GCSpause: {
markroot(L); /* start a new collection */
return 0;
}
這個階段,系統主要是執行markroot
- 把gray、grayagaint和weak清空,
- 把mainthread、全局表和registry表進行標記,把它們從白色標記爲灰色,並且加入到gray鏈表中(字符串、udata和UpValue有特殊處理,暫且不討論)
- 通知系統進入一下階段:GCSpropagate(遍歷灰色節點階段)
markroot的源碼爲:
//lgc.c, line:500
/* mark root set */
static void markroot (lua_State *L) {
global_State *g = G(L);
g->gray = NULL;
g->grayagain = NULL;
g->weak = NULL;
markobject(g, g->mainthread);
/* make global table be traversed before main stack */
markvalue(g, gt(g->mainthread));
markvalue(g, registry(L));
markmt(g);
g->gcstate = GCSpropagate;
}
階段2:GCSpropagate(遍歷灰色節點階段)
這個階段主要是遍歷灰色鏈表(g->gray)和g->grayagain鏈表,把符合規則的灰色節點改爲黑色。
灰色鏈表通過propagatemark入口進入遍歷的,當灰色鏈表已經遍歷完,並且灰色鏈表沒有對象以後,則需要遍歷g->grayagain,調用入口是atomic。
//lgc.c, line:564
case GCSpropagate: {
if (g->gray)
return propagatemark(g);
else { /* no more `gray' objects */
atomic(L); /* finish mark phase */
return 0;
}
}
GC在掃描過程中,大體都是一致的,把節點顏色標記爲黑色。但是針對4種指定特殊的對象有單獨的處理。
- Table對象
如果該表是弱表,則把該對象放到weak鏈表中,並且把它的顏色回退到灰色。
如果不是弱表的話,則遍歷Table的所有元素,包括數組部分和hash部分。 - Closure對象
不管是CClosure還是LClosure,GC都把遍歷所有UpValue標記爲黑色。 - Thread對象
對於線程對象,GC會把它從gclist拿出來,放到grayagain鏈表裏,並且把它回退到灰色,同時遍歷該線程堆棧上(traversestack)所有的元素。 - proto對象
標記proto數據中的文件名、字符串、upvalue、局部變量等所有被引用的對象
//lgc.c, line:273
/*
** traverse one gray object, turning it to black.
** Returns `quantity' traversed.
*/
static l_mem propagatemark (global_State *g) {
GCObject *o = g->gray;
lua_assert(isgray(o));
gray2black(o);
switch (o->gch.tt) {
case LUA_TTABLE: {
Table *h = gco2h(o);
g->gray = h->gclist;
if (traversetable(g, h)) /* table is weak? */
black2gray(o); /* keep it gray */
return sizeof(Table) + sizeof(TValue) * h->sizearray +
sizeof(Node) * sizenode(h);
}
case LUA_TFUNCTION: {
Closure *cl = gco2cl(o);
g->gray = cl->c.gclist;
traverseclosure(g, cl);
return (cl->c.isC) ? sizeCclosure(cl->c.nupvalues) :
sizeLclosure(cl->l.nupvalues);
}
case LUA_TTHREAD: {
lua_State *th = gco2th(o);
g->gray = th->gclist;
th->gclist = g->grayagain;
g->grayagain = o;
black2gray(o);
traversestack(g, th);
return sizeof(lua_State) + sizeof(TValue) * th->stacksize +
sizeof(CallInfo) * th->size_ci;
}
case LUA_TPROTO: {
Proto *p = gco2p(o);
g->gray = p->gclist;
traverseproto(g, p);
return sizeof(Proto) + sizeof(Instruction) * p->sizecode +
sizeof(Proto *) * p->sizep +
sizeof(TValue) * p->sizek +
sizeof(int) * p->sizelineinfo +
sizeof(LocVar) * p->sizelocvars +
sizeof(TString *) * p->sizeupvalues;
}
default: lua_assert(0); return 0;
}
}
灰色鏈表沒有對象以後,則需要遍歷g->grayagain,調用入口是atomic。
這個GC算法稱爲:三色增量標記掃描算法,整體是增量可以被打斷的,唯獨除了調用atomic這一步。
static void atomic (lua_State *L) {
global_State *g = G(L);
size_t udsize; /* total size of userdata to be finalized */
/* remark occasional upvalues of (maybe) dead threads */
remarkupvals(g);
/* traverse objects cautch by write barrier and by 'remarkupvals' */
propagateall(g);
/* remark weak tables */
g->gray = g->weak;
g->weak = NULL;
lua_assert(!iswhite(obj2gco(g->mainthread)));
markobject(g, L); /* mark running thread */
markmt(g); /* mark basic metatables (again) */
propagateall(g);
/* remark gray again */
g->gray = g->grayagain;
g->grayagain = NULL;
propagateall(g);
udsize = luaC_separateudata(L, 0); /* separate userdata to be finalized */
marktmu(g); /* mark `preserved' userdata */
udsize += propagateall(g); /* remark, to propagate `preserveness' */
cleartable(g->weak); /* remove collected objects from weak tables */
/* flip current white */
g->currentwhite = cast_byte(otherwhite(g));
g->sweepstrgc = 0;
g->sweepgc = &g->rootgc;
g->gcstate = GCSsweepstring;
g->estimate = g->totalbytes - udsize; /* first estimate */
}
這個函數主要做的操作有:
- 調用remarkupvals把UpValua標記爲灰色,標記的時候UpVal會加入到gray列表裏,所以需要調用propagateall將gray鏈表標記一下。
- 把gray指向weak表,清除weak表,同時調用propagateall將gray鏈表標記一下。
- 標記grayagain
- 把當前白色類型切換到下一次GC操作的白色類型(此篇文章暫時沒有展開討論)
- 修改GC狀態到下一個階段:GCSsweepstring(回收字符串階段)
也許你會問,爲什麼會有grayagain的存在呢?
因爲在遍歷灰色鏈表的過程中,可能會有新增對象會被掃描過的Table對象引用,這個Table對象將會放在grayagain裏。
特別需要注意的是:在標記過程中,如果新增對象被一個已經被GC掃描過的對象引用了,遇到這種情況時會根據兩個對象的狀態和類型做出處理。(顏色的標記過程是:白->灰->黑,向前指的是向右,向後指是向左)
luaC_barrierf(標記顏色向前一步):如果一個新建的對象(白色的),被一個已經掃描過的對象(黑色的)引用了,那麼會把這個新建的對象(白色的)改成灰色。
luaC_barrierback(標記顏色向後一步): 如果一個新建的對象(白色的),被一個已經掃描過的Table對象(黑色的)引用了,那麼會把這個被掃描過對象(黑色的)改成灰色,並且把它加到grayagain鏈表裏。
不知道你注意到了沒:向前和向後的區別有兩個,第一是:被標記顏色的對象,前者是對新建對象標記,後者是對被掃描過的對象標記;第二,luaC_barrierback是針對Table對象處理的。
階段3:GCSsweepstring(字符串回收階段)
回收階段分爲兩大部分,第一是回收字符串部分,第二是,肯定是除開字符串以外的其它對象部分的啦~
//lgc.c, line:572
case GCSsweepstring: {
lu_mem old = g->totalbytes;
sweepwholelist(L, &g->strt.hash[g->sweepstrgc++]);
if (g->sweepstrgc >= g->strt.size) /* nothing more to sweep? */
g->gcstate = GCSsweep; /* end sweep-string phase */
lua_assert(old >= g->totalbytes);
g->estimate -= old - g->totalbytes;
return GCSWEEPCOST;
}
針對回收字符串階段,系統會對字符串存放的stringtable中的hash表進行遍歷回收,當回收完畢以後,把GC狀態修改爲:GCSsweep,進入下一個回收狀態。
階段4:GCSsweep(除字符串以外的回收階段)
//lgc.c, line:407
case GCSsweep: {
lu_mem old = g->totalbytes;
g->sweepgc = sweeplist(L, g->sweepgc, GCSWEEPMAX);
if (*g->sweepgc == NULL) { /* nothing more to sweep? */
checkSizes(L);
g->gcstate = GCSfinalize; /* end sweep phase */
}
lua_assert(old >= g->totalbytes);
g->estimate -= old - g->totalbytes;
return GCSWEEPMAX*GCSWEEPCOST;
}
對sweepgc鏈表上所有元素進行回收,此時的回收調用的sweeplist,對於GCSsweepstring(字符串回收階段)時調用的是sweepwholelist,sweepwholelist最終執行的還是sweeplist。
當sweepgc鏈表上所有元素都被回收完之後,系統把GC狀態修改爲:GCSfinalize。
階段5:GCSfinalize(終止階段)
標記掃描清除都做完了,一個完整的GC差不多了,由GCSfinalize進行收尾,對tmudata鏈表進行處理,處理完畢以後則把GC狀態切到GCSpause,一次GC就結束了,等待下一次的GC開始。
case GCSfinalize: {
if (g->tmudata) {
GCTM(L);
if (g->estimate > GCFINALIZECOST)
g->estimate -= GCFINALIZECOST;
return GCFINALIZECOST;
}
else {
g->gcstate = GCSpause; /* end collection */
g->gcdept = 0;
return 0;
}
}
在有限的篇幅裏,主要講解的是GC主體邏輯,沒有提及的到請讀者深入探究,如果有誤區,請讀者不吝賜教。