Lua5.3版GC機制理解


lua垃圾回收(Garbage Collect)是lua中一個比較重要的部分。由於lua源碼版本變遷,目前大多數有關這個方面的文章都還是基於lua5.1版本,有一定的滯後性。因此本文通過參考當前的5.3.4版本的Lua源碼,希望對Lua的GC算法有一個較爲詳盡的探討。

1.Lua垃圾回收算法原理簡述

lua採用了標記清除式(Mark and Sweep)GC算法,算法簡述:
標記:每次執行GC時,先以若干根節點開始,逐個把直接或間接和它們相關的節點都做上標記;
清除:當標記完成後,遍歷整個對象鏈表,把被標記爲需要刪除的節點一一刪除即可。

2.Lua垃圾回收中的三種顏色

所謂的顏色就是上文中“算法簡述”提到過的標記,lua用白、灰、黑三色來標記一個對象的可回收狀態。(白色又分爲白1、白2)
白色:可回收狀態。
詳解:如果該對象未被GC標記過則此時白色代表當前對象爲待訪問狀態。舉例:新創建的對象的初始狀態就應該被設定爲白色,因爲該對象還沒有被GC標記到,所以保持初始狀態顏色不變,仍然爲白色。如果該對象在GC標記階段結束後,仍然爲白色則此時白色代表當前對象爲可回收狀態。但其實本質上白色的設定就是爲了標識可回收。
灰色:中間狀態。
詳解:當前對象爲待標記狀態。舉例:當前對象已經被GC訪問過,但是該對象引用的其他對象還沒有被標記。
黑色:不可回收狀態。
詳解:當前對象爲已標記狀態。舉例:當前對象已經被GC訪問過,並且對象引用的其他對象也被標記了。
備註:白色分爲白1和白2。原因:在GC標記階段結束而清除階段尚未開始時,如果新建一個對象,由於其未被發現引用關係,原則上應該被標記爲白色,於是之後的清除階段就會按照白色被清除的規則將新建的對象清除。這是不合理的。於是lua用兩種白色進行標識,如果發生上述情況,lua依然會將新建對象標識爲白色,不過是“當前白”(比如白1)。而lua在清掃階段只會清掃“舊白”(比如白2),在清掃結束之後,則會更新“當前白”,即將白2作爲當前白。下一輪GC將會清掃作爲“舊白”的白1標識對象。通過這樣的一個技巧解決上述的問題。如下圖:(下圖中爲了方便顏色變換的理解,沒有考慮barrier的影響)
顏色轉換圖2018-12-26-Color_conversion_chart

3.Lua垃圾回收詳細過程

算法流程1/2018-12-26-algorithm_flow1
算法流程2/2018-12-26-algorithm_flow2

4.步驟源碼詳解

4.1新建對象階段

STEP1:新建可回收對象,將其顏色標記爲白色
解釋:首先lua對數據類型進行了抽象,具體數據結構可見如下源碼:
(lobject.h)

** Common type for all collectable objects
*/
typedef struct GCObject GCObject;

/*
** Common Header for all collectable objects (in macro form, to be
** included in other objects)
*/
#define CommonHeader  GCObject *next; lu_byte tt; lu_byte marked

/*
** Common type has only the common header
*/
struct GCObject {
  CommonHeader;
};


/*
** Tagged Values. This is the basic representation of values in Lua,
** an actual value plus a tag with its type.
*/
/*
** Union of all Lua values
*/
typedef union Value {
  GCObject *gc;    /* collectable objects */
  void *p;         /* light userdata */
  int b;           /* booleans */
  lua_CFunction f; /* light C functions */
  lua_Integer i;   /* integer numbers */
  lua_Number n;    /* float numbers */
} Value;

#define TValuefields  Value value_; int tt_

typedef struct lua_TValue {
  TValuefields;
} TValue;

由代碼分析可知,Lua的實現中,數據類型可以分爲需要被GC管理回收的對象、按值存儲的對象。而STEP1中提及的“可回收對象”(GCObject)就是指那些需要被GC管理回收的對象,在lua5.3中,具體是指:TString、Udata、Cloure、Table、Proto、lua_State(可通過查看GCUnion)這些數據類型。這些數據類型都有一個共同的部分CommonHeader:
(lobject.h)

#define CommonHeader
GCObject *next; lu_byte tt; lu_byte marked

其中next鏈接下一個GCObject,tt表明數據類型,marked用於存儲之前提到的顏色。
創建上述數據類型對象是通過lua虛擬機調用對應的數據類型創建函數完成的。在創建過程中,總會調用(lgc.c)luaC_newobj這個函數,來完成GCObject的初始化。
以Table爲例,會依次調用:
(lvm.c)[luaV_execute]->(ltable.c)[luaH_new]->(lgc.c)[luaC_newobj]
下面我們來看luaC_newobj:
(lgc.c)

