这篇内容并不是上篇提到的关于弱表实现的分析。而是最近有位同学与我探讨了一些Lua的GC时,发现了两个以前从没注意过的智慧闪光点。
首先是luaC_fullgc的实现.
#define keepinvariant(g) ((g)->gcstate <= GCSatomic) static void entersweep (lua_State *L) { global_State *g = G(L); g->gcstate = GCSswpallgc; lua_assert(g->sweepgc == NULL); g->sweepgc = sweeplist(L, &g->allgc, 1); } void luaC_fullgc (lua_State *L, int isemergency) { global_State *g = G(L); lua_assert(g->gckind == KGC_NORMAL); if (isemergency) g->gckind = KGC_EMERGENCY; /* set flag */ if (keepinvariant(g)) { /* black objects? */ entersweep(L); /* sweep everything to turn them back to white */ } /* finish any pending sweep phase to start a new cycle */ luaC_runtilstate(L, bitmask(GCSpause)); luaC_runtilstate(L, ~bitmask(GCSpause)); /* start new collection */ luaC_runtilstate(L, bitmask(GCScallfin)); /* run up to finalizers */ /* estimate must be correct after a full GC cycle */ lua_assert(g->GCestimate == gettotalbytes(g)); luaC_runtilstate(L, bitmask(GCSpause)); /* finish collection */ g->gckind = KGC_NORMAL; setpause(g); }
在luaC_fullgc中,主要的意图是首先尽可能快的结束执行到一半的GC循环,然后从头开始一轮新的GC循环,以达到fullgc的语义。
那么怎么快速结束当前未结束的循环呢,Lua采用了一个很有智慧的方法,那就是快速进入sweep阶段(即如果当前还没有处理sweep阶段,就直接进入sweep阶段)。那么我为什么说这是一个很有智慧的方式呢,这是因为不论在整个GC状态机的任意一个状态进入sweep阶段,整个GC状态都不会出错,这是我以前没有意识到的。
先来看一下整个GC状态机全部状态:
#define GCSpropagate 0 #define GCSatomic 1 #define GCSswpallgc 2 #define GCSswpfinobj 3 #define GCSswptobefnz 4 #define GCSswpend 5 #define GCScallfin 6 #define GCSpause 7
如果将一个GC循环按照标记和清除来区分,很显然GC循环的开始在GCSpropagate, 结束在GCSpause。整个GC循环被GCSatomic状态分为标记和清除两个大阶段。
为了更清楚的看清整个GC循环,我们将每个GC循环概括为GCMark和GCSweep两个阶段,并把几个GC循环连起来看。
GCMark1, GCSweep1, GCMark2, GCSweep2,GCMark3, GCSweep3…
由于atomic函数会改变当前GC循环的白色值,因此GCSweep1,和GCMakr2阶段拥有相同的白色值,而GCSweep2,GCMark3拥有相同的白色值。
从sweeplist函数可以看出GCSweep2只会清理GCSweep1和GCMark2阶段的白色对象。并把所有存活对象都变成当前GC循环的白色值。
假如我们在执行GCMark2过程中调用了luaC_fullgc导致跳过了atomic阶段的执行,那么就不会产生更换GC循环白色值的行为,这样GCSweep1,GCMark2,GCSweep2,GCMark3都使用了相同的白色值来执行标记清除逻辑。由于GCSweep1已经清除过GCSweep0和GCMark1阶段产生(包括由黑变白)的白色对象,那么GCSweep2的作用其实只剩下“把所有在GCMark2阶段标记过的对象都变白”。相当于撤销了GCMark1的作用。而从GCMark3开始就能执行一轮全新的GC循环。
这就变相达到了最快结束当前GC循环的目的。
另一个就是luaC_barrierback的实现.
#define luaC_barrierback(L,p,v) ( \ (iscollectable(v) && isblack(p) && iswhite(gcvalue(v))) ? \ luaC_barrierback_(L,p) : cast_void(0)) void luaC_barrierback_ (lua_State *L, Table *t) { global_State *g = G(L); lua_assert(isblack(t) && !isdead(g, t)); black2gray(t); /* make table gray (again) */ linkgclist(t, g->grayagain); }
这个函数其实在上篇分析中也有提到过。但那是在GCMark阶段时分析的luaC_barrierback的作用。我从来没有注意过如果在GCSweep阶段,触发了luaC_barrierback会怎么样。
在GCSweep阶段会触发luaC_barrierback么?答案是肯定的。因为sweeplist是增量执行的,不可能一下子就把所有黑对象变白。如果这时操作黑对象,就会触发luaC_barrierback。
那么在GCSweep阶段,如果触发了luaCbarrierback,那么相关黑对象会被变成灰对象,然后被挂在g->grayagain链表上。同时如果一个对象触发了luaC_barrierback, 说明他还没有对sweeplist处理过,也就是说最终他一定会被sweeplist函数变成白色。
这时就会存在一种情况,一个对象被挂在g->grayagain链表上,但是它是白色。这就是其中的智慧闪光点,虽然一个白色对象挂在g->grayagain, 但是他不会有副作用。因为在restartcollection函数中的 g->gray = g->grayagain = NULL
语句会将整个grayagain链表清空。同时虽然白色对象的gclist字段依然有无效值,但是在下次插入gray或grayagain之前,会对此对象的gclist字段重新赋值,因此也不会有任何副作用。
纵观这两个例子可以发现,sweeplist本来是为了GCSatomic阶段之后清除对象而生,而luaC_barrierback本来也是为了在GCMark阶段保持GC循环不变式而生。但是他们在这两个场景之外,依然有良好的适用场景。这得益于良好的抽象,我有时甚至都在想,这也许已经是人类智慧的极限了。