0%

Lua的gc机制

基本概述

在Lua中会自动帮你管理内存,通过gc(垃圾收集器)来收集所有不可能再访问到的死对象,因此开发者们不必去操心该如何释放对象所占用的内存。而Lua中目前适用于自动垃圾回收的类型包括: stringtableuserdatafunctionthread 等。

Lua所采用的gc算法叫做 标记&回收算法 。其原理是在每一次gc时会首先扫描并标记系统中的所有对象,被扫描到并标记的对象认为可达的,在gc中并不会回收,反之则会在gc中被回收释放掉。

早期的Lua采用的是双色标记回收算法,即系统中的对象只支持两种状态:blackwhite 。每个新创建的对象的颜色为白色,若其他地方存在该对象的引用,则将其标记为黑色,在进入gc的回收阶段时,会将白色的对象回收释放掉。这个算法存在一个比较大的缺陷,整个gc过程必须保证是原子性的不能被打断。假设在遍历对象链表标记对象颜色的时候中断,而此时新增了一个对象,这个对象的颜色属性很难确定。如果该新增对象标记为白色,而gc到了回收阶段,这个对象就会在没有遍历其关联对象的情况下被回收;如果标记为黑色,那么进入回收阶段后,该对象在没有被扫描的情况下就认为是不必回收的。因此,标记阶段和回收阶段必须合在一起完成,不能被打断。如此一来,gc的代价也会显得异常昂贵。

在之后的Lua 5.1版本中改进了这个算法,增加了新的颜色状态 : gray

相比之前,每个对象多了一种颜色状态,从而保证了gc过程可以是增量的,能够被中断后继续进行。这三种颜色分类如下:

  • white : 当前对象还没有被gc访问标记过,新增对象的初始状态,如果一个对象在标记过程后仍为白色,则表示该对象没有被系统中的其他对象所引用,可以回收;
  • gray : 表示该对象已经被gc访问过,但是该对象引用的其他对象还没有被访问到;
  • black : 表示该对象已经被gc访问过,并且该对象引用的其他对象也已经被访问过。

如此一来,只要存放灰色对象的链表不为空,标记过程就会继续下去,直到gray链表清空,表示所有对象都被扫描过了。但即使新增了灰色中间状态,仍然存在一个问题。假如在标记过程结束后gc中断,新建了一个对象,此时它的状态应该是白色,但紧接着的回收阶段,会将这个没有被扫描标记的对象认为是没有被引用的对象而回收释放掉。为解决这个问题,Lua又细化出了“双白色”的概念,即将 white 细分为 currentwhite 和 otherwhite 。每次gc会使用其中的一种 white ,而下次gc则使用另外一个,交替递推。这样在回收阶段,会判断相应对象的 white 是否和当前gc的 white 是否相同,如果相同则会让这个对象等到下次gc中扫描回收,不同则将其回收。

流程分析

接下来结合Lua 5.3.2的源代码,进行gc流程的相关分析,其中gc的入口函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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);
}

结合代码和函数注释,我们不难发现,该函数执行了一套完整的gc周期。其中gc包括两种种类,KGC_NORMAL和KGC_ENMERGENCY。在根据参数 isenergency 设置好gc状态之后,会调用 keepinvariant 来判断当前的gc阶段状态,如果仍处于标记阶段,则调用 entersweep(L) 进入扫描初级阶段(GCSswpallgc),调用 sweeptolive(L, &g->allgc, &n) 并将其返回值赋给全局状态机的 sweepgc 字段,表示当前扫描链表的位置。

接下来会连续调用四遍 luaC_runtilstate() 函数,该函数会根据你传入的状态标识位与当前全局状态机的gcstate进行按位与运算,不同则会调用 singlestep(L) 函数。该函数里是一个大的switch语句,根据gcstate进行划分,我们可以很清晰地看到gc过程的8个阶段。下面我们来分别介绍一下这些阶段状态的含义:

  • GCSpause : gc前的初始化状态,global_State中的相关链表置成NULL,标记主线程的全局表、注册表等
  • GCSpropagate : 标记阶段,将gray链表的元素标记为黑色并移除,如果gray链表为空则置换状态为GCSatomic
  • GCSatomic : 标记阶段的原子操作,不可中断
  • GCSswpallgc : 扫描回收常规对象
  • GCSswpfinobj : 扫描回收被终止器标记的对象
  • GCSswptobefnz : 扫描回收带有gc回调的udata对象
  • GCSswpend : 扫描回收阶段结束阶段,扫描主线程、缩减字符串池大小
  • GCScallfin : tobefnz不为NULL并且非紧急模式,则调用gc回调(通过GCTM),否则将状态置成GCSpause

释放流程

sweeplist 函数中会判断如果不是当前gc过程中对应white的对象,会调用 freeobj() 函数,并将该对象节点移除链表。具体的freeobj()代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
static void freeobj (lua_State *L, GCObject *o) {
switch (o->tt) {
case LUA_TPROTO: luaF_freeproto(L, gco2p(o)); break;
case LUA_TLCL: {
freeLclosure(L, gco2lcl(o));
break;
}
case LUA_TCCL: {
luaM_freemem(L, o, sizeCclosure(gco2ccl(o)->nupvalues));
break;
}
case LUA_TTABLE: luaH_free(L, gco2t(o)); break;
case LUA_TTHREAD: luaE_freethread(L, gco2th(o)); break;
case LUA_TUSERDATA: luaM_freemem(L, o, sizeudata(gco2u(o))); break;
case LUA_TSHRSTR:
luaS_remove(L, gco2ts(o)); /* remove it from hash table */
luaM_freemem(L, o, sizelstring(gco2ts(o)->shrlen));
break;
case LUA_TLNGSTR: {
luaM_freemem(L, o, sizelstring(gco2ts(o)->u.lnglen));
break;
}
default: lua_assert(0);
}
}

由代码可知,该函数会根据待释放回收的对象类型进行对应操作,具体分析将在之后的针对Lua类型说明以及内存管理相关的博客中具体展开。

您的赞赏是我前进的动力

欢迎关注我的其它发布渠道