首页 > 代码库 > 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 源码学习二