最近发现在大数据量的 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. mac下配置android环境变量

    下面我将一下mac环境下的配置步骤: 1.在本地目录(home directory)中创建文件.bash_profile2.在文件中写入以下内容:export PATH=${PATH}:/Users/ ...

  2. Mac和Linux下pip更换源

    cd ~mkdir .pip vim .pip/pip.conf 在pip.conf中写入 [global]timeout = 6000index-url = https://pypi.tuna.ts ...

  3. Axiom3D:Ogre公告板集与合并批次

    在上文中,我们把Ogre里的网格分解成点线面后,我们要完成一个新的功能,在点上突出显示. 得到顶点位置后,这个功能也就是一个很简单的事,最开始是每个顶点添加一个子节点,节点上添加一个圆点. forea ...

  4. Axiom3D:Ogre动画基本流程与骨骼动画

    在Axiom中,Animation类用于管理动画,在此对象中主要管理着AnimationTrack对象,此对象用于管理动画的各种类型的每一桢.在Axiom中,动画类型主要有变形动画,姿态动画,骨骼动画 ...

  5. iOS项目的目录结构(Cocoa China)

    目录结构 AppDelegate Models Macro General Helpers Vendors Sections Resources   一个合理的目录结构首先应该是清晰的,让人一眼看上去 ...

  6. python写的读取json配置文件

    配置文件默认为conf.json 使用函数set完成追回配置项. 使用load或取配置项. 代码如下: #!/usr/bin/env python3 # -*- coding: utf-8 -*- ' ...

  7. Git -- 远程仓库简介

    到目前为止,我们已经掌握了如何在Git仓库里对一个文件进行时光穿梭,你再也不用担心文件备份或者丢失的问题了. 可是有用过集中式版本控制系统SVN的童鞋会站出来说,这些功能在SVN里早就有了,没看出Gi ...

  8. PLSQL Developer删除奇葩表出现异常ORA-00942: 表或试图不存在

    简单描述一下问题:发现数据库里有两个名称相同的表,不同的是PLSQL Developer里一个表名显示是大写,而另一个表名显示是小写 一般情况下,无论建表语句是大写,还是小写,因Oracle是区分大小 ...

  9. fence-agents kvm 实验

    1, Method of installing fence-agents on linux: $ git clone https://github.com/ClusterLabs/fence-agen ...

  10. linux中crontab命令

    一.crond简介 crond是linux下用来周期性的执行某种任务或等待处理某些事件的一个守护进程,与windows下的计划任务类似,当安装完成操作系统后,默认会安装此服务工具,并且会自动启动cro ...