目录
- 1.GC触发过程
- 2.过程详解
- 2.1GCdebt
- 2.2stepmul
- 2.3pause
- 3.总结
- 参考资料
在我的上一篇文章《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=2GCestimate,则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触发条件的补充,希望文中错误之处,大家能够多多批评指正,我一定认真及时纠正相关的错误。
参考资料