Lua 源码分析之垃圾回收GC

Lua GC算法

1.概览

垃圾回收算法里,有两大类别:一是引用计数法,二是标记清除法(Mark & Sweep)。Lua 采用的是第二种,标记清除法。
在不同的版本中,Lua的GC有不一样的做法。

5.0版本的双色标记算法

在早期的5.0版本使用的是双色标记清除法(Two-Color Mark and Sweep),它的大概原理是:每一个对象的状态非黑即白,要么被引用,要么没有被引用,系统在清理过程中会把没有引用的回收掉。但是它个算法其中有一个很大的缺陷是:在回收的过程中不可以被打断,程序必须停下来为GC服务,直到GC执行完毕为止。

5.1版本的三色增量标记清除算法

由于双色标记算法对程序的影响很大,从5.1开始优化了GC算法,采用三色增量标记清除算法(全称: Dijkstra’s three-color incremental mark & sweep)。对比旧版本的算法,这个算法的好处是:GC过程可以分步执行,增量回收,被中断之后可以继续执行。具体三种颜色为:
白色:新建对象的初始状态。如果在一轮GC扫描结束后,对象还是为白色,该对象可以被进行回收了。
灰色:表示对象已经被GC扫描过,但该对象引用的对象还没被扫描到。
黑色:表示对象已经被GC扫描过,同时该对象引用的对象也被扫描过了。

三色增量标记清除算法算法过程
简单的说是:每一个对象创建后会赋予为白色,经常各种操作运算赋值等操作,被GC扫描完后如果是还是白色,就把它回收了。
它的大概过程是:

//对象创建时
把新创建的对象标记为白色

//初始化对象
遍历rootgc下所有引用的对象,从白色标记为灰色,并放到g->gray节点列表中

//标记阶段
当灰色链表中还有未扫描的对象:(g->gray会指向GCObject,通过gclist串起来,当最后一个节点指向的gclist为空的,就停止标记)
---- 从链表中取出对象并标记为黑色;
---- 遍历这个对象关联的其它所有对象:
-------- 如果是白色:
-------- 标记为灰色,并加入到灰色链表中

//回收阶段
遍历所有对象:
---- 如果为白色:
-------- 这些对象都是没有被其它对象引用的,则把它回收了
---- 否则:
-------- 重新加入对象链表中等待下一轮的GC检查

新建对象会存放在g->rootgc链表上,同时sweepgc指向rootgc:
每一个对象通过不同的字段指向不同链表。
例如某个GCObject o通过gco2th(o)->gclist链接到gray上,o->gch.next链接到rootgc上。
在这里插入图片描述
rootgc链表

2.关键代码

GC相关的数据主要存放在global_State:

//lstate.h, line:65
/*
** `global state', shared by all threads of this state
*/
typedef struct global_State {
  stringtable strt;  /* hash table for strings */
  lua_Alloc frealloc;  /* function to reallocate memory */
  void *ud;         /* auxiliary data to `frealloc' */
  lu_byte currentwhite;
  lu_byte gcstate;  /* state of garbage collector */
  int sweepstrgc;  /* position of sweep in `strt' */
  GCObject *rootgc;  /* list of all collectable objects */
  GCObject **sweepgc;  /* position of sweep in `rootgc' */
  GCObject *gray;  /* list of gray objects */
  GCObject *grayagain;  /* list of objects to be traversed atomically */
  GCObject *weak;  /* list of weak tables (to be cleared) */
  GCObject *tmudata;  /* last element of list of userdata to be GC */
  Mbuffer buff;  /* temporary buffer for string concatentation */
  lu_mem GCthreshold;
  lu_mem totalbytes;  /* number of bytes currently allocated */
  lu_mem estimate;  /* an estimate of number of bytes actually in use */
  lu_mem gcdept;  /* how much GC is `behind schedule' */
  int gcpause;  /* size of pause between successive GCs */
  int gcstepmul;  /* GC `granularity' */
  lua_CFunction panic;  /* to be called in unprotected errors */
  TValue l_registry;
  struct lua_State *mainthread;
  UpVal uvhead;  /* head of double-linked list of all open upvalues */
  struct Table *mt[NUM_TAGS];  /* metatables for basic types */
  TString *tmname[TM_N];  /* array with tag-method names */
} global_State;

与GC紧密联系的几个重要参数为(个别参数不影响整体流程,暂时不细说):
lu_byte gcstate :当前GC的状态(主要有5个状态:GCSpause(GC暂停状态,还没开始GC), GCSpropagate(遍历灰色节点阶段),GCSsweepstring(字符串回收阶段),GCSsweep(除字符串以外的回收阶段),GCSfinalize(终止阶段)
GCObject *rootgc : 待GC回收对象的链表,所有新建的GC对象会放在这里
GCObject *gray : 灰色节点的链表
GCObject *grayagain :将被进行原子性回的所有对象的链表(原子性意即不能被打断)

3.垃圾回收过程

在Lua GC的源码里,GC过程划分为5个不同阶段,以下根据5种状态来分析记录。

阶段1:GCSpause(暂停阶段)

//lgc.c, line:560

case GCSpause: {
  markroot(L);  /* start a new collection */
  return 0;
}

这个阶段,系统主要是执行markroot

  1. 把gray、grayagaint和weak清空,
  2. 把mainthread、全局表和registry表进行标记,把它们从白色标记为灰色,并且加入到gray链表中(字符串、udata和UpValue有特殊处理,暂且不讨论)
  3. 通知系统进入一下阶段:GCSpropagate(遍历灰色节点阶段)
    markroot的源码为:
    //lgc.c, line:500
/* 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;
}

阶段2:GCSpropagate(遍历灰色节点阶段)

这个阶段主要是遍历灰色链表(g->gray)和g->grayagain链表,把符合规则的灰色节点改为黑色。
灰色链表通过propagatemark入口进入遍历的,当灰色链表已经遍历完,并且灰色链表没有对象以后,则需要遍历g->grayagain,调用入口是atomic。
//lgc.c, line:564

case GCSpropagate: {
  if (g->gray)
	return propagatemark(g);
  else {  /* no more `gray' objects */
	atomic(L);  /* finish mark phase */
	return 0;
  }
}

