最近发现在大数据量的 lua 环境中,GC 占据了很多的 CPU 。差不多是整个 CPU 时间的 20% 左右。希望着手改进。这样,必须先对 lua 的 gc 算法极其实现有一个详尽的理解。我之前读过 lua 的源代码,由于 lua 源码版本变迁,这个工作还需要再做一次。这次我重新阅读了 lua 5.1.4 的源代码。从今天起,做一个笔记,详细分析一下 lua 的 gc 是如何实现的。阅读代码整整花掉了我一天时间。但写出来恐怕比阅读时间更长。我会分几天写在 blog 上。

Lua 采用一个简单的标记清除算法的 GC 系统。

在 Lua 中,一共只有 9 种数据类型,分别为 nil 、boolean 、lightuserdata 、number 、string 、 table 、 function 、 userdata 和 thread 。

其中,只有 string table function thread 四种在 vm 中以引用方式共享,是需要被 GC 管理回收的对象。其它类型都以值形式存在。

但在 Lua 的实现中,还有两种类型的对象需要被 GC 管理。分别是 proto (可以看作未绑定 upvalue 的函数), upvalue (多个 upvalue 会引用同一个值)。

Lua 是以 union + type 的形式保存值。具体定义可见 lobject.h 的 56 - 75 行:

typedef union
{
GCObject *gc;
void *p;
lua_Number n;
int b;
} Value; #define TValuefields Value value;
int tt; typedef struct lua_TValue
{
TValuefields;
} TValue;

  我们可以看到,Value 以 union 方式定义。如果是需要被 GC 管理的对象,就以 GCObject 指针形式保存,否则直接存值。在代码的其它部分,并不直接使用 Value 类型,而是 TValue 类型。它比 Value 多了一个类型标识。用 int tt 记录。通常的系统中,每个 TValue 长为 12 字节。btw, 在The implementation of Lua 5.0中作者讨论了,在 32 位系统下,为何不用某种 trick 把 type 压缩到前 8 字节内。

  所有的 GCObject 都有一个相同的数据头,叫作 CommonHeader ,在 lobject.h 里 43 行以宏形式定义出来的。使用宏是源于使用上的某种便利。C 语言不支持结构的继承。

#define CommonHeader GCObject *next; 
lu_byte tt;
lu_byte marked;
/*GCObject 定义里拥有CommonHeader 实现了单链表结构功能*/

  从这里我们可以看到:所有的 GCObject 都用一个单向链表串了起来。每个对象都以 tt 来识别其类型。marked 域用于标记清除的工作。

  标记清除算法是一种简单的 GC 算法。每次 GC 过程,先以若干根节点开始,逐个把直接以及间接和它们相关的节点都做上标记。对于 Lua ,这个过程很容易实现。因为所有 GObject 都在同一个链表上,当标记完成后,遍历这个链表,把未被标记的节点一一删除即可。

  Lua 在实际实现时,其实不只用一条链表维系所有 GCObject 。这是因为 string 类型有其特殊性。所有的 string 放在一张大的 hash 表中。它需要保证系统中不会有值相同的 string 被创建两份。顾 string 是被单独管理的,而不串在 GCObject 的链表中。

  回头来看看lua_State这个类型。这是写 C 和 Lua 交互时用的最多的数据类型。顾名思义,它表示了 lua vm 的某种状态。从实现上来说,更接近 lua 的一个 thread 以及其间包含的相关数据(堆栈、环境等等)。事实上,一个lua_State也是一个类型为 thread 的 GCObject 。见其定义于 lstate.h 97 行。

struct lua_State
{
CommonHeader;
lu_byte status;
StkId top;
StkId base;
global_State *l_G;
CallInfo *ci; //当前函数的调用信息
const Instruction *savedpc;
StkId stack_last;
StkId stack;
CallInfo *end_ci;
CallInfo *base_ci;
int stacksize;
int size_ci;
unsigned short nCcalls;
unsigned short baseCcalls;
lu_byte hookmask;
lu_byte allowhook;
int basehookcount;
int hookcount;
lua_Hook hook;
TValue l_gt;
TValue env;
GCObject *openupval;
GCObject *gclist;
struct lua_longjmp *errorJmp;
ptrdiff_t errfunc;
};

  一个完整的 lua 虚拟机在运行时,可有多个lua_State,即多个 thread 。它们会共享一些数据。这些数据放在global_State *l_G域中。其中自然也包括所有 GCobject 的链表。

  所有的 string 则以 stringtable 结构保存在 stringtable strt 域。string 的值类型为 TString ,它和其它 GCObject 一样,拥有 CommonHeader 。但需要注意,CommonHeader 中的 next 域却和其它类型的单向链表意义不同。它被挂接在 stringtable 这个 hash 表中。

  除 string 外的 GCObject 链表头在 rootgc ( lstate.h 75 行)域中。初始化时,这个域被初始化为主线程。见 lstate.c 170 行,lua_newstate函数中:

g->rootgc = obj2gco(L);

每当一个新的 GCobject 被创建出来,都会被挂接到这个链表上,挂接函数有两个,在 lgc.c 687 行的

void luaC_link (lua_State *L, GCObject *o, lu_byte tt)
{
global_State *g = G(L);
o->gch.next = g->rootgc;
g->rootgc = o;
o->gch.marked = luaC_white(g);
o->gch.tt = tt;
} void luaC_linkupval (lua_State *L, UpVal *uv)
{
global_State *g = G(L);
GCObject *o = obj2gco(uv);
o->gch.next = g->rootgc;
g->rootgc = o;
if (isgray(o))
{
if (g->gcstate == GCSpropagate)
{
gray2black(o);
luaC_barrier(L, uv, uv->v);
}
else
{
makewhite(g, o);
lua_assert(g->gcstate != GCSfinalize && g->gcstate != GCSpause);
}
}
}

  upvalue 在 C 中类型为 UpVal ,也是一个 GCObject 。但这里被特殊处理。为什么会这样?因为 Lua 的 GC 可以分步扫描。别的类型被新创建时,都可以直接作为一个白色节点(新节点)挂接在整个系统中。但 upvalue 却是对已有的对象的间接引用,不是新数据。一旦 GC 在 mark 的过程中( gc 状态为 GCSpropagate ),则需增加屏障luaC_barrier。对于这个问题,会在以后详细展开。

lua 还有另一种数据类型创建时的挂接过程也被特殊处理。那就是 userdata 。见 lstring.c 的 95 行:

Udata *luaS_newudata (lua_State *L, size_t s, Table *e)
{
Udata *u;
if (s > MAX_SIZET - sizeof(Udata))
luaM_toobig(L);
u = cast(Udata *, luaM_malloc(L, s + sizeof(Udata)));
u->uv.marked = luaC_white(G(L));
u->uv.tt = LUA_TUSERDATA;
u->uv.len = s;
u->uv.metatable = NULL;
u->uv.env = e;
u->uv.next = G(L)->mainthread->next;
G(L)->mainthread->next = obj2gco(u);
return u;
}

这里并没有调用luaC_link来挂接新的 Udata 对象,而是直接使用的

u->uv.next = G(L)->mainthread->next; G(L)->mainthread->next = obj2gco(u);

  把 u 挂接在 mainthread 之后。从前面的 mainstate 创建过程可知。mainthread 一定是 GCObject 链表上的最后一个节点(除 Udata 外)。这是因为挂接过程都是向链表头添加的。

  这里,就可以把所有 userdata 全部挂接在其它类型之后。这么做的理由是,所有 userdata 都可能有 gc 方法(其它类型则没有)。需要统一去调用这些 gc 方面,则应该有一个途径来单独遍历所有的 userdata 。除此之外,userdata 和其它 GCObject 的处理方式则没有区别,顾依旧挂接在整个 GCObject 链表上而不需要单独再分出一个链表。

处理 userdata 的流程见 lgc.c 的 127 行

size_t luaC_separateudata (lua_State *L, int all) 

  这个函数会把所有带有 gc 方法的 userdata 挑出来,放到一个循环链表中。这个循环链表在global_State的 tmudata 域。需要调用 gc 方法的这些 userdata 在当个 gc 循环是不能被直接清除的。所以在 mark 环节的最后,会被重新 mark 为不可清除节点。见 lgc.c 的 545 行:

marktmu(g);

  这样,可以保证在调用 gc 方法环节,这些对象的内存都没有被释放。但因为这些对象被设置了 finalized 标记。(通过 markfinalized ),下一次 gc 过程不会进入 tmudata 链表,将会被正确清理。

具体 userdata 的清理流程,会在后面展开解释。

[转自]

Lua GC 的源码剖析 (1)

[更多参见]

Lua GC 的源码剖析 (2)

Lua GC 的源码剖析 (3)

Lua GC 的源码剖析 (4)

Lua GC 的源码剖析 (5)

Lua GC 的源码剖析 (6) 完结

