最近发现在大数据量的 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. Windows IOT 开发入门(准备工作)

    终于抽出空来了,将最近研究的东西记录下来,物联网,万物皆可联网.然后可以做到智能家居,智能生活,智能城市....一大堆.吹牛的就不说了. 在实际应用中都是一个个小的传感器在收集数据,同时把数据直接或者 ...

  2. 关于Unity中FPS第一人称射击类游戏制作(专题十)

    当前Unity最新版本5.6.3f1,我使用的是5.5.1f1 场景搭建 1: 导入人物模型, 手持一把枪;2: 导入碎片模型;3: 创建一个平面;4: 创建一个障碍物;5: 导入人物模型;6: 配置 ...

  3. 你对linux了解多少,Linux 系统结构详解!

    最近一直有人在请教老K关于Linux系统相关问题,这里我就该问题做个详解,Linux系统一般有4个主要部分:内核.shell.文件系统和应用程序. 内核.shell和文件系统一起形成了基本的操作系统结 ...

  4. 【WPF】创建文本字符串的路径PathGeometry

    /// <summary> /// 创建文本路径 /// </summary> /// <param name="word">文本字符串< ...

  5. 第三百六十八节,Python分布式爬虫打造搜索引擎Scrapy精讲—elasticsearch(搜索引擎)用Django实现搜索的自动补全功能

    第三百六十八节,Python分布式爬虫打造搜索引擎Scrapy精讲—用Django实现搜索的自动补全功能 elasticsearch(搜索引擎)提供了自动补全接口 官方说明:https://www.e ...

  6. jpa无外键配置

    在用jpa这种orm框架时,有时我们实体对象存在关联关系,但实际的业务场景可能不需要用jpa来控制数据库创建数据表之间的关联约束,这时我们就需要消除掉数据库表与表之间的外键关联.但jpa在处理建立外键 ...

  7. 从SQL查询分析器中读取EXCEL中的内容

    很早以前就用sql查询分析器来操作过EXCEL文件了. 由于对于excel公式并不是很了解,所以很多时候处理excel中的内容,常常是用sql语句来处理的.[什么样的人有什么样的办法吧 :)] 今又要 ...

  8. Oracle EM错误,java.lang.Exception: Exception in sending Request :: null 分类: Oracle 2015-07-08 21:24 44人阅读 评论(0) 收藏

    操作系统:Win7 64bit Oracle: 10.2.0.1.0 很久没有使用EM了,打开一看,居然报错了,出现java.lang.Exception: Exception in sending ...

  9. 联想服务器X3650 M2 配置 RAID5 + 热备盘

    实验环境: 1.  服务器型号联想 System X3650 M2 2.  六块300G  SAS硬盘 实验目的: 配置RAID 5 ,搭建重要文件备份服务器. 标注:本教程六块硬盘,其中五块硬盘做R ...

  10. CentOS系统基础优化16条知识汇总

    1.不用root管理,以普通用户的名义通过sudo授权管理: 2.更改默认的远程连接服务端,禁止root用户远程连接,甚至要更改只监听内网ip: 3.定时自动更新服务器时间,使其和互联网时间同步: 4 ...