Lua5.3自動GC觸發條件分析與理解


在我的上一篇文章《Lua5.3版GC機制的學習理解》的4.2部分GC觸發條件中,對這部分內容粗略的解釋爲:LuaGC是當lua使用的內存到達閥值時,自動觸發。那麼這篇文章將對這句描述,進行進一步的理解,並探討一些GC參數的調節問題。

1.GC觸發過程

1. lua在每次分配新的內存時,會主動檢查是否滿足GC條件。我們可以通過審查lapi.c/ldo.c/lvm.c,發現大部分會引起內存增長的API中,都調用了luaC_checkGC。源碼如下:
(lgc.h)

#define luaC_condGC(L,pre,pos) \
	{ if (G(L)->GCdebt > 0) { pre; luaC_step(L); pos;}; \
	  condchangemem(L,pre,pos); }

#define luaC_checkGC(L)		luaC_condGC(L,(void)0,(void)0)

2. 由上訴代碼中可知,當GCdebt大於零時,即會觸發自動GC。
3. 上訴代碼的核心功能,即GC函數的入口爲luaC_step,源碼如下:
(lgc.c)

void luaC_step (lua_State *L) {
	  global_State *g = G(L);
	  l_mem debt = getdebt(g);  /* GC deficit (be paid now) */
	  if (!g->gcrunning) {  /* not running? */
		luaE_setdebt(g, -GCSTEPSIZE * 10);  /* avoid being called too often */
		return;
	  }
	  do {  /* repeat until pause or enough "credit" (negative debt) */
		lu_mem work = singlestep(L);  /* perform one single step */
		debt -= work;
	  } while (debt > -GCSTEPSIZE && g->gcstate != GCSpause);
	  if (g->gcstate == GCSpause)
		setpause(g);  /* pause until next cycle */
	  else {
		debt = (debt / g->gcstepmul) * STEPMULADJ;  /* convert 'work units' to Kb */
		luaE_setdebt(g, debt);
		runafewfinalizers(L);
	  }
}

由此可以明白,當GCdebt大於零時,luaC_step會通過控制GCdebt,循環調用singlestep(上一篇文章中GC回收中的主要函數)來對內存進行回收。請大家注意luaC_step這個函數,本篇文章將圍繞這個函數進行進一步的闡述。

2.過程詳解

2.1GCdebt

在上文中重點提到一個參數GCdebt ,整個GC的觸發過程都是由這個參數調節。這個參數的定義如下(還有一些內存相關的參數):
(lstate.h)

typedef struct global_State {
         …
  		l_mem totalbytes;  /* number of bytes currently allocated - GCdebt */
         l_mem GCdebt;  /* bytes allocated not yet compensated by the collector */
         lu_mem GCestimate;  /* an estimate of the non-garbage memory in use */
         …
}

totalbytes:爲實際內存分配器所分配的內存與GCdebt的差值。
GCdebt:需要回收的內存數量。
GCestimate:內存實際使用量的估計值。
重點關注GCdebt,通過查看其引用,可以明白,lua新建對象luaM_newobject與釋放內存luaM_freemem都是通過調用luaM_realloc完成的,其源碼如下 :
(lmem.c)

void *luaM_realloc_ (lua_State *L, void *block, size_t osize, size_t nsize) {
  void *newblock;
  global_State *g = G(L);
  size_t realosize = (block) ? osize : 0;
  lua_assert((realosize == 0) == (block == NULL));
#if defined(HARDMEMTESTS)
  if (nsize > realosize && g->gcrunning)
    luaC_fullgc(L, 1);  /* force a GC whenever possible */
#endif
  newblock = (*g->frealloc)(g->ud, block, osize, nsize);
  if (newblock == NULL && nsize > 0) {
    lua_assert(nsize > realosize);  /* cannot fail when shrinking a block */
    if (g->version) {  /* is state fully built? */
      luaC_fullgc(L, 1);  /* try to free some memory... */
      newblock = (*g->frealloc)(g->ud, block, osize, nsize);  /* try again */
    }
    if (newblock == NULL)
      luaD_throw(L, LUA_ERRMEM);
  }
  lua_assert((nsize == 0) == (newblock == NULL));
  g->GCdebt = (g->GCdebt + nsize) - realosize;
  return newblock;
}

由g->GCdebt = (g->GCdebt + nsize) - realosize可知,GCdebt就是在不斷的統計釋放與分配的內存。當新增分配內存時,GCdebt值將會增加,即GC需要釋放的內存增加;當釋放內存時,GCdebt將會減少,即GC需要釋放的內存減少。結合1部分可知,GCdebt大於零則意味着有需要GC釋放還未釋放的內存,所以會觸發GC。

2.2stepmul

明白了GCdebt這個參數的意義,我們回頭繼續來看luaC_step這個函數。首先關注
(lgc.c) [luaC_step]

l_mem debt = getdebt(g);  /* GC deficit (be paid now) */

