14

深入Lua:垃圾回收3

 3 years ago
source link: https://zhuanlan.zhihu.com/p/100994602
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

深入Lua:垃圾回收3

作为垃圾回收的最后一篇,要来描述一下内存如何统计,什么时候触发GC,以后回收的灵敏度等问题。

看了这一章,你应该能够知道如何通过pausestep multiplier这两个参数来控制GC的速度。

global_state有两个字段是关于Lua的内存统计的:

l_mem totalbytes;
l_mem GCdebt;

lua_newstate函数里,这两个字段初始为:

g->totalbytes = sizeof(LG);
g->GCdebt = 0;

在内存分配函数luaM_realloc_里,会根据分配或释放内存调整GCdebt的值:

void *luaM_realloc_ (lua_State *L, void *block, size_t osize, size_t nsize) {
  ...
  g->GCdebt = (g->GCdebt + nsize) - realosize;
  ...
}

到这里明确知道,这两个字段之和就是Lua当前已分配的内存,有一个宏可以验证这一点:

#define gettotalbytes(g)    cast(lu_mem, (g)->totalbytes + (g)->GCdebt)

判断是否触发GC的宏是:

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

判断条件非常简单,g->GCdebt如果大于0,就执行一次luaC_step。通过检查GCdebt这个字段的变化,就能知道GC的行为。

设置GCdebt的值是通过luaE_setdebt这个函数实现的:

void luaE_setdebt (global_State *g, l_mem debt) {
  l_mem tb = gettotalbytes(g);
  lua_assert(tb > 0);
  if (debt < tb - MAX_LMEM)
    debt = tb - MAX_LMEM;  /* will make 'totalbytes == MAX_LMEM' */
  g->totalbytes = tb - debt;
  g->GCdebt = debt;
}

debt可以小于0,这表示后面要分配debt的内存量之后,才会使GCdebt变成大于0,也就是才会触发GC。但不管debt怎么变,gettotalbytes(g)的值一定是精确的内存分配量。

luaC_step执行一步GC:

void luaC_step (lua_State *L) {
  global_State *g = G(L);
  // 1. 计算GC的内存债务
  l_mem debt = getdebt(g);  /* GC deficit (be paid now) */
  ...
  // 2. 循环执行singlestep,直到GC周期完毕,或debt小于某个值
  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)
    // 3. 如果GC结束,计算下一个阀值
    setpause(g);  /* pause until next cycle */
  else {
    // 4. 否则计算下一次触发的时机
    debt = (debt / g->gcstepmul) * STEPMULADJ;  /* convert 'work units' to Kb */
    luaE_setdebt(g, debt);
    runafewfinalizers(L);
  }
}

第一步是调用getdebt获得一个debt值,这个值指定GC要做的工作量,先看这个函数的实现:

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;
  }
}

从代码可看出返回值和GCdebt,gcstepmul这两个字段有关,gcstepmul是对GCdebt的一个缩放,gcstepmul越大,返回的值越大,说明GC一步要做的工作量越多。

第二步是不断的调用singlestep进行GC处理,函数返回的值表示它遍历过多少内存,一直到debt小于某个值,或者是GC完毕,这个步算结束。

第三步表示一个周期执行完了,调用setpause重新计算下一个GC周期的内存阀值:

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是正在使用的非垃圾内存的估计值,在原子阶段结束,该值等于gettotalbytes。后面的清扫阶段,该值不断减掉回收的内存。所以在GC周期结束时,该值差不多就是正在使用的内存。

threshold和gcpause成正比,gcpause越大,表示内存阀值越大,比如等于200,表示阀值等于内存使用的2倍。最后通过luaE_setdebt调整下次GC的条件,这样就会等到内存增长为2倍时才开始下一个GC周期。

总结一下:

  • g->gcpause 控制新GC周期间隔,值越大GC越不灵敏,如果值小于100那么GC周期不停顿,如果值等于200,那么GC会等内存增长2倍之后才开始新的GC周期。
  • g->gcstepmul 控制GC的回收速度,200表示回收速度是内存分配的2倍,如果是一个很大值,GC就像一个stop-the-world的回收器一样。

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK