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主体逻辑,没有提及的到请读者深入探究,如果有误区,请读者不吝赐教。