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

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