lua5.4 分代垃圾回收

前注

最近我看了 lua 5.4 分代垃圾回收的代码,网上很少讲到这块内容,于是,我写这篇文章分享一下,也当做总结。文章就以我看的 lua 5.4.4 做分析。

在进入今天的主题前,先回顾我之前写的 lua5.3 垃圾回收分析,重复的内容不再赘述。

lua 5.4 支持两种 gc 方式, 增量式 gc 和分代 gc ,默认是增量式,可以通过以下 api 切换。

切换成分代 gc:
collectgarbage "generational"
切换成增量式 gc:
collectgarbage "incremental"

提出问题

1. 增量式 gc 跟分代 gc 的对比?
2. 分代 gc 过程是怎样的?
3. 分代 gc 有哪些状态?
4. 分代 gc 怎么调优?

1. 增量式 gc 跟分代 gc 的对比?

增量式 gc 使用状态机来推进 gc 过程,状态从 GCSpause 到 GCSpropagate,再到 GCSenteratomic ..., 最后又回到 GCSpause,其主要阶段 GCSpropagate 可以分成无数个小步骤执行,避免了 gc 长时间中断对业务的影响。正是这样,当对象比较多时,增量式 gc 一个周期比较长,对于新对象会延迟到下一个 gc 周期处理,导致临时数据不能快速释放。

分代 gc 大体将对象分成 young 和 old 两大类,过程也分为浅扫描(minor)和深扫描(major)。对象创建时标记为 young,minor 过程扫描 young 对象,当对象存活下来,就会标记成 old ,old 数据将只在 major 过程遍历和清理。这样做目的是,新创建的对象很多是临时数据,很快不再使用,可以被快速清理掉,而长时间存活的数据很大可能还会继续使用,不用频繁重复检查。

另外,原子操作(atomic)的执行频率两者也有很大差异。在每次分代 gc 都会执行,增量式的话只在原子阶段(GCSenteratomic)执行, 所以相对来说,分代的原子操作更加频繁。分代虽然增加了原子操作的次数,增加了根对象的重复扫描的次数,但两个原子操作的间隔更短,这期间产生的新对象更少,使得单次原子操作的数据遍历减少。这样,尽管总的原子操作变多了,但是每次原子操作的时长更短,减少单次 gc 卡顿的时延。而且,分代每次执行都会删除不再使用的新对象,如果临时对象占系统比例较多时,分代对比增量式的较多重复扫描就会减少,而且可以更快释放内存。相反,如果系统的对象都不是临时数据,那分代的这个副作用就会放大。

备注:原子操作(atomic)在后文有说明

2. 分代 gc 过程是怎样的?

将所有 gc 对象分成两类,young 和 old。当 young 对象存活过了两次 gc 过程,就会变成 old 对象。old 对象在浅扫描(minor)将不再被清除和遍历。当总的内存增长超过阈值时,执行深扫描(major),major 会扫描全部数据对象,清理垃圾对象后,把所有存活对象都变为 old 对象。

以上,对象要存活过两次才标记为 old,这是因为如果一次 gc 就标记为 old,会导致本 gc 周期新创建的对象,如函数堆栈上的临时变量都变成 old 对象,很难在短时间释放内存。

对象标记 - marked

/*
** Layout for bit use in 'marked' field. First three bits are
** used for object "age" in generational mode. Last bit is used
** by tests.
*/
#define WHITE0BIT	3  /* object is white (type 0) */
#define WHITE1BIT	4  /* object is white (type 1) */
#define BLACKBIT	5  /* object is black */
#define FINALIZEDBIT	6  /* object has been marked for finalization */

#define TESTBIT		7

marked 用一个字节表达 gc 对象的三色标记和分代状态。从低位到高位,低三位表示分代 gc 的6种状态,高五位用于双色标记算法实现,第4、5位表示两种 white 标记,第6位表示 black 标记,第7位表示对象放最后处理,第8位用于测试。

// lgc.h 