lua_gc源码学习的更多相关文章

  1. Java集合专题总结(1):HashMap 和 HashTable 源码学习和面试总结

    2017年的秋招彻底结束了,感觉Java上面的最常见的集合相关的问题就是hash--系列和一些常用并发集合和队列,堆等结合算法一起考察,不完全统计,本人经历:先后百度.唯品会.58同城.新浪微博.趣分 ...

  2. jQuery源码学习感想

    还记得去年(2015)九月份的时候,作为一个大四的学生去参加美团霸面,结果被美团技术总监教育了一番,那次问了我很多jQuery源码的知识点,以前虽然喜欢研究框架,但水平还不足够来研究jQuery源码, ...

  3. MVC系列——MVC源码学习:打造自己的MVC框架(四:了解神奇的视图引擎)

    前言:通过之前的三篇介绍,我们基本上完成了从请求发出到路由匹配.再到控制器的激活,再到Action的执行这些个过程.今天还是趁热打铁,将我们的View也来完善下,也让整个系列相对完整,博主不希望烂尾. ...

  4. MVC系列——MVC源码学习:打造自己的MVC框架(三:自定义路由规则)

    前言:上篇介绍了下自己的MVC框架前两个版本,经过两天的整理,版本三基本已经完成,今天还是发出来供大家参考和学习.虽然微软的Routing功能已经非常强大,完全没有必要再“重复造轮子”了,但博主还是觉 ...

  5. MVC系列——MVC源码学习:打造自己的MVC框架(二:附源码)

    前言:上篇介绍了下 MVC5 的核心原理,整篇文章比较偏理论,所以相对比较枯燥.今天就来根据上篇的理论一步一步进行实践,通过自己写的一个简易MVC框架逐步理解,相信通过这一篇的实践,你会对MVC有一个 ...

  6. MVC系列——MVC源码学习:打造自己的MVC框架(一:核心原理)

    前言:最近一段时间在学习MVC源码,说实话,研读源码真是一个痛苦的过程,好多晦涩的语法搞得人晕晕乎乎.这两天算是理解了一小部分,这里先记录下来,也给需要的园友一个参考,奈何博主技术有限,如有理解不妥之 ...

  7. 我的angularjs源码学习之旅2——依赖注入

    依赖注入起源于实现控制反转的典型框架Spring框架,用来削减计算机程序的耦合问题.简单来说,在定义方法的时候,方法所依赖的对象就被隐性的注入到该方法中,在方法中可以直接使用,而不需要在执行该函数的时 ...

  8. ddms(基于 Express 的表单管理系统)源码学习

    ddms是基于express的一个表单管理系统,今天抽时间看了下它的代码,其实算不上源码学习,只是对它其中一些小的开发技巧做一些记录,希望以后在项目开发中能够实践下. 数据层封装 模块只对外暴露mod ...

  9. leveldb源码学习系列

    楼主从2014年7月份开始学习<>,由于书籍比较抽象,为了加深思考,同时开始了Google leveldb的源码学习,主要是想学习leveldb的设计思想和Google的C++编程规范.目 ...

随机推荐

  1. Oracle 数据泵使用详解

    数据泵使用EXPDP和IMPDP时应该注意的事项: EXP和IMP是客户端工具程序,它们既可以在客户端使用,也可以在服务端使用. EXPDP和IMPDP是服务端的工具程序,他们只能在ORACLE服务端 ...

  2. 【转】【Python】Python中的__init__.py与模块导入(from import 找不到模块的问题)

    python中的Module是比较重要的概念.常见的情况是,事先写好一个.py文 件,在另一个文件中需要import时,将事先写好的.py文件拷贝 到当前目录,或者是在sys.path中增加事先写好的 ...

  3. C++编程基础练习

    注:本文练习题均出自<Essential C++>第一章 练习1,1 从一个简单程序开始 #include<iostream> #include<string> u ...

  4. Linux系统源码安装软件过程中configure选项-prefix的作用

    在linux和unix环境中,源码安装是最常用的软件安装方式,一些软件出了提供源码外,也提供各种发行版的二进制安装包(如基于redhat包管理工具的rpm包),但强烈建议使用源码安装方式. 在linu ...

  5. Java与编码问题串讲之二–如何理解java采用Unicode编码

    Java开发者必须牢记:在Java中字符仅以一种形式存在,那就是Unicode(不选择任何特定的编码,直接使用他们在字符集中的编号,这是统一的唯一方法).由于java采用unicode编码,char  ...

  6. 升级win10,提示(RAM)内存不足2G的解决的方法,亲測可行

    前两天升级win10,检測我内存不足2G,可我的电脑是8G的内存如图 百度,google了非常多方法,有些是两根内存,去掉一个就好了,但是我的就一根8G的,其它就没什么好的方法了,改biosCPU选项 ...

  7. 记一次艰难的IBM X3850重装系统和系统备份经验

    [贴心话] 刚刚把一切都搞定了,回到电脑前立马就写下的这篇文章,写的很细节,大家就耐心看看,有些细节是网上没有的,共享一下,仅供参考,以减少大家装机时遇到的困难. [面临处境] 机器型号:IBM X3 ...

  8. js 正则去重

    split():字符串中的方法,把字符串转成数组. sort():数组中的排序方法,按照ACALL码进行排序. join():数组中的方法,把数组转换为字符串 var demo="ababb ...

  9. 第一篇 一步一步看透C++

        毕业快一年半了,这些时候,都是在底层方面做的一些工作,虽然内核的C也实现了C++中的一些抽象机制,面向对象,继承,多态,封装等等,但是,想着大学里面,电子类的学习,都是偏向底层的,有过C++的 ...

  10. GCT之数学公式(代数部分)

    一.代数部分: 1.复数 2.一元二次方程   3.数列 4.排列组合