lua5.3 gc总览分析(1)

lua可回收对象

lua从堆中申请内存以及释放,都是通过gc垃圾回收器来管理的。

lua可回收的内存对象有:TString(字符串),Table(表),Udata(用户数据),Closure(分为lua闭包和c闭包),Proto(函数原型),lua_State(线程)

/*
** 在 lstate.h 头文件中罗列了所有可回收的对象结构体类型
** Union of all collectable objects (only for conversions)
*/
union GCUnion {
GCObject gc; /* common header */
struct TString ts;
struct Udata u;
union Closure cl;
struct Table h;
struct Proto p;
struct lua_State th; /* thread */
};

每个可回收的对象头部都有一个 CommonHeader 宏,定义在 lobject.h 头文件中。目的就是为了表示这个结构体是可回收的对象,且需要挂在 g->allgc 单向链表中。

#define CommonHeader GCObject *next; lu_byte tt; lu_byte marked

每个对象通过 next 指针指向下一个对象,而 g->allgc 总是指向最新分配的那个对象,这样就构成一个单向链表。

标记清除算法介绍

在创建新的可回收对象时,都会把它放到一个 g->allgc 的单向链表中串连起来,方便管理其生命周期,管理用到的算法,lua5.3 采用的是标记清除算法,该算法主要包括两个阶段,一个是标记阶段,另一个是清除阶段。

标记阶段:垃圾回收器首先从根节点(主线程,注册表)出发,对其引用到的对象进行遍历标记。如下图所示,a1, a2 所引用的对象在栈上,会被标记到,而不被引用的 TString、Proto 这两个对象就没有被标记。

 

 

 

清除阶段:遍历 g->allgc 单向链表,如果发现对象没有被标记过,就进行释放。如下图所示, TString、Proto 两个对象没有在栈中,或者注册表,或者其他地方引用到,就会被释放掉。

步进式gc

虽然标记清除算法原理简单,但如果是一步到位,那么对象多了,就会导致程序在某一个时刻,出现卡顿,stop the world,卡顿多久完全不可控。所以 lua5.1之后的版本,改进了该算法,采用步进式标记清除。顾名思义,就是每标记一部分对象后,就会中断 gc 操作,继续运行 lua 程序代码,然后在合适的时机,内存达到某个阀值,又切回到 gc 操作,继续标记,直到把所有的对象都标记完后,才进入清除阶段。清除阶段也是同理,在释放一定数量的对象后,退出 gc 操作,回到 lua 层继续执行其他代码,等到申请的内存又涨到某个阀值时,就会切回到 gc 操作,继续清除释放一部分对象,直到 allgc 链表遍历完,如下图所示。

相比 stop the world,步进式 gc 把标记,清除的过程拆分成一小段,一小段,每次在 lua 层运行一段时间后,即申请一定数量的内存对象后,进入 gc 操作,执行一小段 gc step(标记或者清除一小部分对象),这样就可以做到很平滑的执行一轮 gc,lua 层也不会因为有 gc 的存在,而卡在某个地方等很久。 

当然,步进式 gc 原理听起来也是比较简单,但看 lua 的具体实现却不怎么简单。下面就分析下 lua5.3 的标记清除算法实现。

对象颜色

之前说过,可回收的对象在标记阶段,会被标记,那它是标记成什么样子的呢,在lua5.3中定义了三种颜色,白色,灰色,黑色。而每个可回收对象,都有一个 marked 字段,可以看下上面讲到的宏定义 CommonHeader,marked 就是用来记录当前gc对象处于哪种颜色的。

lua gc实现原理

新创建的对象都是被标记为白色,然后在 gc 开启后,进入标记阶段,第一步,从主线程,全局注册表,全局元表开始扫描,将扫描到的对象置为灰色,然后加入到 gray 灰色链表,第二步,从 gray 灰色链表上摘下一个对象,标记为黑色,然后对这个对象所引用的其他对象继续扫描标记为灰色,加入 gray 灰色链表,如此反复,直到 gray 链表为空,所有的对象要么是白色,要么为黑色,标记阶段也就算完成了,最后进入清除阶段。清除阶段,就是从头遍历 allgc 链表,如果对象是白色,证明标记阶段,该对象没有再被其他地方引用到,那么就需要 free 释放,如果对象是黑色,证明有被其他地方引用到,只需把颜色更新为白色就可以了,完成一轮 gc 操作。

如图所示,元素 a1,a2,a3,a4 都被栈引用着,会被标记为灰色,再从灰色变成黑色,标记阶段完成。

图1