/* object age in generational mode */
#define G_NEW		0	/* created in current cycle */
#define G_SURVIVAL	1	/* created in previous cycle */
#define G_OLD0		2	/* marked old by frw. barrier in this cycle */
#define G_OLD1		3	/* first full cycle as old */
#define G_OLD		4	/* really old object (not to be visited) */
#define G_TOUCHED1	5	/* old object touched this cycle */
#define G_TOUCHED2	6	/* old object touched in previous cycle */
#define AGEBITS		7  /* all age bits (111) */

#define getage(o)	((o)->marked & AGEBITS)
#define setage(o,a)  ((o)->marked = cast_byte(((o)->marked & (~AGEBITS)) | a))
#define isold(o)	(getage(o) > G_SURVIVAL)

除了 G_NEW, G_SURVIVAL, 其他都是 old 的不同状态

2.1 minor gc 的主要过程:
// lgc.c
/*
** Does a young collection. First, mark 'OLD1' objects. Then does the
** atomic step. Then, sweep all lists and advance pointers. Finally,
** finish the collection.
*/
static void youngcollection (lua_State *L, global_State *g) {
  GCObject **psurvival;  /* to point to first non-dead survival object */
  GCObject *dummy;  /* dummy out parameter to 'sweepgen' */
  lua_assert(g->gcstate == GCSpropagate);
  if (g->firstold1) {  /* are there regular OLD1 objects? */
    // 将前一次 youngcollection 新标记的 G_OLD1 数据标记为 G_OLD
    markold(g, g->firstold1, g->reallyold);  /* mark them */
    g->firstold1 = NULL;  /* no more OLD1 objects (for now) */
  }
  markold(g, g->finobj, g->finobjrold);
  markold(g, g->tobefnz, NULL);
  /*
   * 原子操作,遍历 rootgc 开始标记,将未标记的对象加入 g->gray 链表。
   * 再遍历 g->gray 链表取所有对象完成标记,如果对象是弱表或协程,则加入
   * g->grayagain 链表。遍历 g->grayagain 链表取所有对象完成标记,遍历
   * 弱表,将白色的项置nil。遍历 g->finobj 链表,把白色的对象移到 g->tobefnz
   * 链表。遍历 g->tobefnz 链表,完成标记。切换当前白色到另一种白色。
   */
  atomic(L);
  // 原子操作结束后,白色数据就是可以清理的数据。

  g->gcstate = GCSswpallgc;
  /*
   * 主要是遍历 g->allgc 到 g->survival 取得状态为 G_NEW 的对象,
   * 清理垃圾数据,存活数据状态改 G_SURVIVAL
   */
  psurvival = sweepgen(L, g, &g->allgc, g->survival, &g->firstold1);
  /*
   * 主要是遍历 g->survival 到 g->old1 取得状态为 G_SURVIVAL 的对象,
   * 清理垃圾数据,存活数据状态改 G_OLD1
   */
  sweepgen(L, g, psurvival, g->old1, &g->firstold1);
  // g->firstold1 为以上遍历过程遇到的第一个 G_OLD1,等待下次 gc 处理
  g->reallyold = g->old1;
  g->old1 = *psurvival;  /* 'survival' survivals are old now */
  g->survival = g->allgc;  /* all news are survivals */

  /* repeat for 'finobj' lists */
  dummy = NULL;  /* no 'firstold1' optimization for 'finobj' lists */
  psurvival = sweepgen(L, g, &g->finobj, g->finobjsur, &dummy);
  /* sweep 'survival' */
  sweepgen(L, g, psurvival, g->finobjold1, &dummy);
  g->finobjrold = g->finobjold1;
  g->finobjold1 = *psurvival;  /* 'survival' survivals are old now */
  g->finobjsur = g->finobj;  /* all news are survivals */

  sweepgen(L, g, &g->tobefnz, NULL, &dummy);
  // 完成 gc
  finishgencycle(L, g);
}

以上可以看出,g->survival, g->old1, g->reallyold, g->firstold1 都是一个游标,用来快速标记 g->allgc 链表上不同状态对象的边界。
新数据创建时,会加到 g->allgc 链表头部,所以遍历 g->allgc 到 g->survival 取得 G_NEW 对象;遍历
g->survival 到 g->old1 取得 G_SURVIVAL 对象;遍历 g->firstold1 到 g->reallyold 取得上一个 gc 周期新标记的 G_OLD1 对象。

备注:实际上,从 g->allgc 到 g->old1,中间还可能取得 G_OLD0 对象

重点看下以上代码涉及到的 sweepgen 和 finishgencycle:
sweepgen :分代回收

// lgc.c
/*
** Sweep for generational mode. Delete dead objects. (Because the
** collection is not incremental, there are no "new white" objects
** during the sweep. So, any white object must be dead.) For
** non-dead objects, advance their ages and clear the color of
** new objects. (Old objects keep their colors.)
*/
static GCObject **sweepgen (lua_State *L, global_State *g, GCObject **p,
                            GCObject *limit, GCObject **pfirstold1) {
  static const lu_byte nextage[] = {
    G_SURVIVAL,  /* from G_NEW */
    G_OLD1,      /* from G_SURVIVAL */
    G_OLD1,      /* from G_OLD0 */
    G_OLD,       /* from G_OLD1 */
    G_OLD,       /* from G_OLD (do not change) */
    G_TOUCHED1,  /* from G_TOUCHED1 (do not change) */
    G_TOUCHED2   /* from G_TOUCHED2 (do not change) */
  };
  int white = luaC_white(g);
  GCObject *curr;
  while ((curr = *p) != limit) {
    // 经历原子操作(atomic)后,白色对象就是垃圾数据
    if (iswhite(curr)) {  /* is 'curr' dead? */
      lua_assert(!isold(curr) && isdead(g, curr));
      *p = curr->next;  /* remove 'curr' from list */
      freeobj(L, curr);  /* erase 'curr' */
    }
    else {  /* correct mark and age */
      // 如果存活对象的状态是 G_NEW,状态改为 G_SURVIVAL,颜色改为当前白
      if (getage(curr) == G_NEW) {  /* new objects go back to white */
        int marked = curr->marked & ~maskgcbits;  /* erase GC bits */
        curr->marked = cast_byte(marked | G_SURVIVAL | white);
      }
      else {  /* all other objects will be old, and so keep their color */
        // 其他状态的对象,状态改到下一阶段
        setage(curr, nextage[getage(curr)]);
        if (getage(curr) == G_OLD1 && *pfirstold1 == NULL)
          *pfirstold1 = curr;  /* first OLD1 object in the list */
      }
      p = &curr->next;  /* go to next element */
    }
  }
  return p;
}

finishgencycle:完成 gc 回收

// lgc.c
/*
** Finish a young-generation collection.
*/
static void finishgencycle (lua_State *L, global_State *g) {
  // 处理所有的灰色链表
  correctgraylists(g);
  // 看情况调整string表大小
  checkSizes(L, g);
  g->gcstate = GCSpropagate;  /* skip restart */
  // 遍历 g->tobefnz 执行各自 metatable __gc 函数,对象移回 g->allgc
  if (!g->gcemergency)
    callallpendingfinalizers(L);
}

/*
** Correct all gray lists, coalescing them into 'grayagain'.
*/
static void correctgraylists (global_State *g) {
  // 先后处理 g->grayagain、g->weak、g->allweak、g->ephemeron,
  // 有效对象合并到 g->grayagain,其他几个设为 null
  GCObject **list = correctgraylist(&g->grayagain);
  *list = g->weak; g->weak = NULL;
  list = correctgraylist(list);
  *list = g->allweak; g->allweak = NULL;
  list = correctgraylist(list);
  *list = g->ephemeron; g->ephemeron = NULL;
  correctgraylist(list);
}

