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主體邏輯,沒有提及的到請讀者深入探究,如果有誤區,請讀者不吝賜教。

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