/*
** create a new collectable object (with given type and size) and link
** it to 'allgc' list.
*/
GCObject *luaC_newobj (lua_State *L, int tt, size_t sz) {
  global_State *g = G(L);
  GCObject *o = cast(GCObject *, luaM_newobject(L, novariant(tt), sz));
  o->marked = luaC_white(g);
  o->tt = tt;
  o->next = g->allgc;
  g->allgc = o;
  return o;
}

該函數依次完成:

  • 1.通過luaM_newobject進行內存分配,並更新GCdebt(內存使用相關的參數)
  • 2.將GCObject置爲“當前白”,設置數據類型,將GCObject掛載到alloc鏈表上

注意:alloc鏈表就是清除階段,被GC依次遍歷鏈表上的對象標誌並根據標誌進行清除的重要數據結構。
如此便從創建一個可回收對象開始了我們的GC之旅。

4.2觸發條件

STEP2:達到GC條件
Lua分爲自動、手動兩種GC方式。
手動式:
(lapi.h)

 LUAI_FUNC void luaC_step (lua_State *L);

自動式:
(lgc.h)

#define luaC_condGC(L,pre,pos) \
  { if (G(L)->GCdebt > 0) { pre; luaC_step(L); pos;}; \
    condchangemem(L,pre,pos); }
/* more often than not, 'pre'/'pos' are empty */
#define luaC_checkGC(L)   luaC_condGC(L,(void)0,(void)0)

手動方式就是通過調用collectgarbage ([opt [, arg]])來啓動GC。而自動方式則需要滿足GC的條件。如果我們審查lapi.c/ldo.c/lvm.c ,會發現大部分會引起內存增長的API中,都調用了luaC_checkGC。從而實現GC可以隨內存使用增加而自動進行。而這些觸發條件是通過g-> GCdebt、g-> totalbytes等參數計算得來。由於參數的意義,參數的計算方式等問題需要進一步闡述,限於篇幅,我準備在之後的關於手動GC調參方面的文章中詳細闡述。但目前可以概括的說明luaGC的觸發條件:當lua使用的內存達到閥值,便會觸發GC。當然這個閥值是動態設定的。

4.3 GC函數狀態機

當滿足GC條件後,進入了真正的GC過程。
在開始這個過程之前,需要闡述一個問題。lua5.0之前的GC因爲只採用了白、黑兩種標識色,所以GC時需要stop the world,即GC過程需要一次性完成。而lua5.1之後採用了三標識色,最大的的一個改進就是實現了分步。
**這個改進的意義在於提高了lua系統的實時性,**使GC過程可以分段執行。這個改進是由一系列變化引起的,比如:灰色的引入、barrier機制的引入、singlestep函數(GC過程中最重要的一個函數)的狀態細分等。但是我認爲真正從原理上可以實現分步,是在於灰色的引入。這是因爲如果只有黑、白兩色,每個對象的狀態就僅是“二元的”,不能有中間態,所以GC操作時需要不可被打斷。
接下來分析luaGC機制中最重要的一個函數singlestep。所有的GC流程,都是從singlestep函數開始的。
(lgc.h)

static lu_mem singlestep (lua_State *L)

**singlestep實際上就是一個簡單的狀態機。**具體如下:
GC狀態機/2018-12-26-GC_StateMachine

4.4標記階段

STEP3:從根對象開始標記,將白色置爲灰色,並加入到灰色鏈表中
該步驟實際對應狀態:GCSpause
(lgc.c)[singlestep]