/*
** Correct a list of gray objects. Return pointer to where rest of the
** list should be linked.
** Because this correction is done after sweeping, young objects might
** be turned white and still be in the list. They are only removed.
** 'TOUCHED1' objects are advanced to 'TOUCHED2' and remain on the list;
** Non-white threads also remain on the list; 'TOUCHED2' objects become
** regular old; they and anything else are removed from the list.
*/
static GCObject **correctgraylist (GCObject **p) {
  GCObject *curr;
  while ((curr = *p) != NULL) {
    GCObject **next = getgclist(curr);
    // 白色对象从当前灰色链表移除
    if (iswhite(curr))
      goto remove;  /* remove all white objects */
    else if (getage(curr) == G_TOUCHED1) {  /* touched in this cycle? */
      // G_TOUCHED1 对象状态改 G_TOUCHED2,颜色变黑,对象在当前灰色
      // 链表保留,最后由 correctgraylists 合并到 g->grayagain
      lua_assert(isgray(curr));
      nw2black(curr);  /* make it black, for next barrier */
      changeage(curr, G_TOUCHED1, G_TOUCHED2);
      goto remain;  /* keep it in the list and go to next element */
    }
    else if (curr->tt == LUA_VTHREAD) {
      lua_assert(isgray(curr));
      goto remain;  /* keep non-white threads on the list */
    }
    else {  /* everything else is removed */
      // G_TOUCHED2 对象状态改 G_OLD,颜色变黑,从当前灰色链表移除
      lua_assert(isold(curr));  /* young objects should be white here */
      if (getage(curr) == G_TOUCHED2)  /* advance from TOUCHED2... */
        changeage(curr, G_TOUCHED2, G_OLD);  /* ... to OLD */
      nw2black(curr);  /* make object black (to be removed) */
      goto remove;
    }
    remove: *p = *next; continue;
    remain: p = next; continue;
  }
  return p;
}
2.2 major gc 的主要过程:
// lgc.c
/*
** Does a full collection in generational mode.
*/
static lu_mem fullgen (lua_State *L, global_State *g) {
  /*
   * gc 模式暂时改增量式,所有对象颜色变为白色,遍历所有对象,
   * 执行三色标记清除,最后将存活对象状态变为 G_OLD, 
   * 颜色变黑(部分变灰),gc 模式改为分代
   */
  enterinc(g);
  return entergen(L, g);
}

/*
** Enter incremental mode. Turn all objects white, make all
** intermediate lists point to NULL (to avoid invalid pointers),
** and go to the pause state.
*/
static void enterinc (global_State *g) {
  // 所有对象颜色变为白色
  whitelist(g, g->allgc);
  g->reallyold = g->old1 = g->survival = NULL;
  whitelist(g, g->finobj);
  whitelist(g, g->tobefnz);
  g->finobjrold = g->finobjold1 = g->finobjsur = NULL;
  // gc 模式改增量式,gc 状态改 GCSpause
  g->gcstate = GCSpause;
  g->gckind = KGC_INC;
  g->lastatomic = 0;
}

/*
** Enter generational mode. Must go until the end of an atomic cycle
** to ensure that all objects are correctly marked and weak tables
** are cleared. Then, turn all objects into old and finishes the
** collection.
*/
static lu_mem entergen (lua_State *L, global_State *g) {
  lu_mem numobjs;
  // 确保 gc 状态推进到 GCSpropagate,再执行 atomic
  luaC_runtilstate(L, bitmask(GCSpause));  /* prepare to start a new cycle */
  luaC_runtilstate(L, bitmask(GCSpropagate));  /* start new cycle */
  numobjs = atomic(L);  /* propagates all and then do the atomic stuff */
  // 清理垃圾数据,完成 gc
  atomic2gen(L, g);
  return numobjs;
}

/*
** Clears all gray lists, sweeps objects, and prepare sublists to enter
** generational mode. The sweeps remove dead objects and turn all
** surviving objects to old. Threads go back to 'grayagain'; everything
** else is turned black (not in any gray list).
*/
static void atomic2gen (lua_State *L, global_State *g) {
  cleargraylists(g);
  /* sweep all elements making them old */
  g->gcstate = GCSswpallgc;
  // 清理白色对象,存活的状态改 G_OLD,颜色变黑,协程和open upvalues除外
  sweep2old(L, &g->allgc);
  /* everything alive now is old */
  g->reallyold = g->old1 = g->survival = g->allgc;
  g->firstold1 = NULL;  /* there are no OLD1 objects anywhere */

  /* repeat for 'finobj' lists */
  sweep2old(L, &g->finobj);
  g->finobjrold = g->finobjold1 = g->finobjsur = g->finobj;

  sweep2old(L, &g->tobefnz);

  // gc 模式变回分代
  g->gckind = KGC_GEN;
  g->lastatomic = 0;
  g->GCestimate = gettotalbytes(g);  /* base for memory control */
  // 完成 gc,前文有解析
  finishgencycle(L, g);
}

