首页 > 代码库 > lua_gc 源码学习二
lua_gc 源码学习二
普及下常识:GC 是 garbage collector 资源回收器;
初期的 Lua GC 采取的是 stop the world 的实现。一旦产生 gc 就需要期待全部 gc 流程走完。lua 自己是个很精简的体系,但不代表处理的数据量也必然很小。
从 Lua 5.1 入手下手,GC 的实现改成分步的。固然照旧是 stop the world ,可是,每个步骤均可以分阶段执行。这样,屡次搁浅的时间较小。随之,这部门的代码也相对于纷乱了。分步执行最关键的问题是需要处理在 GC 的步骤之间,如果数据关联的状态发生了变化,如何保证 GC 的准确性。GC 的分步执行相对一次执行完,总的时候开销的不同并非零代价的。只是在实现上,要尽可能让额外增长的价钱较小。
先来看 GC 流程的分别。
lua 的 GC 分为五个大的阶段。GC 处于哪一个阶段(代码中被称为状态),根据的是global_State中的 gcstate 域。状态以宏式样界说在 lgc.h 的 14 行。
/*** Possible states of the Garbage Collector*/#define GCSpause 0#define GCSpropagate 1#define GCSsweepstring 2#define GCSsweep 3#define GCSfinalize 4
状态的值的巨细也表示着它们的履行顺序。需要注重的是,GC 的执行历程并不是每程序都堵塞在一个状况上。
GCSpause 阶段是每一个 GC 流程的启始步调。仅仅标识表记标帜体系的根节点。见 lgc.c 的 561 行。
switch (g->gcstate)
{ case GCSpause: markroot(L); /* start a new collection */ return 0;
markroot 这个函数所做之事,就是标记主线程对象,标记主线程的局部表、注册表,以及为局部范例注册的元表。标记的详细过程我们背面再讲。
GCSpause 阶段执行完,立即就将状态切换到了 GCSpropagate 。这是一个标记流程。这个流程会分步结束。当检测到另有对象待标记时,迭代标记(重复调用 propagatemark);终究,会有一个标记功程不可被打断,这些操纵放在一个叫作 atomic 的函数中执行。见 lgc.c 的 565 行:
case GCSpropagate:
{ if (g->gray) return propagatemark(g); else /* no more `gray‘ objects */ atomic(L); /* finish mark phase */ return 0;}
这里大概需要顺带提一下的是 gray 域。望文生义,它指 GCObject 中的灰色节点链。作为灰色,即处于白色和黑色之间的状态。对于节点的颜色,顿时就会阐明。
接下来就是清除流程了。
前面我们提到过,string 在 Lua 中是单独经管的,以是也需要单独清除。GCSsweepstring 阶段干的就是这个事。string table 以 hash 表形式治理所有的 string 。GCSsweepstring 中,每个步骤(step) 清理 hash 表的一列。代码见 lgc.c 的 573 行
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;
这里可以看到 estimate 和 totalbytes 两个域,从名字上可以知道,它们别离暗示了 lua vm 占用的内存字节数以及实际分派的字节数。
ps. 如果你本身实现过内存管理器,当知道内存治理自己是有分外的内存开销的。如果有需要确切控制内存数目,我偏向于连续内存管理器统计精确的内存使用环境。比如你向内存管理器索取 8 字节内存,实际的内存开销极可能是 12 字节,乃至更多。如果想做这方面的点窜,让 lua 的 gc 能更真正的反映内存实际使用情形,保举修改 lmem.c 的 76 行,luaM_realloc_函数。所有的 lua 中的内存使用改变城市经由过程这个函数。
从下面这段代码中,我们还见到了 GCSWEEPCOST 这个秘密数字。这个数字用于节制 GC 的进度。这超越了本日的话题。留待今后阐发。
接下来就是对所有未标记的别的 GCObject 做清理劳动了。即 GCSsweep 阶段。它和上面的 GCSsweepstring 近似。
最终是 GCSfinalize 阶段。假如在前方的阶段发现了需要调用 gc 元方法的 userdata 对象,将在这个阶段逐一调用。做这件工作的函数是 GCTM 。
后面已谈到过,全部具有 gc 元要领的 userdata 对象和其联系关系的数据,现实上都不会在以前的断根阶段被肃清。(因为零丁做过标志)所有的元方法挪用都是平安的。而它们的现实排除,则需比及下一次 GC 流程了。或是在lua_close被调历时消灭。
ps.lua_close其实不做完全的 gc 事情,只是简单的处置惩罚所有 userdata 的 gc 元方法,以及开释所有效到的内存。它是相对于便宜的。
接下来我们看看 GC 标记流程触及的一些观点。
简略的说,lua 以为每一个 GCObject (需要被 GC 搜集器办理的工具)都有一个色彩。一起头,一切节点都是白色的。新建立进去的节点也被默许配置为红色。
在标记阶段,可见的节点,逐个被设置为玄色。有些节点比较庞大,它会联系关系此外节点。再不处置完所相关联节点前,lua 以为它是灰色的。
节点的颜色被贮存在 GCObject 的 CommonHeader 里,放在 marked 域中。为了节俭内存,因此用位寄存。marked 是一个单字节量。统共可以储存 8 个标记。而 lua 5.1.4 用到了 7 个标记位。在 lgc.h 的 41 行,有其诠释:
/*** Layout for bit use in `marked‘ field:** bit 0 - object is white (type 0)** bit 1 - object is white (type 1)** bit 2 - object is black** bit 3 - for userdata: has been finalized** bit 3 - for tables: has weak keys** bit 4 - for tables: has weak values** bit 5 - object is fixed (should not be collected)** bit 6 - object is "super" fixed (only the main thread)*/#define WHITE0BIT 0#define WHITE1BIT 1#define BLACKBIT 2#define FINALIZEDBIT 3#define KEYWEAKBIT 3#define VALUEWEAKBIT 4#define FIXEDBIT 5#define SFIXEDBIT 6#define WHITEBITS bit2mask(WHITE0BIT, WHITE1BIT)
lua 定义了一组宏来操纵这些标记位,代码就再也不列出。只要要翻开 lgc.h 就可以很轻快的明白这些宏的函数。
白色和黑色是划分标记的。当一个对象非白非黑时,就认为它是灰色的。
为何有两个白色符号位?这是 lua 采取的一个小技能。在 GC 的标志流程完成,但清算流程还没有作完前。一旦对象间的干系产生转变,好比新增添了一个对象。这些对象的生命期是不能预知的。最安定的方法是把它们标记为不可消除的。由于清理进程结束,需要把所有对象设置回白色,便利下次清算。lua 其实是单遍扫描,处置完一个节点就重置一个节点的颜色的。简朴的把新创建出来的对象设置为黑,有可能致使它在 GC 流程竣事后,再也没机遇变回白色了。
简略的方法便是设置从第三种状态。也便是第 2 种白色。
在 Lua 中,两个白色状态是一个乒乓开关,当前需要删除 0 型白色节点时, 1 型白色节点就是被保护起来的;反之也同样。
以后的白色是 0 型仍是 1 型,见global_State的 currentwhite 域。otherwhite() 用于乒乓切换。获适当前白色状态,利用定义在 lgcc.h 77 行的宏:
#define luaC_white(g) cast(lu_byte, (g)->currentwhite & WHITEBITS)
FINALIZEDBIT 用于标记 userdata 。当 userdata 确定不被援用,则设置上这个标记。它分歧于颜色标记。因为 userdata 由于 gc 元方法的存在,释放所占内存是需要放到 gc 元方法调用以后的。这个标记可以包管元方法不会被频频调用。
KEYWEAKBIT 和 VALUEWEAKBIT 用于标记 table 的 weak 属性,无需多言。
FIXEDBIT 可以保证一个 GCObject 不会在 GC 流程中被清除。为什么要有这类状态?关键在于 lua 本身会用到一个字符串,它们有可能不被任何处所引用,但在屡次用到这个字符串时。那么,这些字符串就会被珍爱起来,设置上 FIXEDBIT ,Mac下主动杀死ssh历程并重连。
在 lstring.h 的 24 行界说有:
#define luaS_fix(s) l_setbit((s)->tsv.marked, FIXEDBIT)
能够把一个字符串配置为被庇护的。
典范的运用场所见 llex.c 的 64 行:
void luaX_init (lua_State *L) { int i; for (i=0; i<NUM_RESERVED; i++)
TString *ts="luaS_new(L," luaX_tokens[i]);
luaS_fix(ts);
reserved words are never collected * lua_assert(strlen(luaX_tokens[i])+1 <="TOKEN_LEN);" ts->tsv.reserved = cast_byte(i+1); /* reserved word */}
以及 ltm.c 的 30 行:
void luaT_init (lua_State *L) { static const char *const luaT_eventname[] = /* ORDER TM */ "__index", "__newindex", "__gc", "__mode", "__eq", "__add", "__sub", "__mul", "__div", "__mod", "__pow", "__unm", "__len", "__lt", "__le", "__concat", "__call"; int i; for (i=0; itmname[i] = luaS_new(L, luaT_eventname[i]); luaS_fix(G(L)->tmname[i]); /* never collect these names */}
以元方式为例,若是咱们操纵 lua 尺度 api 来摹拟 metatable 的行动,就不能写的和原生的 meta 机制高效。由于,当我们取到一个 table 的 key ,想知道它是否是__index时,要末我们需要挪用 strcmp 做比力;要么利用lua_pushlstring先将需要比较的 string 压入lua_State,而后再比较。
咱们晓得 lua 中值分歧的 string 同享了一个 string 工具,即 TString 地点是一概的。对照两个 lua string 的价格是很小(只需求比力一个指针),比 C 函数 strcmp 高效。但lua_pushlstring却有额外开消。它必要去计较 hash 值,盘问 hash 表 (string table) 。
lua 的 GC 算法并不做内存清算,它不会在内存中迁徙数据。实际上,如果你能确定一个 string 不会被清除,那末它的内存地点也是稳定的,这样就带来的优化空间。ltm.c 中就是如许做的。
见 lstate.c 的 93 行:
TString *tmname[TM_N]; /* array with tag-method names */
global_State中 tmname 域就干脆以 TString 指针的体例记实了所有元方法的名字。换作准则的 lua api 来做的话,凡是我们需要把这些 string 放到注册表,或情况表中,才干担保其不被 gc 清除,且可以在比较时拿到。lua 本身的完成则哄骗 FIXEDBIT 做了一步优化。
最后,我们来看看 SFIXEDBIT 。实在它的用处只要一个,就是标记主 mainthread 。也就是一概的出发点。我们调用lua_newstate返回的阿谁布局。
为甚么需要把这个布局特别看待?因为即便到lua_close的那一刻,这个构造也是不克随便清除的。我们来看看天下末日时,法式都履行了什么?见 lstate.c 的 105 行。
static void close_state (lua_State *L) global_State *g = G(L); luaF_close(L, L->stack); /* close all upvalues for this thread */ luaC_freeall(L); /* collect all objects */ lua_assert(g->rootgc == obj2gco(L)); lua_assert(g->strt.nuse == 0); luaM_freearray(L, G(L)->strt.hash, G(L)->strt.size, TString *); luaZ_freebuffer(L, &g->buff); freestack(L, L); lua_assert(g->totalbytes == sizeof(LG)); (*g->frealloc)(g->ud, fromstate(L), state_size(LG), 0);
这是lua_close的最后一个步骤。luaC_freeall将释放所有的 GCObject ,但不包罗有 SFIXEDBIT 的 mainthread 对象 。见 lgc.c 484 行
void luaC_freeall (lua_State *L) bitmask(SFIXEDBIT); /* mask to collect all elements */ sweepwholelist(L, &g->rootgc); for (i = 0; i < g->strt.size; i++) /* free all string lists */ sweepwholelist(L, &g->strt.hash[i]);
这里 FIXEDBIT 是被疏忽的,而在此以前,FIXEDBIT 被护卫着。见 lstate.c 的 153 行(lua_newstate函数):
g->currentwhite = bit2mask(WHITE0BIT, FIXEDBIT);
这么做很轻易了解,lua 天下的发源,统统根数据都放在这个对象中,要是被提早清理,背面的代码就会出题目。真实释放这个对象不是在 GC 中,而是末了那句:
lua_assert(g->totalbytes == sizeof(LG)); (*g->frealloc)(g->ud, fromstate(L), state_size(LG), 0);
顺带还 assert 了一下,最后,世界上是不是只剩下这个结构。
lua_gc 源码学习二