GC在扫描过程中,大体都是一致的,把节点颜色标记为黑色。但是针对4种指定特殊的对象有单独的处理。

  1. Table对象
    如果该表是弱表,则把该对象放到weak链表中,并且把它的颜色回退到灰色。
    如果不是弱表的话,则遍历Table的所有元素,包括数组部分和hash部分。
  2. Closure对象
    不管是CClosure还是LClosure,GC都把遍历所有UpValue标记为黑色。
  3. Thread对象
    对于线程对象,GC会把它从gclist拿出来,放到grayagain链表里,并且把它回退到灰色,同时遍历该线程堆栈上(traversestack)所有的元素。
  4. proto对象
    标记proto数据中的文件名、字符串、upvalue、局部变量等所有被引用的对象
    //lgc.c, line: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) {
    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);
    }
    case LUA_TFUNCTION: {
      Closure *cl = gco2cl(o);
      g->gray = cl->c.gclist;
      traverseclosure(g, cl);
      return (cl->c.isC) ? sizeCclosure(cl->c.nupvalues) :
                           sizeLclosure(cl->l.nupvalues);
    }
    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;
    }
    case LUA_TPROTO: {
      Proto *p = gco2p(o);
      g->gray = p->gclist;
      traverseproto(g, p);
      return sizeof(Proto) + sizeof(Instruction) * p->sizecode +
                             sizeof(Proto *) * p->sizep +
                             sizeof(TValue) * p->sizek + 
                             sizeof(int) * p->sizelineinfo +
                             sizeof(LocVar) * p->sizelocvars +
                             sizeof(TString *) * p->sizeupvalues;
    }
    default: lua_assert(0); return 0;
  }
}

灰色链表没有对象以后,则需要遍历g->grayagain,调用入口是atomic。
这个GC算法称为:三色增量标记扫描算法,整体是增量可以被打断的,唯独除了调用atomic这一步。

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 */
}

这个函数主要做的操作有:

  1. 调用remarkupvals把UpValua标记为灰色,标记的时候UpVal会加入到gray列表里,所以需要调用propagateall将gray链表标记一下。
  2. 把gray指向weak表,清除weak表,同时调用propagateall将gray链表标记一下。
  3. 标记grayagain
  4. 把当前白色类型切换到下一次GC操作的白色类型(此篇文章暂时没有展开讨论)
  5. 修改GC状态到下一个阶段:GCSsweepstring(回收字符串阶段)

也许你会问,为什么会有grayagain的存在呢?
因为在遍历灰色链表的过程中,可能会有新增对象会被扫描过的Table对象引用,这个Table对象将会放在grayagain里。

特别需要注意的是:在标记过程中,如果新增对象被一个已经被GC扫描过的对象引用了,遇到这种情况时会根据两个对象的状态和类型做出处理。(颜色的标记过程是:白->灰->黑,向前指的是向右,向后指是向左)
luaC_barrierf(标记颜色向前一步):如果一个新建的对象(白色的),被一个已经扫描过的对象(黑色的)引用了,那么会把这个新建的对象(白色的)改成灰色。
luaC_barrierback(标记颜色向后一步): 如果一个新建的对象(白色的),被一个已经扫描过的Table对象(黑色的)引用了,那么会把这个被扫描过对象(黑色的)改成灰色,并且把它加到grayagain链表里。
不知道你注意到了没:向前和向后的区别有两个,第一是:被标记颜色的对象,前者是对新建对象标记,后者是对被扫描过的对象标记;第二,luaC_barrierback是针对Table对象处理的。

阶段3:GCSsweepstring(字符串回收阶段)

回收阶段分为两大部分,第一是回收字符串部分,第二是,肯定是除开字符串以外的其它对象部分的啦~
//lgc.c, line:572

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;
}

针对回收字符串阶段,系统会对字符串存放的stringtable中的hash表进行遍历回收,当回收完毕以后,把GC状态修改为:GCSsweep,进入下一个回收状态。

阶段4:GCSsweep(除字符串以外的回收阶段)

//lgc.c, line:407

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;
}

对sweepgc链表上所有元素进行回收,此时的回收调用的sweeplist,对于GCSsweepstring(字符串回收阶段)时调用的是sweepwholelist,sweepwholelist最终执行的还是sweeplist。
当sweepgc链表上所有元素都被回收完之后,系统把GC状态修改为:GCSfinalize。

阶段5:GCSfinalize(终止阶段)

标记扫描清除都做完了,一个完整的GC差不多了,由GCSfinalize进行收尾,对tmudata链表进行处理,处理完毕以后则把GC状态切到GCSpause,一次GC就结束了,等待下一次的GC开始。

case GCSfinalize: {
  if (g->tmudata) {
	GCTM(L);
	if (g->estimate > GCFINALIZECOST)
	  g->estimate -= GCFINALIZECOST;
	return GCFINALIZECOST;
  }
  else {
	g->gcstate = GCSpause;  /* end collection */
	g->gcdept = 0;
	return 0;
  }
}

在有限的篇幅里,主要讲解的是GC主体逻辑,没有提及的到请读者深入探究,如果有误区,请读者不吝赐教。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章