3. 分代 gc 有哪些状态?

状态 说明
G_NEW 当前 gc 周期创建的对象,颜色白色
G_SURVIVAL G_NEW 对象存活到了下一个 gc 周期,经历 minor sweepgen 后,状态变 G_SURVIVAL,颜色仍是白色(改当前白)
G_OLD0 G_NEW 或 G_SURVIVAL 对象被 old 对象引用到后,向前标记,颜色白变灰(部分变黑),状态变为 G_OLD0
G_OLD1 G_SURVIVAL 对象和 G_OLD0 对象存活到了下一个 gc 周期,经历 minor sweepgen 后,会标记为 G_OLD1,颜色不变(白色会被清理掉)
G_OLD G_OLD1 对象存活到了下一个 gc 周期,经历 minor markold 后,会标记为 G_OLD, 颜色不变(白色会被清理掉)
G_TOUCHED1 黑色的 old 对象引用了其他白色对象,向后标记,颜色黑变灰,状态变为 G_TOUCHED1
G_TOUCHED2 G_TOUCHED1 对象存活到下一个 gc 周期,经历 minor sweepgen 后,状态变 G_TOUCHED2,颜色灰变黑

为什么要使用 G_OLD0?
白色对象被黑色对象引用时,会向前标记,颜色变灰(部分变黑),如果使用 G_OLD1,将导致这个对象在下一个 gc 周期开始前,就变成 G_OLD,这样, G_NEW 对象在一个 gc 周期内就变成了 old 对象,如果对象不再被使用,在之后的 minor gc 中不会被处理掉,不符合需求。

为什么要使用 G_TOUCHED1?
黑色的 old 对象引用了其他白色对象,会向后标记,颜色变灰,状态变 G_TOUCHED1。
其他对象可能是 old 对象,young 对象。过程中,其他对象可能被清理掉了。old 对象要等其引用的其他所有对象都检查过了,才能处理,所以 old 对象颜色先改成灰色,状态改成 G_TOUCHED1,加入 g->grayagain,等 g->gray 遍历结束后检查。

为什么要 G_TOUCHED2,为什么 G_TOUCHED1 不直接转 G_OLD?
对象状态由 G_TOUCHED1 转变为 G_TOUCHED2,颜色灰变黑,但不会从 g->grayagain 移除。等下次 gc 周期后,状态再由 G_TOUCHED2 转 G_OLD 后,才从 g->grayagain 移除

举个例子,说明这样做的目的:
黑色的 old 对象 A 引用了白色的 young 对象 B,这时候,A 的颜色变灰,状态变 G_TOUCHED1, 加入 g->grayagain, B 的颜色变灰,状态变 G_OLD0,加入 g->gray。 gc atomic 在遍历 B 可能发现他需要加入 g->weak 、g->allweak 或 g->ephemeron,这几个链表都滞后于 g->grayagain 处理, 所以 A 在当前 gc 周期结束后,颜色可以变黑,但仍要继续留在 g->grayagain, 状态记为 G_TOUCHED2

4. 分代 gc 怎么调优?

collectgarbage ([opt [, arg]])
这个函数是 lua 垃圾回收的通用接口,通过不同参数,对外提供了不同的功能:

"collect" 做一次完整的垃圾收集。 这是默认选项。
"stop" 停止垃圾收集器的运行。 在调用重启前,收集器只会因显式的调用运行。
"restart" 重启垃圾收集器的自动运行。
"count" 以 K 字节数为单位返回 Lua 使用的总内存数。 这个值有小数部分,所以只需要乘上 1024 就能得到 Lua 使用的准确字节数(除非溢出)。
"isrunning" 返回表示收集器是否在工作的布尔值 (即未被停止)。
"step" 单步运行垃圾收集器。 步长“大小”由 arg 控制。 传入 0 时,收集器步进(不可分割的)一步。 传入非 0 值, 收集器收集相当于 Lua 分配这些多(K 字节)内存的工作。 如果收集器结束一个循环将返回 true 。
"setpause" 将 arg 设为收集器的间歇率。 返回 间歇率 的前一个值。
"setstepmul" 将 arg 设为收集器的步进倍率。 返回 步进倍率 的前一个值。
"incremental" 设置垃圾收集器为增量模式。还支持3个参数,分别为 pause, stepmul, step,缺省值为 0, 不改变原设定。作用同上
"generational" 设置垃圾收集器为分代模式,还支持2个参数,分别为 minormul, majormul,缺省值为 0, 不改变原设定。作用后述

调整分代 gc 参数

collectgarbage("generational", minormul, majormul) 

实现:


// lapi.c
case LUA_GCGEN: {
  int minormul = va_arg(argp, int);
  int majormul = va_arg(argp, int);
  res = isdecGCmodegen(g) ? LUA_GCGEN : LUA_GCINC;
  if (minormul != 0)
    g->genminormul = minormul;
  if (majormul != 0)
    setgcparam(g->genmajormul, majormul);
  luaC_changemode(L, KGC_GEN);
  break;
}

minormul 控制 minor 回收的频率,默认值20,没有数值范围限制,建议最大值 200
majormul 控制 major 回收的频率,默认值100,没有数值范围限制,建议最大值 1000


// lgc.c
static void genstep (lua_State *L, global_State *g) {
  if (g->lastatomic != 0)  /* last collection was a bad one? */
    stepgenfull(L, g);  /* do a full step */
  else {
    lu_mem majorbase = g->GCestimate;  /* memory after last major collection */
    lu_mem majorinc = (majorbase / 100) * getgcparam(g->genmajormul);
    if (g->GCdebt > 0 && gettotalbytes(g) > majorbase + majorinc) {
      lu_mem numobjs = fullgen(L, g);  /* do a major collection */
      if (gettotalbytes(g) < majorbase + (majorinc / 2)) {
        /* collected at least half of memory growth since last major
           collection; keep doing minor collections */
        setminordebt(g);
      }
      else {  /* bad collection */
        g->lastatomic = numobjs;  /* signal that last collection was bad */
        setpause(g);  /* do a long wait for next (major) collection */
      }
    }
    else {  /* regular case; do a minor collection */
      youngcollection(L, g);
      setminordebt(g);
      g->GCestimate = majorbase;  /* preserve base value */
    }
  }
  lua_assert(isdecGCmodegen(g));
}

// lgc.c
static void setminordebt (global_State *g) {
  luaE_setdebt(g, -(cast(l_mem, (gettotalbytes(g) / 100)) * g->genminormul));
}

// lstate.c
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->GCdebt 的同时,保证总的内存始终等于 g->GCdebt + g->totalbytes
  g->totalbytes = tb - debt; 
  g->GCdebt = debt;
}

可以看出:
1. g->GCdebt 小于等于0,或者对比上次 major 回收,总的内存小于百分之 g->genmajormul,就会执行 minor 回收,否则执行 major 回收

2. 每次回收内存后, g->GCdebt 会设置为总的内存使用的百分之 g->genminormul 的负值 (bad collection 除外)

到这里,可能会有疑问, g->GCdebt 究竟是啥?

// lstate.h

l_mem GCdebt;  /* bytes allocated not yet compensated by the collector */

字面意思是债务,注释是,已申请内存的字节数(还没有被收集器偿还)

当内存申请、释放时,这个值会调整

// lmem.c
// 申请内存时
void *luaM_malloc_ (lua_State *L, size_t size, int tag) {
  if (size == 0)
    return NULL;  /* that's all */
  else {
    global_State *g = G(L);
    void *newblock = firsttry(g, NULL, tag, size);
    if (l_unlikely(newblock == NULL)) {
      newblock = tryagain(L, NULL, tag, size);
      if (newblock == NULL)
        luaM_error(L);
    }
    g->GCdebt += size;
    return newblock;
  }
}