上面提到了白色,其实是分成2种白的,即 white0 white1,为啥白色会有两种白呢,那是因为 lua gc 是步进式的,在清除阶段,有可能清除一部分对象后,就退出 gc step,回到 lua 脚本层继续执行,在执行 lua 代码过程中,有可能会产生新的对象,这个新的对象可能被栈,或者栈上的表,或者闭包等其他地方引用着,然后呢,颜色是白色的,不是黑色,就会被当作未被标记,而清除掉,一个被引用着的对象,还会被清除,那肯定是不行的。所以,为了解决清除阶段新创建的对象因白色而被清除的问题,引入了两种白色。比如本轮  gc,标记阶段用的白色是 white0,新建的对象以及未被引用的对象都是 white0,如果对象有被引用,white0 最终会被切成黑色 black,到了清除阶段,新创建的对象,白色用 white1 来表示,目的是用来区分标记阶段的白色 white0,这样就可以回收标记阶段未被引用的所有 white0 白色对象。

在进入清除阶段那刻,我们会用 g->sweeplist 指针指向 g->allgc 链表头部,用 g->sweeplist 代替 g->allgc,我们遍历 g->sweeplist 链表上的对象,为白色 white0 的就清除(c1,c2),只保留黑色  black 对象(a1,a2),并且把 a1,a2 黑色切换成当前白色 white1。如果在清除阶段,有新创建的对象(对象颜色为 white1),就插到 g->allgc 头部。这样就可以做到,在清除阶段,一条单向链表,新创建的对象由 allgc 指针串连,释放对象则由 sweeplist 指针来遍历。

白色对象

接下来,我们再对两种白加深下印象

(每一轮过后的 ... 表示暂停一段时间后,才开始进入下一轮 gc)

我们从上图中可以观察到,每一轮 gc,标记阶段新对象用到白色,和清除阶段用的白色不一样,并且得出结论,本轮标记阶段用到的白色和上一轮清除阶段的白色是同一种白。比如,在第二轮 gc 中,清除阶段,清除的白色对象,其实包括了第一轮的 white1 + 第二轮标记阶段的 white1。同理,在第三轮 gc 中,清除阶段,要清除的白色对象就是 第二轮的 white0 + 第三轮标记阶段的 white0,如此反复切换白色。

可能大家会有疑问,如果在标记阶段,新创建的对象是白色,而在清除阶段,新创建的对象直接标记为黑色,而不是白色,是不是也可以做到区分 对象是在标记阶段创建的,还是清除阶段创建的呢?

这样就可以做到不用两种白色了呢,答案嘛,我个人觉得应该是可以的,因为在清除阶段创建的对象,是不能被回收的。标记为黑色,确实也可以阻止回收,但这些新增的对象就需要在一轮完整 gc 过后,原子遍历一次,把黑色重置为白色,有时间开销。如下图,m1,m2,就需要被重新遍历一次了。又或者你会说,不管它,等到下一轮 gc 再处理不就行了吗,我觉得也是可以的,但下一轮 gc 只能在清除阶段做到把这些黑色对象翻转为 白色,还不能做到及时清理,需要等下下轮才有机会 free,这就意味着需要2轮 gc 才有可能回收 清除阶段的黑色对象,这种等待时间更久,不再被引用的对象会在内存中存活很长时间,导致清理不及时。所以说,两种白色在完整的 gc 中,轮流切换,效率更高。

灰色对象以及 gray 链表

接下来说下灰色,以及灰色链表的作用

再回顾下,对于标记阶段,为了识别对象哪些被引用,哪些没有被引用,我们引入了颜色的概念,去标记区分没有被引用到的对象是白色,有被引用到的,最终会被记为黑色,在清除阶段,最终清除掉所有没有再被引用到的白色对象。在标记和清除阶段过程中,lua gc 都是步进式的,每次 gc 只会处理一小部分对象,就回到 lua 层继续执行其他代码。

如果我们把栈上的对象,直接由白色标记为黑色,行不行呢?

感觉好像是可以,但我们要考虑一下,一次性标记多少个对象呢,对象如果又引用了其他的对象,我们应不应该原子操作去继续标记呢,还有要考虑在支持步进式 gc 的情况下,我们需要保存当前遍历的对象的上下文环境,以便知道后续能恢复从哪个对象开始继续遍历,还有如果遍历过的对象,又引用了新的白色对象该怎么办等等,一大堆情况要考虑。

所以,为了解决上述问题,以及简化实现,lua gc 引入了灰色,作为白色切到黑色的中间过渡状态。为了记录当前访问到了哪个灰色对象,还需要有一个地方存放着这些灰色对象,而这个集合的地方,就是 gray 灰色链表。

概括下灰色状态以及灰色链表作用

灰色状态:当前对象为待扫描状态,表示该对象已被 gc 访问过了,但该对象引用的其他对象还没有被访问过。

大家可以再看一下原理那节的图1。有了灰色以及 gray 链表的加入,标记清除算法就不需要一次性执行完,可以拆分成多次,每次往前推进一点,直到结束。新创建的白色对象,只要有被引用到,最终也会被加入到 gray 链表中,而 gray 链表只要有待访问的对象,标记阶段就会一直持续下去,直到 gray 链表为NULL,才进入清除阶段。

posted @   墨色山水  阅读(54)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)
点击右上角即可分享
微信分享提示