高级内存管理(二)

简介: 接上篇高级内存管理,这篇分析一下lua的gc机制## lua的gc概述lua的垃圾回收gc是一个很经典的实现。本文是基于的lua5.3.6版本的源码进行分析。lua的gc算法采用了标记清除算法。在Lua中字符串、表、用户数据、函数、线程、 内部结构等对象,都使用GC模块进行管理。## 标记清除算法该算法分为两个阶段:标记阶段:gc进行回收过程,从若干根节点依次遍历进行标记,这样就把

接上篇高级内存管理,这篇分析一下lua的gc机制

lua的gc概述

lua的垃圾回收gc是一个很经典的实现。本文是基于的lua5.3.6版本的源码进行分析。lua的gc算法采用了标记清除算法。在Lua中字符串、表、用户数据、函数、线程、 内部结构等对象,都使用GC模块进行管理。

标记清除算法

该算法分为两个阶段:

标记阶段:gc进行回收过程,从若干根节点依次遍历进行标记,这样就把所有可以访问到的节点都进行了标记
清除阶段:遍历所有的节点,把上述标记阶段没有标记的节点全部进行清除

这个就是该算法的核心思想,lua早期版本只采用黑白两色标记,导致gc过程需要一次完成,后期版本采取了三色标记法,加入了一个灰色这样可以增量完成。

三色介绍

颜色就是算法描述的标记阶段标记,5.3版本的总共用了黑白灰三个颜色标记,下面描述一下这三个颜色, 这里特别说明下白色分为当前白和其他白。

白色:表示可以回收的状态,新创建的对象的初始状态就应该被设定为白色,如果该对象在GC标记阶段结束后,仍然为白色则此时白色代表当前对象为可回收状态。但其实本质上白色的设定就是为了标识可回收。这里的白色都是指代当前白。

灰色:中间状态,当前对象已经被GC访问过,但是该对象引用的其他对象还没有被标记。

黑色:不可回收状态,当前对象已经访问过了,并且该对象引用的其他对象也被标记过。

特殊:当前白和其他白。原因是gc在标记阶段和清除阶段之间,lua虚拟机仍然可能创建新的对象。这个时候如果还被标记当前白,就会导致错误的清除该对象,所有设计了这两个白色。这个时候用其他白作为新对象的标记色。清除的时候仍然可以合理清除掉。然后清除完毕了把黑色的设置为其他白, 然后虚拟机切换自己的当前白为其他白,这里设计比较巧妙。总之就是有两个白色,虚拟机在每轮gc的时候回来回切换白色,另外一个白色就降为其他白。

gc的具体实现

在分析gc的具体实现,先来做一些准备工作

数据对象的创建

//lobject.h
 
 /*
 ** Common type for all collectable objects
 */
 typedef struct GCObject GCObject;
 
 
 /*
 ** Common Header for all collectable objects (in macro form, to be
 ** included in other objects)
 */
 #define CommonHeader    GCObject *next; lu_byte tt; lu_byte marked
 
 
 /*
 ** Common type has only the common header
 */
 struct GCObject {
   CommonHeader;
 };
 
/*
 ** Union of all Lua values
 */
 typedef union Value {
   GCObject *gc;    /* collectable objects */
   void *p;         /* light userdata */
   int b;           /* booleans */
   lua_CFunction f; /* light C functions */
   lua_Integer i;   /* integer numbers */
   lua_Number n;    /* float numbers */
 } Value;
 
 #define TValuefields    Value value_; int tt_
 
 typedef struct lua_TValue {
   TValuefields;
 } TValue;

lua中的数据全部是以TValue来实现的,具体该数据是什么类型依赖tt_字段,之类分为需要被gc管理回收的和不需要。需要被管理的数据类型有TString、Udata、Cloure、Table、Proto、lua_State这些。 需要被管理的都可以统一转换为GCObject类型,他们都有一个共同的部分CommonHeader,这里有三个字段next,tt和marked, next是指向下一个元素的指针,用于保存所有的GCObject, tt是数据类型和tt_一致,marked是上述算法提到的颜色标记

接下来我们来看看创建数据类型的函数,创建所有的gc回收的函数最后都会调用(lgc.c)luaC_newobj这个函数,以table创建为例