// 释放内存时
void luaM_free_ (lua_State *L, void *block, size_t osize) {
  global_State *g = G(L);
  lua_assert((osize == 0) == (block == NULL));
  (*g->frealloc)(g->ud, block, osize, 0);
  g->GCdebt -= osize;
}

当 GCdebt > 0 时,收集器就要工作。

// 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)

// lgc.c
void luaC_step (lua_State *L) {
  global_State *g = G(L);
  lua_assert(!g->gcemergency);
  if (gcrunning(g)) {  /* running? */
    if(isdecGCmodegen(g))
      genstep(L, g);
    else
      incstep(L, g);
  }
}

以上,当 g->GCdebt > 0 , 执行 genstep,将 g->GCdebt 设置为负值,避免每次自动检查都触发 gc 收集。当总的内存使用提高了百分之 g->genminormul,使得 g->GCdebt > 0,执行 genstep,也就是说,minormul 的值越大,gc 触发的频率越低。可以看出,这个参数同时影响了 minor 和 major 收集

lua 中,除了自动 gc 检查,用户还可以手动执行 collectgarbage("step") ,来触发一次 gc

// lapi.c lua_gc
case LUA_GCSTEP: {
  int data = va_arg(argp, int);
  l_mem debt = 1;  /* =1 to signal that it did an actual step */
  lu_byte oldstp = g->gcstp;
  g->gcstp = 0;  /* allow GC to run (GCSTPGC must be zero here) */
  if (data == 0) {
    luaE_setdebt(g, 0);  /* do a basic step */
    luaC_step(L);
  }
  else {  /* add 'data' to total debt */
    debt = cast(l_mem, data) * 1024 + g->GCdebt;
    luaE_setdebt(g, debt);
    luaC_checkGC(L);
  }
  g->gcstp = oldstp;  /* restore previous state */
  if (debt > 0 && g->gcstate == GCSpause)  /* end of cycle? */
    res = 1;  /* signal it */
  break;
}

当 step 参数为 nil 或 0时,g->GCdebt 会被设置为 0,再执行 genstep, g->GCdebt 满足小于等于 0,将执行 youngcollection, 完成 minor 收集

不过,在分代 gc 下,collectgarbage("step") 不推荐使用。

有两个问题:
问题1: 结合 genstep 的处理,每次执行后,g->GCdebt 会重置为总内存的百分之 g->genminormul 的负值,容易使得 major 收集一直没有机会触发。
由 youngcollect 可以看出, minor 收集只清理 g->allgc 和 g->finobj 中 G_NEW 和 G_SURVIVAL 状态的垃圾数据,及 g->tobefnz 中的垃圾数据。
如果想做一次完整的 gc,使用 collectgarbage("collect") 或 collectgarbage()

问题2:接连两次调用会让当前 young 数据变成 old 数据。

Lua 5.4.4  Copyright (C) 1994-2022 Lua.org, PUC-Rio
> t = {}
> for k=1, 10000 do table.insert(t, k) end
> t = nil
> collectgarbage "count"
277.9267578125
> collectgarbage "step"
false
> collectgarbage "count"
23.4033203125

以上是预期的结果,但如果是下面这种调用,结果有点问题

Lua 5.4.4  Copyright (C) 1994-2022 Lua.org, PUC-Rio
> t = {}
> for k=1, 10000 do table.insert(t, k) end
> collectgarbage "step"
false
> collectgarbage "step"
false
> t = nil
> collectgarbage "step"
false
> collectgarbage "step"
false
> collectgarbage "count"
277.4521484375
> 

以上, 表 t 经过两次 gc 后,变成了 old 数据,无法在 minor 过程中回收,这种情况下,要进行一次完整的 gc

结束语

本文是在前文《lua5.3 垃圾回收分析》继续往下写的,所以部分内容没有复述,建议一起阅读。

最后,感谢阅读,有问题可以撩我。

发表评论

邮箱地址不会被公开。 必填项已用*标注