switch (g->gcstate) {
      case GCSpause: {
      g->GCmemtrav = g->strt.size * sizeof(GCObject*);
      restartcollection(g);
      g->gcstate = GCSpropagate;
      return g->GCmemtrav;
      }

主要工作由restartcollection函數完成:
(lgc.c)

/*
** mark root set and reset all gray lists, to start a new collection
*/
static void restartcollection (global_State *g) {
  g->gray = g->grayagain = NULL;
  g->weak = g->allweak = g->ephemeron = NULL;
  markobject(g, g->mainthread);
  markvalue(g, &g->l_registry);
  markmt(g);
  markbeingfnz(g);  /* mark any finalizing object left from previous cycle */
}
  • 1.將用於輔助標記的各類型對象鏈表進行初始化清空,其中g->gray是灰色節點鏈;g->grayagain是需要原子操作標記的灰色節點鏈;g->weak、g->allweak、g->ephemeron是與弱表相關的鏈。
  • 2.然後依次利用markobject、markvalue、markmt、markbeingfnz標記根(全局)對象:mainthread(主線程(協程), 註冊表(registry), 全局元表(metatable), 上次GC循環中剩餘的finalize中的對象,並將其加入對應的輔助標記鏈中。

STEP4:灰色鏈表是否爲空
STEP5:從灰色鏈表中取出一個對象將其標記爲黑色,並遍歷和這個對象相關聯的其他對象
上述步驟對應狀態:GCSpropagate,這是標記階段的核心內容。
(lgc.c)[singlestep]

case GCSpropagate: {
      g->GCmemtrav = 0;
      lua_assert(g->gray);
      propagatemark(g);
      if (g->gray == NULL)  /* no more gray objects? */
       g->gcstate = GCSatomic;  /* finish propagate phase */
      return g->GCmemtrav;  /* memory traversed in this step */
  }

主要工作由propagatemark函數完成:
(lgc.c)

/*
** traverse one gray object, turning it to black (except for threads,
** which are always gray).
*/
static void propagatemark (global_State *g) {
  lu_mem size;
  GCObject *o = g->gray;
  lua_assert(isgray(o));
  gray2black(o);
  switch (o->tt) {
    case LUA_TTABLE: {
      Table *h = gco2t(o);
      g->gray = h->gclist;  /* remove from 'gray' list */
      size = traversetable(g, h);
      break;default: lua_assert(0); return;
  }
  g->GCmemtrav += size;
}

步驟四中循環判斷灰色鏈表,其實並不是通過循環實現的。而是如果灰色表不爲空,狀態將不會發生改變。進而每次進入狀態機時,由於狀態未發生改變,而反覆執行這個狀態對應的處理函數。直到狀態發生改變後,進入下一個狀態。
設計的原因可以結合propagetemark這個函數一同理解,在propagatemark中,每次只會從灰色鏈表中取一個灰色節點,將其置爲黑(與lua5.1的GC有區別),從灰色鏈表中出去,遍歷與此節點相關的其他節點,並將有關節點加入到灰色鏈中,至此就完成了一次GCSpropagate狀態處理。不難發現,這個過程只是處理了一個原先灰色鏈表中的灰色節點。這是因爲標記與對應節點有關的節點實際上是通過遍歷完成的,這個過程的開銷會很大,所以lua只希望每次GCSpropagate時,處理一個這樣的節點。
這樣的好處就是將開銷大的步驟通過多次調用,減少每次阻塞的時間。而同時帶來了一個新的問題,如果lua創建分配對象的速度遠大於GCSpropagate處理的速度,那麼lua的GC過程將會阻塞在GCSpropagate這個狀態。解決方法就留給讀者思考了
在propagetemark方法中,會針對不同的數據類型進行分類操作,但是最終都會落腳到reallymarkobject這個方法上:
(lgc.c)

static void reallymarkobject (global_State *g, GCObject *o) {
 reentry:
  white2gray(o);
  switch (o->tt) {
    case LUA_TSHRSTR: {
      gray2black(o);
      g->GCmemtrav += sizelstring(gco2ts(o)->shrlen);
      break;
    }case LUA_TTABLE: {
      linkgclist(gco2t(o), g->gray);
      break;
    }default: lua_assert(0); break;
  }
}

如果是字符串類型,因爲其特殊性,可以直接判定爲黑。其他類型加入灰色鏈或其他的輔助標記鏈。
STEP6:最後對灰色鏈表進行一次清除且保證是原子操作。
該步驟實際對應狀態:GCSatomic
(lgc.c)[singlestep]

case GCSatomic: {
      lu_mem work;
      propagateall(g);  /* make sure gray list is empty */
      work = atomic(L);  /* work is what was traversed by 'atomic' */
      entersweep(L);
      g->GCestimate = gettotalbytes(g);  /* first estimate */;
      return work;
    }

1.Propagateall
(lgc.c)

static void propagateall (global_State *g) {
      while (g->gray) propagatemark(g);}

先檢測g->gray,因爲luaC_barrier函數(用於處理新建對象的一種機制)的存在,它調用reallymarkobject時有可能會操作變量g->gray.
2.atomic完成需要原子操作的步驟,主要如下:

  • 1重新遍歷(跟蹤)根對象。
  • 2遍歷之前的grayagain(grayagain上會有弱表的存在), 並清理弱表的空間。
  • 3調用separatetobefnz函數將帶__gc函數的需要回收的(白色)對象放到global_State.tobefnz表中,留待以後清理。
  • 4.使global_State.tobefnz上的所有對象全部可達。
  • 5.將當前白色值切換到新一輪的白色值。

以上內容只是對atomic簡述,有很多具體的細節會牽扯過多的內容,限於篇幅就不具體展開了。

4.5清除階段

STEP7:據不同類型的對象,進行分步回收。回收中遍歷不同類型對象的存儲鏈表
STEP8:該對象存儲鏈表是否到達鏈尾
STEP9:逐個判斷對象顏色是否爲白
STEP10:釋放對象所佔用的空間
STEP11:將對象顏色置爲白
上述步驟實際對應狀態:GCSswpallgcGCSswpfinobjGCSswptobefnzGCSswpend
(lgc.c)[singlestep]

case GCSswpallgc: {  /* sweep "regular" objects */
      return sweepstep(L, g, GCSswpfinobj, &g->finobj);
    }
  case GCSswpfinobj: {  /* sweep objects with finalizers */
      return sweepstep(L, g, GCSswptobefnz, &g->tobefnz);
    }
  case GCSswptobefnz: {  /* sweep objects to be finalized */
      return sweepstep(L, g, GCSswpend, NULL);
    }
  case GCSswpend: {  /* finish sweeps */
      makewhite(g, g->mainthread);  /* sweep main thread */
      checkSizes(L, g);
      g->gcstate = GCScallfin;
      return 0;
}

這個過程可以理解成對垃圾分類回收
GCSswpallgc將通過sweepstep將g->allgc上的所有死對象釋放(GCSatomic狀態以前的白色值的對象),並將活對象重新標記爲當前白色值。
GCSswpfinobj和GCSswptobefnz兩個狀態也調用了sweepstep函數。但是g->finobj和g->tobefnz鏈表上是不可能有死對象的(原因留給讀者思考),因此它們的作用僅僅是將這些對象重新設置爲新一輪的白色。
GCSswpend用來釋放mainthread上的一些空間,如:字符串表,連接緩衝區等。
這些狀態的切換是通過判定當前類型的對象鏈表是否到達尾部實現的。
上述的狀態真正清掃操作是通過sweepstep來調用sweeplist完成的。
(lgc.c)

static GCObject **sweeplist (lua_State *L, GCObject **p, lu_mem count) {
  global_State *g = G(L);
  int ow = otherwhite(g);
  int white = luaC_white(g);  /* current white */
  while (*p != NULL && count-- > 0) {
    GCObject *curr = *p;
    int marked = curr->marked;
    if (isdeadm(ow, marked)) {  /* is 'curr' dead? */
      *p = curr->next;  /* remove 'curr' from list */
      freeobj(L, curr);  /* erase 'curr' */
    }
    else {  /* change mark to 'white' */
      curr->marked = cast_byte((marked & maskcolors) | white);
      p = &curr->next;  /* go to next element */
    }
  }
  return (*p == NULL) ? NULL : p;
}

上文中提到的各種可回收數據類型對象鏈表都是很長的,所以清除也是分段完成的,比如可以通過sweeplist中的count參數來控制每次清理的數量。(這個值的設定以及動態變換的過程,我希望在下一篇關於GC參數調節的文章中闡述)。從上述的清除過程中,可以明白lua的GC過程是非搬遷式的,即沒有對數據進行遷移,不做內存整理。
最後一個GC狀態:GCScallfin
(lgc.c)

case GCScallfin: {  /* call remaining finalizers */
      if (g->tobefnz && g->gckind != KGC_EMERGENCY) {
        int n = runafewfinalizers(L);
        return (n * GCFINALIZECOST);
      }
      else {  /* emergency mode or no more finalizers */
        g->gcstate = GCSpause;  /* finish collection */
        return 0;
      }
}

在這個狀態會逐個取出g->tobefnz鏈表上的對象,然後調用其__gc函數,並將其放入g->allgc鏈表中,準備在下個GC循環回正式回收此對象。

5.總結

至此,其實lua的GC算法已經講述完畢了。但是爲了在解釋完源碼之後,重新呼應文章開頭的算法簡述的內容,也方便讀者能夠有一個宏觀的理解。所以我想最後用一句話來完成對這個算法的描述:(當然表述只是針對了主幹情況)
Lua通過藉助grey鏈表,依次利用reallymarkobject對對象進行了顏色的標記,之後通過遍歷alloc鏈表,依次利用sweeplist清除需要回收的對象。
本篇文章是我這個後學晚輩倉促成文,實在是誠惶誠恐的。希望文中錯誤之處,大家能夠多多批評指正,我一定認真及時糾正相關的錯誤。感謝大家的閱讀。

參考資料

codedump-lua設計與實現
lua5.3.4源碼
雲風的博客
Programming in Lua

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