//ltable.c
Table *luaH_new (lua_State *L) {
  GCObject *o = luaC_newobj(L, LUA_TTABLE, sizeof(Table));
  Table *t = gco2t(o);
  t->metatable = NULL;
  t->flags = cast_byte(~0);
  t->array = NULL;
  t->sizearray = 0;
  setnodevector(L, t, 0);
  return t;
}

//lgc.c
Object *luaC_newobj (lua_State *L, int tt, size_t sz) {
  global_State *g = G(L);
  GCObject *o = cast(GCObject *, luaM_newobject(L, novariant(tt), sz));
  o->marked = luaC_white(g);
  o->tt = tt;
  o->next = g->allgc;
  g->allgc = o;
  return o;
}

#define luaC_white(g)    cast(lu_byte, (g)->currentwhite & WHITEBITS)

从创建函数luaC_newobj我们可以看到主要做了三件事

  1. 创建table所需要的内存,通过调用luaM_newobject来创建(lua本身没有使用内存池, 就是用的操作系统提供的realloc函数进行分配的)
  2. 颜色标记为当前白, 这里使用的就是(g)->currentwhite 来进行操作的
  3. 把该对象挂载到g->allgc链表上, GC的所需要管理的所有生命周期的元素都在这个链表上

我们知道的数据类型创建以及位置,下面就可以进入正式的gc之旅了

gc触发

gc触发分为手动和自动触发

//手动触发
LUAI_FUNC void luaC_step (lua_State *L);

//自动触发
#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)

手动触发没啥好说的,直接调用触发,自动触发机制需要满足GC的条件,这个条件通过g-> GCdebt、g-> totalbytes等参数计算来触发,本文就不讨论这部分内容。这两个最后都会调用到gc最重要的一个函数singlestep。

singlestep内部就是一个简单的状态机,会根据g->gcstate执行特定的环节的操作, 默认状态是GCSpause

标记阶段

GCSpause

//lgc.c
/*
** mark root set and reset all gray lists, to start a new collection
*/
static void restartcollection (global_State *g) {
  g->gray = g->grayagain = NULL;
  g->weak = g->allweak = g->ephemeron = NULL;
  markobject(g, g->mainthread);
  markvalue(g, &g->l_registry);
  markmt(g);
  markbeingfnz(g);  /* mark any finalizing object left from previous cycle */
}

switch (g->gcstate) {
      case GCSpause: {
      g->GCmemtrav = g->strt.size * sizeof(GCObject*);
      restartcollection(g);
      g->gcstate = GCSpropagate;
      return g->GCmemtrav;
      }
}

这个阶段是gc的开始,主要是restartcollection, 其他是记录信息用作gc启动条件的,简单提一下。 目前阶段不存在任何的灰色对象。进行初始化清空灰色对象链表,其中g->gray是灰色节点链;g->grayagain是需要原子操作标记的灰色节点。其他三个示弱表链表这里不讨论。

