• Share
  • Email
  • Embed
  • Like
  • Save
  • Private Content
Lua gc代码
 

Lua gc代码

on

  • 1,615 views

 

Statistics

Views

Total Views
1,615
Views on SlideShare
1,615
Embed Views
0

Actions

Likes
2
Downloads
15
Comments
0

0 Embeds 0

No embeds

Accessibility

Categories

Upload Details

Uploaded via as Microsoft Word

Usage Rights

© All Rights Reserved

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Processing…
Post Comment
Edit your comment

    Lua gc代码 Lua gc代码 Document Transcript

    • 最近发现在大数据量的 lua 环境中,GC 占据了很多的 CPU 。差不多是整个 CPU 时间的 20% 左右。希望着手改进。这样,必须先对 lua 的 gc 算法极其实现有一个详尽的理解。我之前读过 lua 的源代码,由于 lua 源码版本变迁,这个工作还需要再做一次。这次我重新阅读了 lua 5.1.4 的源代码。从今天起,做一个笔记,详细分析一下 lua 的 gc 是如何实现的。阅读代码整整花掉了我一天时间。但写出来恐怕比阅读时间更长。我会分几天写在 blog 上。Lua 采用一个简单的标记清除算法的 GC 系统。在 Lua 中,一共只有 9 种数据类型,分别为 nil 、boolean、lightuserdata、number、string、table、function、userdata 和 thread 。其中,只有 string table functionthread 四种在 vm 中以引用方式共享,是需要被 GC 管理回收的对象。其它类型都以值形式存在。但在 Lua 的实现中,还有两种类型的对象需要被 GC 管理。分别是 proto (可以看作未绑定 upvalue 的函数), upvalue (多个 upvalue 会引用同一个值)。Lua 是以 union + type 的形式保存值。具体定义可见 lobject.h 的 56 - 75 行: /*** Union of all Lua values */ typedef union { GCObject *gc; void *p; lua_Number n; int b; } Value; /*** Tagged Values*/ #define TValuefields Value value; int tt typedef struct lua_TValue { TValuefields; } TValue;我们可以看到,Value 以 union 方式定义。如果是需要被 GC 管理的对象,就以 GCObject 指针形式保存,否则直接存值。在代码的其它部分,并不直接使用 Value 类型,而是 TValue 类型。它比 Value 多了一个类型标识。 int tt 记录。 用 通常的系统中,每个 TValue 长为 12 字节。btw, 在 The implementation of Lua 5.0 中作者讨论了,在 32 位系统下,为何不用某种 trick 把 type 压缩到前 8 字节内。所有的 GCObject 都有一个相同的数据头,叫作 CommonHeader,在 lobject.h 里 43 行 以宏形式定义出来的。使用宏是源于使用上的某种便利。C 语言不支持结构的继承。#define CommonHeader GCObject *next; lu_byte tt; lu_byte marked从这里我们可以看到:所有的 GCObject 都用一个单向链表串了起来。每个对象都以 tt 来识别其类型。marked域用于标记清除的工作。
    • 标记清除算法是一种简单的 GC 算法。每次 GC 过程,先以若干根节点开始,逐个把直接以及间接和它们相关的节点都做上标记。对于 Lua ,这个过程很容易实现。因为所有 GObject 都在同一个链表上,当标记完成后,遍历这个链表,把未被标记的节点一一删除即可。Lua 在实际实现时,其实不只用一条链表维系所有 GCObject 。这是因为 string 类型有其特殊性。所有的string 放在一张大的 hash 表中。它需要保证系统中不会有值相同的 string 被创建两份。 string 是被单独管 顾理的,而不串在 GCObject 的链表中。回头来看看 lua_State 这个类型。这是写 C 和 Lua 交互时用的最多的数据类型。顾名思义,它表示了 lua vm的某种状态。从实现上来说,更接近 lua 的一个 thread 以及其间包含的相关数据(堆栈、环境等等)。事实上,一个 lua_State 也是一个类型为 thread 的 GCObject 。见其定义于 lstate.h 97 行。 /*** `per thread state */ struct lua_State { CommonHeader; lu_byte status; StkId top; /* first free slot in the stack */ StkId base; /* base of current function */ global_State *l_G; CallInfo *ci; /* call info for current function */ const Instruction *savedpc; /* `savedpc of current function */ StkId stack_last; /* last free slot in the stack */ StkId stack; /* stack base */ CallInfo *end_ci; /* points after end of ci array*/ CallInfo *base_ci; /* array of CallInfos */ int stacksize; int size_ci; /* size of array `base_ci */ unsigned short nCcalls; /* number of nested C calls */ unsigned short baseCcalls; /* nested C calls when resuming coroutine */ lu_byte hookmask; lu_byte allowhook; int basehookcount; int hookcount; lua_Hook hook; TValue l_gt; /* table of globals */ TValue env; /* temporary place for environments */ GCObject *openupval; /* list of open upvalues in this stack */ GCObject *gclist; struct lua_longjmp *errorJmp; /* current error recover point */ ptrdiff_t errfunc; /* current error handling function (stack index) */ };一个完整的 lua 虚拟机在运行时,可有多个 lua_State ,即多个 thread 。它们会共享一些数据。这些数据放在 global_State *l_G 域中。其中自然也包括所有 GCobject 的链表。
    • 所有的 string 则以 stringtable 结构保存在 stringtable strt 域。string 的值类型为 TString ,它和其它GCObject 一样,拥有 CommonHeader 。但需要注意,CommonHeader 中的 next 域却和其它类型的单向链表意义不同。它被挂接在 stringtable 这个 hash 表中。除 string 外的 GCObject 链表头在 rootgc ( lstate.h 75 行)域中。初始化时,这个域被初始化为主线程。见lstate.c 170 行,lua_newstate 函数中: g->rootgc = obj2gco(L);每当一个新的 GCobject 被创建出来,都会被挂接到这个链表上,挂接函数有两个,在 lgc.c 687 行的 void luaC_link (lua_State *L, GCObject *o, lu_byte tt) { global_State *g = G(L); o->gch.next = g->rootgc; g->rootgc = o; o->gch.marked = luaC_white(g); o->gch.tt = tt; } void luaC_linkupval (lua_State *L, UpVal *uv) { global_State *g = G(L); GCObject *o = obj2gco(uv); o->gch.next = g->rootgc; /* link upvalue into `rootgc list */ g->rootgc = o; if (isgray(o)) { if (g->gcstate == GCSpropagate) { gray2black(o); /* closed upvalues need barrier */ luaC_barrier(L, uv, uv->v); } else { /* sweep phase: sweep it (turning it into white) */ makewhite(g, o); lua_assert(g->gcstate != GCSfinalize && g->gcstate != GCSpause); } } }upvalue 在 C 中类型为 UpVal ,也是一个 GCObject 。但这里被特殊处理。为什么会这样?因为 Lua 的 GC可以分步扫描。别的类型被新创建时,都可以直接作为一个白色节点(新节点)挂接在整个系统中。但upvalue 却是对已有的对象的间接引用,不是新数据。一旦 GC 在 mark 的过程中( gc 状态为GCSpropagate ),则需增加屏障 luaC_barrier 。对于这个问题,会在以后详细展开。lua 还有另一种数据类型创建时的挂接过程也被特殊处理。那就是 userdata 。见 lstring.c 的 95 行:
    • Udata *luaS_newudata (lua_State *L, size_t s, Table *e) { Udata *u; if (s > MAX_SIZET - sizeof(Udata)) luaM_toobig(L); u = cast(Udata *, luaM_malloc(L, s + sizeof(Udata))); u->uv.marked = luaC_white(G(L)); /* is not finalized */ u->uv.tt = LUA_TUSERDATA; u->uv.len = s; u->uv.metatable = NULL; u->uv.env = e; /* chain it on udata list (after main thread) */ u->uv.next = G(L)->mainthread->next; G(L)->mainthread->next = obj2gco(u); return u; }这里并没有调用 luaC_link 来挂接新的 Udata 对象,而是直接使用的 /* chain it on udata list (after main thread) */ u->uv.next = G(L)->mainthread->next; G(L)->mainthread->next = obj2gco(u);把 u 挂接在 mainthread 之后。从前面的 mainstate 创建过程可知。mainthread 一定是 GCObject 链表上的最后一个节点(除 Udata 外)。这是因为挂接过程都是向链表头添加的。这里,就可以把所有 userdata 全部挂接在其它类型之后。这么做的理由是,所有 userdata 都可能有 gc 方法(其它类型则没有)。需要统一去调用这些 gc 方面,则应该有一个途径来单独遍历所有的 userdata 。除此之外,userdata 和其它 GCObject 的处理方式则没有区别,顾依旧挂接在整个 GCObject 链表上而不需要单独再分出一个链表。处理 userdata 的流程见 lgc.c 的 127 行 /* move `dead udata that need finalization to list `tmudata */ size_t luaC_separateudata (lua_State *L, int all) {这个函数会把所有带有 gc 方法的 userdata 挑出来,放到一个循环链表中。这个循环链表在 global_State 的tmudata 域。需要调用 gc 方法的这些 userdata 在当个 gc 循环是不能被直接清除的。所以在 mark 环节的最后,会被重新 mark 为不可清除节点。见 lgc.c 的 545 行:
    • marktmu(g); /* mark `preserved userdata */这样,可以保证在调用 gc 方法环节,这些对象的内存都没有被释放。但因为这些对象被设置了 finalized 标记(通过 markfinalized ),下一次 gc 过程不会进入 tmudata 链表,将会被正确清理。具体 userdata 的清理流程,会在后面展开解释。早期的 Lua GC 采用的是 stop the world 的实现。一旦发生 gc 就需要等待整个 gc 流程走完。如果你用 lua处理较少量数据,或是数据增删不频繁,这样做不是问题。但当处理的数据量变大时,对于实时性要求较高的应用,比如网络游戏服务器,这个代价则是不可忽略的。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 。在 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; i<TM_N; i++) { G(L)->tmname[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) { global_State *g = G(L); int i; g->currentwhite = WHITEBITS | 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 部分的代码了。我们知道,lua 对外的 API 中,一切个 gc 打交道的都通过 lua_gc 。 语言构建系统时,一般不讲设计模式。 C但模式还是存在的。若要按《设计模式》中的分类,这应该归于 Facade 模式。代码在 lapi.c 的 895 行: /*** Garbage-collection function */ LUA_API int lua_gc (lua_State *L, int what, int data) { int res = 0; global_State *g; lua_lock(L); g = G(L); switch (what) { case LUA_GCSTOP: { g->GCthreshold = MAX_LUMEM; break; } case LUA_GCRESTART: { g->GCthreshold = g->totalbytes; break; } case LUA_GCCOLLECT: { luaC_fullgc(L); break; } case LUA_GCCOUNT: { /* GC values are expressed in Kbytes: #bytes/2^10 */ res = cast_int(g->totalbytes >> 10); break; } case LUA_GCCOUNTB: { res = cast_int(g->totalbytes & 0x3ff); break; } case LUA_GCSTEP: { lu_mem a = (cast(lu_mem, data) << 10); if (a <= g->totalbytes) g->GCthreshold = g->totalbytes - a; else
    • g->GCthreshold = 0; while (g->GCthreshold <= g->totalbytes) { luaC_step(L); if (g->gcstate == GCSpause) { /* end of cycle? */ res = 1; /* signal it */ break; } } break; } case LUA_GCSETPAUSE: { res = g->gcpause; g->gcpause = data; break; } case LUA_GCSETSTEPMUL: { res = g->gcstepmul; g->gcstepmul = data; break; } default: res = -1; /* invalid option */ } lua_unlock(L); return res; }从代码可见,对内部状态的访问,都是直接访问 global state 表的。 控制则是调用内部 api 。 中对外的 GC luaapi 和内部模块交互的 api 都是分开的。这样层次分明。内部子模块一般名为 luaX_xxx X 为子模块代号。对于收集器相关的 api 一律以 luaC_xxx 命名。这些 api 定义在 lgc.h 中。此间提到的 api 有两个: LUAI_FUNC void luaC_step (lua_State *L); LUAI_FUNC void luaC_fullgc (lua_State *L);用于分步 GC 已经完整 GC 。另一个重要的 api 是: #define luaC_checkGC(L) { condhardstacktests(luaD_reallocstack(L, L->stacksize - EXTRA_STACK - 1)); if (G(L)->totalbytes >= G(L)->GCthreshold)
    • luaC_step(L); }它以宏形式定义出来,用于自动的 GC 。如果我们审查 lapi.c ldo.c lvm.c ,会发现大部分会导致内存增长的api 中,都调用了它。保证 gc 可以随内存使用增加而自动进行。这里插几句。使用自动 gc 会有一个问题。它很可能使系统的峰值内存占用远超过实际需求量。原因就在于,收集行为往往发生在调用栈很深的地方。当你的应用程序呈现出某种周期性(大多数包驱动的服务都是这样)。在一个服务周期内,往往会引用众多临时对象,这个时候做 mark 工作,会导致许多临时对象也被 mark 住。一个经验方法是,调用 LUA_GCSTOP 停止自动 GC。在周期间定期调用 gcstep 且使用较大的 data 值,在有限个周期做完一整趟 gc。另 condhardstacktests 是一个宏,通常是不开启的。先来看 luaC_fullgc 。它用来执行完整的一次 gc 动作。fullgc 并不是仅仅把当前的流程走完。因为之前的 gc 行为可能执行了一半,可能有一些半路加进来的需要回收的对象。所以在走完一趟流程后,fullgc 将阻塞着再完整跑一遍 gc 。整个流程有一些优化的余地。即,前半程的 gc 流程其实不必严格执行,它并不需要真的去清除什么。只需要把状态恢复。这个工作是如何做到的呢?见 lgc.c 的 637 行: void luaC_fullgc (lua_State *L) { global_State *g = G(L); if (g->gcstate <= GCSpropagate) { /* reset sweep marks to sweep all elements (returning them to white) */ g->sweepstrgc = 0; g->sweepgc = &g->rootgc; /* reset other collector lists */ g->gray = NULL; g->grayagain = NULL; g->weak = NULL; g->gcstate = GCSsweepstring; } lua_assert(g->gcstate != GCSpause && g->gcstate != GCSpropagate); /* finish any pending sweep phase */ while (g->gcstate != GCSfinalize) { lua_assert(g->gcstate == GCSsweepstring || g->gcstate == GCSsweep); singlestep(L); }比较耗时的 mark 步骤被简单跳过了(如果它还没进行完的话)。和正常的 mark 流程不同,正常的 mark 流程最后,会将白色标记反转。见 lgc.c 548 行,atomic 函数: /* flip current white */
    • g->currentwhite = cast_byte(otherwhite(g));在 fullgc 的前半程中,直接跳过了 GCSpropagate ,重置了内部状态,但没有翻转白色标记。这会导致后面的sweep 流程不会真的释放那些白色对象。sweep 工作实际做的只是把所有对象又重新设置回白色而已。接下来就是一个完整不被打断的 gc 过程了。 markroot(L); while (g->gcstate != GCSpause) { singlestep(L); } setthreshold(g);从根开始 mark ,直到整个 gc 流程执行完毕。最后,重新设置了 GCthreshold 。注:调用 fullgc 会重置GCthreshold ,所以如果你曾经调用 LUA_GCSTOP 暂停自动 GC 的话(也是通过修改 GCthreshold 实现) ,记得再调用一次。stepgc 要相对复杂一些。在 lua 手册的 2.10 解释了 garbage-collector pause 和 step multiplier 的意义,却没有给出精确定义。lua_gc 的说明里,也只说“LUA_GCSTEP: 发起一步增量垃圾收集。步数由 data 控制(越大的值意味着越多步),而其具体含义(具体数字表示了多少)并未标准化。如果你想控制这个步数,必须实验性的测试 data 的值。如果这一步结束了一个垃圾收集周期,返回返回 1 。并没有给出准确的含义。实践中,我们也都是以经验取值。回到源代码,我们就能搞清楚它们到底是什么了。 case LUA_GCSETPAUSE: { res = g->gcpause; g->gcpause = data; break; } case LUA_GCSETSTEPMUL: { res = g->gcstepmul; g->gcstepmul = data; break; }这里只是设置 gcpause gcstepmul。gcpause 实际只在 lgc.c 59 行的 setthreshold 宏中用到 #define setthreshold(g) (g->GCthreshold = (g->estimate/100) * g->gcpause)
    • 看见,GCSETPAUSE 其实是通过调整 GCthreshold 来实现的。当 GCthreshold 足够大时,luaC_step 不会被 luaC_checkGC 自动触发。事实上,GCSTOP 正是通过设置一个很大的 GCthreshold 值来实现的。 case LUA_GCSTOP: { g->GCthreshold = MAX_LUMEM; break; }gcpause 值的含义很文档一致,用来表示和实际内存使用量 estimate 的比值(放大 100 倍)。一旦内存使用量超过这个阀值,就会出发 GC 的工作。要理解 gcstepmul,就要从 lua_gc 的 LUA_GCSTEP 的实现看起。 case LUA_GCSTEP: { lu_mem a = (cast(lu_mem, data) << 10); if (a <= g->totalbytes) g->GCthreshold = g->totalbytes - a; else g->GCthreshold = 0; while (g->GCthreshold <= g->totalbytes) { luaC_step(L); if (g->gcstate == GCSpause) { /* end of cycle? */ res = 1; /* signal it */ break; } } break; }step 长度 data 被放大了 1024 倍。在 lgc.c 的 26 行,也可以看到 #define GCSTEPSIZE 1024u我们姑且可以认为 data 的单位是 KBytes ,和 lua 总共占用的内存 totalbytes 有些关系。ps. 这里 totalbytes 是严格通过 Alloc 管理的内存量。而前面提到的 estimate 则不同,它是一个估算量,比totalbytes 要小。这是因为,前面也提到过,userdata 的回收比较特殊。被检测出已经访问不到的 userdata 占用的内存并不会马上释放(保证 gc 元方法的安全调用),但 estimate 会抛去这部分,不算在实际内存使用量内。见 lgc.c 544 行
    • udsize = luaC_separateudata(L, 0); /* separate userdata to be finalized */以及 lgc.c 553 行 g->estimate = g->totalbytes - udsize; /* first estimate */从代码逻辑,我们暂时可以把 data 理解为,需要处理的字节数量(以 K bytes 为单位)。如果需要处理的数据量超过了 totalbytes ,自然就可以把 GCthreshold 设置为 0 了。实际上不能完全这么理解。因为 GC 过程并不是一点点回收内存,同时可用内存越来越多。GC 分标记(mark)清除(sweep) 调用 userdata 元方法等几个阶段。只有中间的清除阶段是真正释放内存的。所以可用内存的增加( totalbytes 减少)过程,时间上并不是线性的。通常标记的开销更大。为了让 gcstep 的每个步骤消耗的时间更平滑,就得有手段动态调整 GCthreshold 值。它和 totalbytes 最终影响了每个 step 的时间。下面的关注焦点转向 luaC_step ,见 lgc.c 的 611 行: void luaC_step (lua_State *L) { global_State *g = G(L); l_mem lim = (GCSTEPSIZE/100) * g->gcstepmul; if (lim == 0) lim = (MAX_LUMEM-1)/2; /* no limit */ g->gcdept += g->totalbytes - g->GCthreshold; do { lim -= singlestep(L); if (g->gcstate == GCSpause) break; } while (lim > 0); if (g->gcstate != GCSpause) { if (g->gcdept < GCSTEPSIZE) g->GCthreshold = g->totalbytes + GCSTEPSIZE; /* - lim/g->gcstepmul;*/ else { g->gcdept -= GCSTEPSIZE; g->GCthreshold = g->totalbytes; } } else { lua_assert(g->totalbytes >= g->estimate); setthreshold(g); } }
    • 从代码我们可以看到,GC 的核心其实在于 singlestep 函数。luaC_step 每次调用多少次 singlestep 跟gcstepmul 的值有关。如果是自动进行的 GC ,当 totalbytes 大于等于 GCthreshold 时,就会触发 luaC_step 。每次 luaC_step ,GCthreshold 都会被调高 1K (GCSTEPSIZE) 直到 GCthreshold 追上 totalbytes 。这个追赶过程通常发生在 mark 流程。因为这个流程中,totalbytes 是只增不减的。如果是手控 GC ,每次 gcstep 调用执行多少次 luaC_step 则跟 data 值有关。大体上是 1 就表示一次(在mark 过程中就是这样)到了 sweep 流程就不一定了。这和 singlestep 调用次数,即 gcstepmul 的值有关。它影响了 totalbytes 的减小速度。所以,一两句话很难严格定义出这些控制 GC 步进量的参数的含义,只能慢慢阅读代码,看看实现了。在 lua 手册的 2.10 这样描述“step multiplier 控制了收集器相对内存分配的速度。 更大的数字将导致收集器工作的更主动的同时,也使每步收集的尺寸增加。 小于 1 的值会使收集器工作的非常慢,可能导致收集器永远都结束不了当前周期。 缺省值为 2 ,这意味着收集器将以内存分配器的两倍速运行。”从代码看,这绝非严格定义。至少从今天已经分析的代码中还看不出这一点。gcstepmul 的值和内存增涨速度如何产生联系?明天再写 :)今天来看一下 mark 过程是怎样实现的。所有的 GC 流程,都从 singlestep 函数开始。singlestep 就是一个最简单的状态机。 状态简单的从一个状态 GC切换到下一个状态,循环不止。状态标识放在 global state 的 gcstate 域中。这一点前面谈过。开始的两个状态和 mark 过程有关。初始的 GCSpause 状态下,执行 markroot 函数。我们来看一下 markroot 的代码。见 lgc.c 的 501 行。 /* 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; }主要是把状态复位的操作以及标记根节点。
    • lua 采用是 Tri-colour marking GC 算法 。需要维护一个灰色节点集。这里用 gary 存放。grayagain 保存着需要原子操作标记的灰色节点。weak 保存着需要清理的 weak 表。这里的 markobject 和 markvalue 都是用宏实现的。区别仅在于操作的是 GCObject 还是 TValue 。最终都是通过调用 reallymarkobject 实现的。markmt 也是直接 mark 的 global state 中的若干元表。这些元表作用于基本的几个数据类型。这些加起来成为整个数据的根。对 lua 语言有所了解的话,不难理解这几行实现。核心的实现见 lgc.c 的 69 行 static void reallymarkobject (global_State *g, GCObject *o) { lua_assert(iswhite(o) && !isdead(g, o)); white2gray(o); switch (o->gch.tt) { case LUA_TSTRING: { return; } case LUA_TUSERDATA: { Table *mt = gco2u(o)->metatable; gray2black(o); /* udata are never gray */ if (mt) markobject(g, mt); markobject(g, gco2u(o)->env); return; } case LUA_TUPVAL: { UpVal *uv = gco2uv(o); markvalue(g, uv->v); if (uv->v == &uv->u.value) /* closed? */ gray2black(o); /* open upvalues are never black */ return; } case LUA_TFUNCTION: { gco2cl(o)->c.gclist = g->gray; g->gray = o; break; } case LUA_TTABLE: { gco2h(o)->gclist = g->gray; g->gray = o; break; } case LUA_TTHREAD: { gco2th(o)->gclist = g->gray; g->gray = o; break;
    • } case LUA_TPROTO: { gco2p(o)->gclist = g->gray; g->gray = o; break; } default: lua_assert(0); } }它按 GCObject 的实际类型来 mark 它。reallymarkobject 的时间复杂度是 O(1) 的,它不会递归标记相关对象,虽然大多数 GCObject 都关联有其它对象。保证 O(1) 时间使得标记过程可以均匀分摊在逐个短小的时间片内,不至于停止世界太久。这里就需要用到三色标记法。reallymarkobject 进入时,先把对象设置为灰色(通过 white2gray 这个宏)。然后再根据具体类型,当一个对象的所有关联对象都被标记后,再从灰色转为黑色。因为 TSTRING 一定没有关联对象,而且所有的字符串都是统一独立处理的。这里可以做一个小优化,不需要设置为黑色,只要不是白色就可以清理。所以此处不必染黑。但 TUSERDATA 则不同,它是跟其它对象一起处理的。标记 userdata 就需要调用 gray2black 了。另外,还需要标记 userdata 的元表和环境表。TUPVAL 是一个特殊的东西。 lua 编程,以及写 C 代码和 lua 交互时,都看不到这种类型。 在 它用来解决多个closure 共享同一个 upvalue 的情况。实际上是对一个 upvalue 的引用。问什么 TUPVAL 会有 open 和 closed两种状态?应该这样理解。当一个 lua 函数本执行的时候,和 C 语言不一样,它不仅可以看到当前层次上的 local 变量,还可以看到上面所有层次的 local 变量。这个可见性是由 lua 解析器解析你的 lua 代码时定位的(换句话说,就是在“编译”期决定的)。那些不属于你的函数当前层次上的 local 变量,就称之为 upvalue 。upvalue 这个概念是由parser 引入的。在 Lua 中,任何一个 function 其实都是由 proto 和运行时绑定的 upvalue 构成的。proto 将如何绑定 upvalue 是在 parser 生成的 bytecode 里描述清楚了的。如果对这一块的实现代码有兴趣,可以参考 lvm.c 的 719 行: case OP_CLOSURE: { Proto *p; Closure *ncl; int nup, j; p = cl->p->p[GETARG_Bx(i)]; nup = p->nups; ncl = luaF_newLclosure(L, nup, cl->env); ncl->l.p = p;
    • for (j=0; j<nup; j++, pc++) { if (GET_OPCODE(*pc) == OP_GETUPVAL) ncl->l.upvals[j] = cl->upvals[GETARG_B(*pc)]; else { lua_assert(GET_OPCODE(*pc) == OP_MOVE); ncl->l.upvals[j] = luaF_findupval(L, base + GETARG_B(*pc)); } } setclvalue(L, ra, ncl); Protect(luaC_checkGC(L)); continue; }我们会看到,调用 luaF_newLclosure 生成完一个 Lua Closure 后,会去填那张 upvalue 表。 upvalue 尚在 当堆栈上时,其实是调用 luaF_findupval 去生成一个对堆栈上的特定值之引用的 TUPVAL 对象的 。luaF_findupval 的实现不再列在这里,它的主要作用就是保证对堆栈相同位置的引用之生成一次。生成的这个对象就是 open 状态的。所有 open 的 TUPVAL 用一个双向链表串起来,挂在 global state 的 uvhead 中。一旦函数返回,某些堆栈上的变量就会消失,这时,还被某些 upvalue 引用的变量就必须找个地方妥善安置。这个安全的地方就是 TUPVAL 结构之中。修改引用指针的结果,就被认为是 close 了这个 TUPVAL 。相关代码可以去看 lfunc.c 中 luaF_close 的实现。走题太多。现在回来看 TUPVAL 的 mark 过程。mark upvalue 引用的对象之后,closed TUPVAL 就可以置黑了为何 open 状态的 TUPVAL 需要留为灰色待处理呢?这是因为 open TUPVAL 是易变的。GC 分步执行,我们无法预料在 mark 流程走完前,堆栈上被引用的数据会不会发生变化。事实上,在 mark 的最后一个步骤,我们会看到所有的 open TUPVAL 被再次 mark 一次。做这件事情的函数是 lgc.c 516 行的: static void remarkupvals (global_State *g) { UpVal *uv; for (uv = g->uvhead.u.l.next; uv != &g->uvhead; uv = uv->u.l.next) { lua_assert(uv->u.l.next->u.l.prev == uv && uv->u.l.prev->u.l.next == uv); if (isgray(obj2gco(uv))) markvalue(g, uv->v); } }其它的数据类型处理都很简单。把自己挂在 gray 链表上即可。这条链表将在后面的 GCSpropagate 步骤被处理到。GC 状态为 GCSpropagate 时,singlestep 就是调用 propagatemark 处理 global state 中的 gray 链。
    • propagatemark 内部的细节分析得等到下一篇了。从总体看来,propagatemark 标记了各种 GCObject 的直接相关级的对象。到这里,我们可以回答昨天遗留下来的一个问题:GC 的分步过程是如何控制进度的。从前面分析过的代码上来,singlestep 的返回值决定了 GC 的进度。在 GC 进行过程中,如何知道做完整个GC 流程的时间,以及目前做的进度呢?我们可以依靠的数据只有内存管理器辖下的内存数量。大致上,整个 GC 需要的总时间和 GCObject 的数量成正比。但每种类型的 GCObject 的处理时间复杂度却各不相同。仔细衡量每种类型的处理时间差别不太现实,它可能和具体机器也有关系。但我们却可以大体上认为,占用内存较多的对象,处理时间也更长一点。这里不包括 string 和 userdata 。它们即使占用内存较多,但却没有增加 mark 的时间。虽然不太精确,但对 table,function ,thread 这些类型来说,这种估算方式却比较接近。所以在 propagatemark 过程中,每 mark 一个灰色节点,就返回了它实际占用的内存字节数作为 quantity值。如果回顾昨天看过的代码。 l_mem lim = (GCSTEPSIZE/100) * g->gcstepmul; if (lim == 0) lim = (MAX_LUMEM-1)/2; /* no limit */ g->gcdept += g->totalbytes - g->GCthreshold; do { lim -= singlestep(L); if (g->gcstate == GCSpause) break; } while (lim > 0);我们就能理解,当 gcstepmul 为 100 时,每次由 luaC_checkGC 满足条件后调用一次 luaC_step ,大致就会标记 1K 的数据(当处于 mark 环节中时)。而每次 luaC_step 会递增 GCthreshold 1K 。 if (g->gcstate != GCSpause) { if (g->gcdept < GCSTEPSIZE) g->GCthreshold = g->totalbytes + GCSTEPSIZE; /* - lim/g->gcstepmul;*/ else { g->gcdept -= GCSTEPSIZE; g->GCthreshold = g->totalbytes; } }依赖自动 GC ,只有 totalbytes 持续增长才会驱动 luaC_step 不断工作下去的。但增长速度如果快于 mark速度,那么就会产生问题。这正是 lua 手册中所警告的:“step multiplier 控制了收集器相对内存分配的速度。更大的数字将导致收集器工作的更主动的同时,也使每步收集的尺寸增加。 小于 1 的值会使收集器工作的非常慢,可能导致收集器永远都结束不了当前周期。”
    • 我们也可以看到,自动 GC 过程处于 sweep 流程时,收集器也并不那么急于释放内存了。因为释放内存会导致 totalbytes 减少,如果没有新的对象分配出来,totalbytes 是不会增加到触发新的 luaC_step 的。手动分步 GC 则是另一番光景。lua_gc GCSTEP 使用的 data 值,在处于 mark 流程时,便可以认为扫描的内存字节数乘上 step multiplier 这个系数。当然这只是大致上个估计。毕竟内存不是理想中的逐字节扫描的。等待所有灰色节点都处理完,我们就可以开始清理那些剩下的白色节点了吗?不,因为 mark 是分步执行的,中间 lua 虚拟机中的对象关系可能又发生的变化。所以在开始清理工作前,我们还需要做最后一次扫描。这个过程不可以再被打断。让我们看看相关的处理函数。见 lgc.c 526 行: 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 */ }代码中注释的很详细。propagateall 是用来迭代所有的灰色节点的。所以每个关键步骤结束后,都需要调用一下保证处理完所有可能需要标记的对象。第一个步骤是那些 open 的 TUPVAL ,在前面已经提及。然后是处理弱表。
    • 弱表是个比较特殊的东西,这里,弱表需要反复 mark ,跟 write barrier 的行为有关,超出了今天的话题 。write barrier 还会引起另一些对象再 mark ,即对 grayagain 链的处理。都留到明天再谈。接下来处理 userdata 的部分,luaC_separateudata 之前已经解释过了。所以的一切都标记完后,那些不被引用的对象就都被分离出来了。可以安全的清理所有尚被引用的弱表 。cleartable 就是干的这件事情。实现没有什么好谈的,一目了然。这里也不列代码了。不过值得一提的是对可以清理的项的判定函数:iscleared 。见 lgc.c 的 329 行。 /* ** The next function tells whether a key or value can be cleared from ** a weak table. Non-collectable objects are never removed from weak ** tables. Strings behave as `values, so are never removed too. for ** other objects: if really collected, cannot keep them; for userdata ** being finalized, keep them in keys, but not in values */ static int iscleared (const TValue *o, int iskey) { printf("is cleared %p,%dn", o,iskey); if (!iscollectable(o)) return 0; if (ttisstring(o)) { stringmark(rawtsvalue(o)); /* strings are `values, so are never weak */ return 0; } return iswhite(gcvalue(o)) || (ttisuserdata(o) && (!iskey && isfinalized(uvalue(o)))); }如果你对 for userdata being finalized, keep them in keys, but not in values 这一句表示困惑的话。可以读读 Roberto 对此的解释 。和那些被清理掉的 userdata 一样。当 userdata 被标为白色,在下一个 GC 周期中,它便不再认为是isfinalized 状态。对应弱表中的 k/v 就被真的被清除掉了。来说说 write barrier 。在 GC 的扫描过程中,由于分步执行,难免会出现少描了一半时,那些已经被置黑的对象又被修改,需要重新标记的情况。这就需要在改写对象时,建立 write barrier 。在扫描过程中触发 writebarrier 的操作影响的对象被正确染色,或是把需要再染色的对象记录下来,留到 mark 的最后阶段 atomic完成。和 barrier 相关的 API 有四个,定义在 lgc.h 86 行: #define luaC_barrier(L,p,v) { if (valiswhite(v) && isblack(obj2gco(p))) luaC_barrierf(L,obj2gco(p),gcvalue(v)); } #define luaC_barriert(L,t,v) { if (valiswhite(v) && isblack(obj2gco(t)))
    • luaC_barrierback(L,t); } #define luaC_objbarrier(L,p,o) { if (iswhite(obj2gco(o)) && isblack(obj2gco(p))) luaC_barrierf(L,obj2gco(p),obj2gco(o)); } #define luaC_objbarriert(L,t,o) { if (iswhite(obj2gco(o)) && isblack(obj2gco(t))) luaC_barrierback(L,t); }luaC_barrier 和 luaC_objbarrier 功能上相同,只不过前者是针对 TValue 的,后者则针对 GCObject 。它用于把 v 向 p 关联时,当 v 为白色且 p 为黑色时,调用 luaC_barrierf 。luaC_barriert 和 luaC_objbarriert 则是用于将 v 关联到 t 时,调用 luaC_barrierback 。当然前提条件也是 v为白色,且 t 为黑色。为何 table 要被特殊对待?因为 table 的引用关系是 1 对 N ,往往修改同一个 table 是很频繁的。而其它对象之间则是有限的联系。分开处理可以减少 barrier 本身的开销。luaC_barrierf 用于把新建立联系的对象立刻标记。它的实现在 lgc.c 的 663 行: void luaC_barrierf (lua_State *L, GCObject *o, GCObject *v) { global_State *g = G(L); lua_assert(isblack(o) && iswhite(v) && !isdead(g, v) && !isdead(g, o)); lua_assert(g->gcstate != GCSfinalize && g->gcstate != GCSpause); lua_assert(ttype(&o->gch) != LUA_TTABLE); /* must keep invariant? */ if (g->gcstate == GCSpropagate) reallymarkobject(g, v); /* restore invariant */ else /* dont mind */ makewhite(g, o); /* mark as white just to avoid other barriers */ }简而言之,当 GC 处于 GCSpropagate 状态(mark 流程中),就标记 v 。否则,把与之联系的节点 o 从黑色置为白色。前面我们已经说过,lua 的 GC 采用了两种白色标记,乒乓切换。 mark 流程结束后,白色状态 在被切换。这个时候调用 makewhite 并不会导致 sweep 过程清除这个节点。同时,由于 o 变为白色,就可以减少 barrier 的开销。即,再有对 o 的修改,不会产生 luaC_barrierf 的函数调用了。luaC_barrierback 会把要修改的 table 退回 gary 集合,留待将来继续标记。见代码 lgc.c 的 676 行: void luaC_barrierback (lua_State *L, Table *t) { global_State *g = G(L); GCObject *o = obj2gco(t);
    • lua_assert(isblack(o) && !isdead(g, o)); lua_assert(g->gcstate != GCSfinalize && g->gcstate != GCSpause); black2gray(o); /* make table gray (again) */ t->gclist = g->grayagain; g->grayagain = o; }这里提到了 grayagain 链,昨天已经看到,这个链表会推迟到 atomic 阶段处理。把 table 设会灰色可使后续对其的修改不再触发 luaC_barrierback 。注意,对弱表的修改是不会触发 luaC_barrierback 的。因为弱表不会是黑色。弱表也不能触发它。因为每个GCObject 都只能存在于一个链表上。弱表自己挂接到 weak 链,就不能向 grayagain 挂接。这就解释了 lgc.c285 行: if (traversetable(g, h)) /* table is weak? */ black2gray(o); /* keep it gray */为何要把弱表从黑色设回灰色。因为弱表又分 key weak 和 value weak ,它倒不一定完全都包含了弱引用。在扫描过程中,对弱表的修改又不会触发 barrier 。这会导致最终扫描完成后,某些弱表里可能还藏有一些新建立的强应用关系。到这里,我们便可以解释昨天遗留的另一个问题,弱表为何到了 atomic 节点需要再次 mark 。见 lgc.c 的 533 行: /* 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);再次 mark basic metatables 的原因是,设置基本类型的全局 metatable 并没有设置 write barrier (这组metatable 比较特殊,不适合用以上的 barrier 函数)。见 lapi.c 的 710 行: switch (ttype(obj)) { case LUA_TTABLE: { hvalue(obj)->metatable = mt; if (mt) luaC_objbarriert(L, hvalue(obj), mt); break;
    • } case LUA_TUSERDATA: { uvalue(obj)->metatable = mt; if (mt) luaC_objbarrier(L, rawuvalue(obj), mt); break; } default: { G(L)->mt[ttype(obj)] = mt; break; } }这里,修改 table 的 metatable 调用了 luaC_objbarriert ,修改 userdata 的 metatable 调用了 luaC_objbarrier ,修改全局 metatable 是直接进行的。如果审查所有的代码,会发现 barrier 的调用分布在很多地方。大多数情况下,barrier 的开销并不大,就是一次条件判断。但 barrier 也会被执行很频繁,性能必须有保障,这也是为什么 barrier 相关 api 都用宏来实现的缘故。理解了 write barrier 的实现,我们就可以回头来看 propagatemark 过程了。propagatemark 每次会标记一个灰色对象所有的关联节点,并把自己置为黑色。见 lgc.c 的 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) {之后就是对需要迭代标记的具体类型的处理了。对 table 的处理是这样的,见 lgc.c 的 282 行: 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); }从 gray 链上取掉当前 table 节点,然后迭代这个 table ,如果是一个弱表,则把其还原为灰色。traversetable 枚举了 table 中的所有引用项。并对弱表做了处理。见 lgc.c 的 166 行: if (mode && ttisstring(mode)) { /* is there a weak mode? */ weakkey = (strchr(svalue(mode), k) != NULL); weakvalue = (strchr(svalue(mode), v) != NULL); if (weakkey || weakvalue) { /* is really weak? */ h->marked &= ~(KEYWEAK | VALUEWEAK); /* clear bits */ h->marked |= cast_byte((weakkey << KEYWEAKBIT) | (weakvalue << VALUEWEAKBIT)); h->gclist = g->weak; /* must be cleared after GC, ... */ g->weak = obj2gco(h); /* ... so put in the appropriate list */ } }被扫描到的弱表把自己挂到了 weak 链上。单独保存一个链表是因为弱表在扫描的最后阶段还需要把里面不存在的弱引用项清理掉。关于清理工作,是由 atomic 函数中对 cleartable 调用完成的。昨天已经讲过,无庸赘述。对 function 的 mark 工作在 traverseclosure 函数中进行,lgc.c 224 行: static void traverseclosure (global_State *g, Closure *cl) { markobject(g, cl->c.env); if (cl->c.isC) { int i; for (i=0; i<cl->c.nupvalues; i++) /* mark its upvalues */ markvalue(g, &cl->c.upvalue[i]); } else { int i; lua_assert(cl->l.nupvalues == cl->l.p->nups); markobject(g, cl->l.p); for (i=0; i<cl->l.nupvalues; i++) /* mark its upvalues */ markobject(g, cl->l.upvals[i]);
    • } }lua 中 C Function 和 Lua Function 的内部结构是不同的。所以是分别独立的代码。主要就是对 upvalue 以及环境表的标记了。对 thread 的处理相对复杂一点。见 lgc.c 297 行: 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; }这里的链表操作可以简述为,把自己从 gray 链上摘下,放到 grayagain 链表中。并把自己保持为灰色状态。这是因为 thread 关联的对象其实是运行堆栈,堆栈是随着运行过程不断变化的。如果目前尚处于 mark 流程没有结束的话,后面发生变化是很有可能的。 stack 上数据的修改是不经过 write barrier 的。 而 也没有必要为最频繁的 stack 修改做 barrier ,那样对性能影响就很大了。最简单的办法就是把 thread 推迟到 atomic 阶段重扫描一次。atomic 函数,lgc.c 537 行的 markobject(g, L); /* mark running thread */也是这个用意。ps. lua 的 GC 在实现时,即使要推迟扫描,也要先做一次。都是为了 atomic 阶段尽量短,把有可能遍历的部分先遍历完。TPROTO 对象的遍历函数在 lgc.c 的 199 行: /* ** All marks are conditional because a GC may happen while the ** prototype is still being created
    • */ static void traverseproto (global_State *g, Proto *f) { int i; if (f->source) stringmark(f->source); for (i=0; i<f->sizek; i++) /* mark literals */ markvalue(g, &f->k[i]); for (i=0; i<f->sizeupvalues; i++) { /* mark upvalue names */ if (f->upvalues[i]) stringmark(f->upvalues[i]); } for (i=0; i<f->sizep; i++) { /* mark nested protos */ if (f->p[i]) markobject(g, f->p[i]); } for (i=0; i<f->sizelocvars; i++) { /* mark local-variable names */ if (f->locvars[i].varname) stringmark(f->locvars[i].varname); } }如注释所言,这里有很多的判断。这缘于 prototype 的创建过程是可能分步的,中间有可能被 GC 打断 。prototype 的创建伴随着诸多 GCObject 的创建。新对象的创建(大多数是 string)带来的是新的内存申请,很容易触发自动 GC 的进行。如果对 prototype 的创建过程有兴趣,可以去阅读 lparser.c 的 luaY_parser 函数,以及 lundump.c 中的 luaU_undump 函数。以上就是所有需要迭代遍历的数据类型了。一切准备妥当后,GC 将切换到 GCSsweepstring 。见 lgc.c 548 行: /* 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 */这时,userdata 的 gc 元方法虽未调用,所有不再有引用的 userdata 不会在这趟 GC 环节中清理出去,但它们占据的空间已经被 estimate 忽略。GC 中最繁杂的 mark 部分已经谈完了。剩下的东西很简单。今天一次可以写完。sweep 分两个步骤,一个是清理字符串,另一个是清理其它对象。看代码,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; } 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; }在 GCSsweepstring 中,每步调用 sweepwholelist 清理 strt 这个 hash 表中的一列。理想状态下,所有的string 都被 hash 散列开没有冲突,这每一列上有一个 string 。我们可以读读 lstring.c 的 68 行: if (tb->nuse > cast(lu_int32, tb->size) && tb->size <= MAX_INT/2) luaS_resize(L, tb->size*2); /* too crowded */当 hash 表中的 string 数量(nuse) 大于 hash 表的列数(size) 时,lua 将 hash 表的列数扩大一倍。就是按一列一个元素来估计的。值得一提的是,分布执行的 GC ,在这个阶段,string 对象是有可能清理不干净的。当 GCSsweepstring 步骤中,step 间若发生以上 string table 的 hash 表扩容事件,那么 string table 将被 rehash 。一些来不及清理的 string 很有可能被打乱放到已经通过 GCSsweepstring 的表列里。一旦发生这种情况,部分 string 对象则没有机会在当次 GC 流程中被重置为白色。在某些极端情况下,即使你调用 fullgc 一次也不能彻底的清除垃圾关于 string 对象,还有个小地方需要了解。lua 是复用相同值的 TString 的,且同值 string 绝对不能有两份。而 GC 的分步执行,可能会导致一些待清理的 TString 又复活。所以在它在创建新的 TString 对象时做了检查见 lstring.c 86 行: if (ts->tsv.len == l && (memcmp(str, getstr(ts), l) == 0)) { /* string may be dead */
    • if (isdead(G(L), o)) changewhite(o); return ts; }相同的问题也存在于 upvalue 。同样有类似检查。此处 GCSWEEPCOST 应该是一个经验值。前面我们知道 singlestep 返回的这个步骤大致执行的时间。这样可以让 luaC_step 这个 api 每次执行的时间大致相同。mark 阶段是按扫描的字节数确定这个值的。而每次释放一个 string 的时间大致相等(和 string 的长度无关),GCSWEEPCOST 就是释放一个对象的开销了。GCSsweep 清理的是整个 GCObject 链表。这个链表很长,所以也是分段完成的。记录遍历位置的指针是sweepgc ,每次遍历 GCSWEEPMAX 个。无论遍历是否清理,开销都是差不太多的。因为对于存活的对象,需要把颜色位重置;需要清理的对象则需要释放内存。每趟 GCSsweep 的开销势必比 GCSsweepstring 大。大致估算时间为 GCSWEEPMAX*GCSWEEPCOST 。真正的清理工作通过 lgc.c 408 行的 sweeplist 函数进行。 static GCObject **sweeplist (lua_State *L, GCObject **p, lu_mem count) { GCObject *curr; global_State *g = G(L); int deadmask = otherwhite(g); while ((curr = *p) != NULL && count-- > 0) { if (curr->gch.tt == LUA_TTHREAD) /* sweep open upvalues of each thread */ sweepwholelist(L, &gco2th(curr)->openupval); if ((curr->gch.marked ^ WHITEBITS) & deadmask) { /* not dead? */ lua_assert(!isdead(g, curr) || testbit(curr->gch.marked, FIXEDBIT)); makewhite(g, curr); /* make it white (for next cycle) */ p = &curr->gch.next; } else { /* must erase `curr */ lua_assert(isdead(g, curr) || deadmask == bitmask(SFIXEDBIT)); *p = curr->gch.next; if (curr == g->rootgc) /* is the first element of the list? */ g->rootgc = curr->gch.next; /* adjust first */ freeobj(L, curr); } } return p; }后半段比较好理解,当对象存活的时候,调用 makewhite ;死掉则调用 freeobj 。sweeplist 的起点并不是从rootgc 而是 sweepgc (它们的值可能相同),所以对于头节点,需要做一点调整。
    • 前面的 sweep open upvalues of each thread 需要做一点解释。为什么 upvalues 需要单独清理?这要从upvalue 的储存说起。upvalues 并不是放在整个 GCObject 链表中的。而是存在于每个 thread 自己的 L 中(openupval 域)。为何要这样设计?因为和 string table 类似,upvalues 需要唯一性,即引用相同变量的对象只有一个。所以运行时需要对当前 thread 的已有 upvalues 进行遍历。Lua 为了节省内存,并没有为 upvalues 多申请一个指针放置额外的链表。就借用 GCObject 本身的单向链表。所以每个 thread 拥有的 upvalues 就自成一链了。相关代码可以参考 lfunc.c 53 行处的 luaF_findupval 函数。但也不是所有 upvalues 都是这样存放的。closed upvalue 就不再需要存在于 thread 的链表中。 luaF_close 在函数中将把它移到其它 GCObject 链中。见 lfunc.c 106 行: unlinkupval(uv); setobj(L, &uv->u.value, uv->v); uv->v = &uv->u.value; /* now current value lives here */ luaC_linkupval(L, uv); /* link upvalue into `gcroot list */另,某些 upvalue 天生就是 closed 的。它们可以直接通过 luaF_newupval 构造出来。按道理来说,对 openvalues 的清理会增加单次 **sweeplist 的负荷,当记入 singlestep 的返回值。但这样会导致 sweeplist 接口变复杂,实现的代价也会增加。鉴于 thread 通常不多,GC 开销也只是一个估算值,也就没有特殊处理了。GC 的最后一个流程为 GCSfinalize 。它通过 GCTM 函数,每次调用一个需要回收的 userdata 的 gc 元方法。见 lgc.c 的 446 行: static void GCTM (lua_State *L) { global_State *g = G(L); GCObject *o = g->tmudata->gch.next; /* get first element */ Udata *udata = rawgco2u(o); const TValue *tm; /* remove udata from `tmudata */ if (o == g->tmudata) /* last element? */ g->tmudata = NULL; else g->tmudata->gch.next = udata->uv.next; udata->uv.next = g->mainthread->next; /* return it to `root list */ g->mainthread->next = o; makewhite(g, o); tm = fasttm(L, udata->uv.metatable, TM_GC); if (tm != NULL) {
    • lu_byte oldah = L->allowhook; lu_mem oldt = g->GCthreshold; L->allowhook = 0; /* stop debug hooks during GC tag method */ g->GCthreshold = 2*g->totalbytes; /* avoid GC steps */ setobj2s(L, L->top, tm); setuvalue(L, L->top+1, udata); L->top += 2; luaD_call(L, L->top - 2, 0); L->allowhook = oldah; /* restore hooks */ g->GCthreshold = oldt; /* restore threshold */ } }代码逻辑很清晰。需要留意的是,gc 元方法里应避免再触发 GC 。所以这里采用修改 GCthreshold 为比较较大值来回避。这其实不能完全避免 GC 的重入。甚至用户错误的编写代码也可能主动触发,但通常问题不大。因为最坏情况也只是把 GCthreshold 设置为一个不太正确的值,并不会引起逻辑上的错误。