(lgc.c)

static l_mem getdebt (global_State *g) {
  l_mem debt = g->GCdebt;
  int stepmul = g->gcstepmul;
  if (debt <= 0) return 0;  /* minimal debt */
  else {
    debt = (debt / STEPMULADJ) + 1;
    debt = (debt < MAX_LMEM / stepmul) ? debt * stepmul : MAX_LMEM;
    return debt;
  }
}

在luaC_step這個函數中,debt並非是GCdebt而是被乘以倍率的GCdebt。這個倍率即爲gcstepmul。關於這個參數,相信如果手動GC過的人,一定明白,在手動GC函數中有collectgarbage(“setstepmul”),這個參數默認爲200,可以通過手動設定的方式令其改變。這個參數的意義就是GCdebt的一個倍率,上述getdebt函數的核心功能爲debt=debt*stepmul。即通過stepmul將GCdebt放大或縮小一個倍率。
之後會執行luaC_step的核心部分:
(lgc.c)[luaC_step]

do {  /* repeat until pause or enough "credit" (negative debt) */
		lu_mem work = singlestep(L);  /* perform one single step */
		debt -= work;
	} while (debt > -GCSTEPSIZE && g->gcstate != GCSpause);

結合上文可知,將GCdebt放大後的debt將會導致該循環的次數增加,從而延長”一步”的工作量,所以stepmul被稱爲“步進倍率”。如果將stepmul設定的很大,則將會將GCdebt放大很多倍,那麼GC將會退化成之前的GC版本stop-the-world ,因爲它試圖在儘可能多的回收內存,導致阻塞。在這個循環中,將會調用singlestep,進行GC的分步過程(可參考我的上一篇文章)。當進行完一個完整的GC過程或GCdebt小於一個基準量(-GCSTEPSIZE)時,將會退出這個循環。

2.3pause

1.如果是完成一個GC循環,則需要設定下一次GC循環的等待時間,當然依然是通過GCdebt來設定。
(lgc.c) [luaC_step]

if (g->gcstate == GCSpause)
    setpause(g);  /* pause until next cycle */

(lgc.c)

static void setpause (global_State *g) {
  l_mem threshold, debt;
  l_mem estimate = g->GCestimate / PAUSEADJ;  /* adjust 'estimate' */
  lua_assert(estimate > 0);
  threshold = (g->gcpause < MAX_LMEM / estimate)  /* overflow? */
            ? estimate * g->gcpause  /* no overflow */
            : MAX_LMEM;  /* overflow; truncate to maximum */
  debt = gettotalbytes(g) - threshold;
  luaE_setdebt(g, debt);
}

上述代碼中GCestimate可以理解爲lua的實際佔用內存(當GC循環執行到GCScallfin狀態以前,g->GCestimate與gettotalbytes(g)必然相等,即可以將GCestimate理解爲當前lua的實際佔用內存),而MAX_LMEM/estimate即爲本機最大內存量與當前lua實際使用量的比值。而threshold即爲之前文章中提到內存的閥值。該閥值大部分時間是通過estimategcpause得到的。gcpause默認值爲100。
當然gcpause這個值也是可以通過手動GC函數collectgarbage(“setpause”)來設定的,當gcpause爲200時,意味着,threshold=2
GCestimate,則debt=-GCestimate(gettotalbytes約等於GCestimate),所以GCdebt將在內存分配器分配新內存時由-GCestimate緩慢增長到大於零之後再開始新的一輪GC,所以pause被稱爲“間歇率”,即將pause設定爲200時就會讓收集器等到總內存使用量達到之前的兩倍時纔開始新的GC循環。

2.當然如果一個GC循環未結束,則需要重新設置GCdebt,等待下一次的觸發。
(lgc.c) [luaC_step]

else {
    debt = (debt / g->gcstepmul) * STEPMULADJ;  /* convert 'work units' to Kb */
    luaE_setdebt(g, debt);
    runafewfinalizers(L);
   }

通過以上的分析,我們可以得出結論:通過gcpause、gcstepmul可以對debt的值進行縮放,debt的值越大,則需要GC償還的債務越大,GC的過程會越活躍;反之GC的債務越小,GC會越慢。即:debt越大則GC越快,反之則越慢。

3.總結

至此,lua GC觸發條件相關內容已經講述完畢了。但是爲了在解釋完源碼之後,能讓讀者有一個宏觀的理解。所以我想最後用一句話來完成對這個條件的描述:(當然表述只是針對了主幹情況)
Lua在每次申請新的內存分配時,會調用luaC_checkGC來檢測GCdebt是否大於零,如果是則觸發自動GC過程。
本篇文章是針對我的上一篇文章《Lua5.3版GC機制的學習理解》中GC觸發條件的補充,希望文中錯誤之處,大家能夠多多批評指正,我一定認真及時糾正相關的錯誤。

參考資料

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

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