然后依次标记markobject、markvalue、markmt、markbeingfnz。 这些分别对mainthread(主线程(协程), 注册表(registry), 全局元表(metatable), 上次GC循环中剩余的finalize中的对象。就是从root依次遍历可以到达的对象进行标记
这里底层会调用reallymarkobject, 这个函数会按照数据类型进行灰色或者黑色标记,灰色的加入grey链表里

/*
** mark an object. Userdata, strings, and closed upvalues are visited
** and turned black here. Other objects are marked gray and added
** to appropriate list to be visited (and turned black) later. (Open
** upvalues are already linked in 'headuv' list.)
*/
static void reallymarkobject (global_State *g, GCObject *o) {
 reentry:
  white2gray(o);
  switch (o->tt) {
    case LUA_TSHRSTR: {
      gray2black(o);
      g->GCmemtrav += sizelstring(gco2ts(o)->shrlen);
      break;
    }
    case LUA_TLNGSTR: {
      gray2black(o);
      g->GCmemtrav += sizelstring(gco2ts(o)->u.lnglen);
      break;
    }
    case LUA_TUSERDATA: {
      TValue uvalue;
      markobjectN(g, gco2u(o)->metatable);  /* mark its metatable */
      gray2black(o);
      g->GCmemtrav += sizeudata(gco2u(o));
      getuservalue(g->mainthread, gco2u(o), &uvalue);
      if (valiswhite(&uvalue)) {  /* markvalue(g, &uvalue); */
        o = gcvalue(&uvalue);
        goto reentry;
      }
      break;
    }
    case LUA_TLCL: {
      linkgclist(gco2lcl(o), g->gray);
      break;
    }
    case LUA_TCCL: {
      linkgclist(gco2ccl(o), g->gray);
      break;
    }
    case LUA_TTABLE: {
      linkgclist(gco2t(o), g->gray);
      break;
    }
    case LUA_TTHREAD: {
      linkgclist(gco2th(o), g->gray);
      break;
    }
    case LUA_TPROTO: {
      linkgclist(gco2p(o), g->gray);
      break;
    }
    default: lua_assert(0); break;
  }
}

可以看到string和userdata直接标记为黑

GCSpropagate

/*
** traverse one gray object, turning it to black (except for threads,
** which are always gray).
*/
static void propagatemark (global_State *g) {
  lu_mem size;
  GCObject *o = g->gray;
  lua_assert(isgray(o));
  gray2black(o);
  switch (o->tt) {
    case LUA_TTABLE: {
      Table *h = gco2t(o);
      g->gray = h->gclist;  /* remove from 'gray' list */
      size = traversetable(g, h);
      break;
     …

    default: lua_assert(0); return;
  }

  g->GCmemtrav += size;
}

case GCSpropagate: {
      g->GCmemtrav = 0;
      lua_assert(g->gray);
      propagatemark(g);
      if (g->gray == NULL)  /* no more gray objects? */
       g->gcstate = GCSatomic;  /* finish propagate phase */
      return g->GCmemtrav;  /* memory traversed in this step */
  }

在该阶段就是从灰色链表取一个对象,然后标记黑色,根据它的类型分别进行遍历。图里就是table类型,调用traversetable进行遍历,把能访问到的对象进行标记(也是通过reallymarkobject进行的标记。

这里要说明一下的是,可以看到这里不是一个循环而是执行一次,当gray == NULL才会设置下一个状态。这里好处就可以减少阻塞的时间,更加及时的响应lua虚拟机运行

GCSatomic

case GCSatomic: {
      lu_mem work;
      propagateall(g);  /* make sure gray list is empty */
      work = atomic(L);  /* work is what was traversed by 'atomic' */
      entersweep(L);
      g->GCestimate = gettotalbytes(g);  /* first estimate */;
      return work;
    }
    
static void propagateall (global_State *g) {
      while (g->gray) propagatemark(g);}    

关于写屏障机制luaC_barrier这里提一下,因为我们都是单步执行的不像之前是stop the world一次搞完。所以会有一种情况我标记了黑色,然后修改了它,这时候就需要这个机制根据标记颜色进行再次修订,让gc感知到这个变化。

回到代码会发现先调用了propagateall再次检查gray链表,这是因为写屏障机制luaC_barrier会操作gray这个链表

对于atomic函数,这里只关乎主要流程,忽略一些细节

  1. 重新遍历根对象(当前thread, 注册表l_registry, 全局metatable, 线程相关的上值), 主要是因为这些值中有些是已经标记过但还可能会被修改,并且是没有使用写屏障的。还有这些值已经标记过不会在处理所以不会造成重复计算。
  2. 遍历一些grayagain,然后清理。这里提一下这个用于存放一些易变化的对象同时也有弱表。跟gray链表有点像但是单独存放。
  3. 将当前白色值切换到新一轮的白色值(在此之后,所有新建的对象虽然也为白色,但是在GC循环走完之前并不会被回收),

接下来是entersweep(L)函数

/*
** Enter first sweep phase.
** The call to 'sweeplist' tries to make pointer point to an object
** inside the list (instead of to the header), so that the real sweep do
** not need to skip objects created between "now" and the start of the
** real sweep.
*/
static void entersweep (lua_State *L) {
  global_State *g = G(L);
  g->gcstate = GCSswpallgc;
  lua_assert(g->sweepgc == NULL);
  g->sweepgc = sweeplist(L, &g->allgc, 1);
}

这里要说明的是通过sweeplist遍历g->allgc,然后g->sweepgc记录当前的位置。sweeplist的第三个参数1代表遍历访问几个元素。
这里之所以这样设计在该原子操作阶段进行一次清除操作, 该阶段结束后又会进行增量模式,添加的新对象是放在g->allgc表头的而且必然不会被回收的。这样可以尽可能的避免这种干扰。

GCSatomic状态之后又进入增量模式,接着就是GCSswpallgc状态。

清除阶段

    case GCSswpallgc: {  /* sweep "regular" objects */
      return sweepstep(L, g, GCSswpfinobj, &g->finobj);
    }
    case GCSswpfinobj: {  /* sweep objects with finalizers */
      return sweepstep(L, g, GCSswptobefnz, &g->tobefnz);
    }
    case GCSswptobefnz: {  /* sweep objects to be finalized */
      return sweepstep(L, g, GCSswpend, NULL);
    }
    case GCSswpend: {  /* finish sweeps */
      makewhite(g, g->mainthread);  /* sweep main thread */
      checkSizes(L, g);
      g->gcstate = GCScallfin;
      return 0;
    }
    case GCScallfin: {  /* call remaining finalizers */
      if (g->tobefnz && g->gckind != KGC_EMERGENCY) {
        int n = runafewfinalizers(L);
        return (n * GCFINALIZECOST);
      }
      else {  /* emergency mode or no more finalizers */
        g->gcstate = GCSpause;  /* finish collection */
        return 0;
      }
    }

这个过程就是垃圾分类回收

GCSswpallgc就是通过上述的sweeplist增量的把g->allgc上面的死对象回收掉并且将或对象标记为当前白

GCSswpfinobj,GCSswptobefnz也是调用sweeplist进行清除并且标记,主要是对global_State.finobj和global_State.tobefnz链表,这里是用于当对象回收时候想要调用一些自己设置的回收函数,比如关闭连接等等之类的。

GCSswpend用来释放mainthread上的一些空间,如:字符串表,连接缓冲区等。

整个大循环最后一步就是GCScallfin,会遍历global_State.tobefnz链表上的对象,然后调用其__gc函数(就是自定义回收函数),然后把它放入global_State.allgc链表中,会在下个GC循环回回收

总结

到这gc的算法已经分析完毕了,像弱表,写屏障,greyagain,global_State.finobj以及gc触发机制都只是简单提了一下,和本主题不是强相关就忽略了。

最后总结一下该算法

1.用一个global_State.allgc保存所有的要管理的内存对象

2.从root节点开始标记能访问到的对象。GC只要发现一个可回收对象就标记为灰,然后遍历它所能访问到的对象

3.该对象没有可以访问的,标记为黑

4.递归重复2,3直到完成所有可访问的对象被遍历完毕为止

5.GC清除掉所有白色对象

写在最后,欢迎有任何建议和想法或者指出哪里写的不足的地方的童鞋联系我~~~

相关文章
|
6月前
|
存储 调度
操作系统基础:内存管理概述【下】
操作系统基础:内存管理概述【下】
|
6月前
|
算法
操作系统基础:内存管理概述【上】
操作系统基础:内存管理概述【上】
|
3月前
|
存储 程序员 C++
内存管理概念 (二)
内存管理概念 (二)
52 1
|
3月前
|
存储 算法 程序员
内存管理概念(一)
内存管理概念(一)
82 0
|
5月前
|
存储 编译器 C语言
【C++】学习笔记——内存管理
【C++】学习笔记——内存管理
52 15
|
6月前
|
存储 安全 程序员
C++语言中的内存管理技术
C++语言中的内存管理技术
|
5月前
|
存储 缓存 人工智能
深入探讨现代操作系统的内存管理机制
在不断发展的计算机科学领域,内存管理一直是操作系统设计中的关键问题。本文将深入探讨现代操作系统中使用的各种内存管理技术,包括虚拟内存、分页、分段和缓存策略。通过分析这些技术的实现原理和实际应用,我们不仅能了解它们如何提升系统性能,还能看出它们在不同场景下的优缺点。
118 0
|
6月前
|
存储 调度
操作系统基础:内存管理概述【中】
操作系统基础:内存管理概述【中】
|
6月前
|
存储 缓存 Linux
嵌入式Linux中内存管理详解分析
嵌入式Linux中内存管理详解分析
92 0
|
存储 Linux C语言
c++学习之内存管理
c++学习之内存管理
190 1