Lua GC 机制
垃圾回收
采用了自动内存管理。 这意味着你不用操心新创建的对象需要的内存如何分配出来, 也不用考虑在对象不再被使用后怎样释放它们所占用的内存。
Lua 运行了一个垃圾收集器来收集所有死对象 (即在 Lua 中不可能再访问到的对象)来完成自动内存管理的工作。 Lua 中所有用到的内存,如:字符串、表、用户数据、函数、线程、 内部结构等,都服从自动管理。
垃圾回收器函数
1 |
|
gc 算法分析
Lua gc 采用的是 标记 - 清除 算法,即一次 gc 分两步:
- 在标记阶段,从根集对象开始标记,把整个对象层级都标记完,这些被标记到的对象称为可达到的对象。
- 在清除阶段,遍历上面提到的对象链表,如果对象被标记过则擦除标记并跳过,如果对象没有被标记说明他不可达到,那就把它释放掉。
lua5.1 开始采用了一种三色回收的算法
- 白色:表示对象未被标记,在 GC 周期开始之前对象为白色,如果到清除阶段还是白色,那就说明该对象不可到达,要被清除掉。
- 灰色:表示对象本身已标记,但它引用的对象未被标记,比如一个表已被标记,它的键值对象还未被标记。这表示对象标记的一个中间状态。
- 黑色:表示对象和它引用的对象都已被标记,在清除阶段黑色对象为可到达对象。
对象链表
global_State
有好多和 GC 相关的字段,其中有一些重要链表。所有 GC 对象总是在下面的链表中:
allgc
:所有未被标记为终结的对象finobj
:所有标记为终结的对象tobefnz
:所有准备终结的对象(准备调用__gc
的对象)fixedgc
:不会被回收的对象
正常情况下,新建对象都会进入 allgc
链表。
如果对象设置一个元表,且元表有 __gc
元方法,那么它会从 allgc
取出,加入到 finobj
去,并设置对象的 FINALIZEDBIT
标记为。
在标记阶段,finobj
中的白对象会移到 tobefnz
链表去,然后标记这些对象,这样当前周期不会释放这些对象;清除完之后会进入 GCScallfin
阶段,在这里调用 tobefnz
对象的 gc 方法,同时把对象移回 allgc
链表;如果 gc 中使对象重新变成可到达,则对象复活过来;否则下个周期这个对象就会被正常清除。
fixedgc
是那些不会被回收的对象,在新建完对象后,必须马上调用 luaC_fic
把对象从 allgc
移到 fixedgc
去。GC 的过程不会对 fixedgc
进行清除。
luajit 使用状态机来执行 gc 算法,共有 6 中状态:
GCSpause
: gc 开始阶段,初始化一些属性,将一些跟节点(主线程对象,主线程环境对象,全局对象等)push 到灰色链表中GCSpropagate
: 分步进行扫描,每次从灰色链表 pop 一个对象,遍历该对象的子对象,例如如果该对象为 table,并且 value 没有设置为 week,则会遍历 table 所有 table 可达的 value,如果 value 为 gc 对象且为白色,则会被 push 到灰色链表中,这一步将一直持续到灰色链表为空的时候GCSatomic
: 原子操作,因为GCSpropagate
是分步的,所以分步过程中可能会有新的对象创建,这时候将再进行一次补充遍历,这遍历是不能被打断的,但因为绝大部分工作被GCSpropagate
做了,所以过程会很快。新创建的没有被引用的userdata
,如果该userdata
自定义了 gc 元方法,则会加入到全局的userdata
链表中,该链表会在最后一步GCSfinalize
处理GCSsweepstring
: 遍历全局字符串 hash 表,每次遍历一个 hash 节点,如果 hash 冲突严重,会在这里影响 gc。如果字符串为白色并且没有被设置为固定不释放,则进行释放GCSsweep
: 遍历所有全局 gc 对象,每次遍历 40 个,如果 gc 对象为白色,将被释放GCSfinalize
: 遍历GCSatomic
生成的userdata
链表,如果该userdata
还存在 gc 元方法,调用该元方法,每次处理一个
什么时候会导致 gc
global_state
有两个字段是关于 Lua 的内存统计的:
1 |
|
在 lua_newstate
函数里,这两个字段初始为:
1 |
|
在内存分配函数 luaM_realloc_
里,会根据分配或释放内存调整 GCdebt
的值:
1 |
|
判断是否触发 GC 的宏是:
1 |
|
g->GCdebt
如果大于 0,就执行一次luaC_step
。
回收控制
luaC_step
执行一步 GC:
1 |
|
GC 的过程
Lua 会自动管理内存,它通过垃圾收集器回收掉不再使用的对象,所谓不再使用,可以简单理解为没有其他对象引用的对象。
Lua 使用简单的标记和清除的垃圾回收容器,每次完整的 GC 分为 4 个阶段:
- 标记(mark):Lua 将根集对象标记为活的,根集对象实际上就是 C 注册表,像主线程和全局环境都是注册表的预定义项。从根集对象一直往下标记,任何存在于活对象中的对象,只要程序可达(弱表除外),也会被标记为活的。
- 清理(cleaning):这一阶段,Lua 遍历那些未标记的对象,如果它们有终结函数,将它们移出到另一个列表中,后面第 4 个阶段会用到。接着遍历 Lua 弱表,把其中 key 或 value 未被标记的对象从弱表移除。
- 清除(sweep):Lua 遍历所有对象,如表未被标记则回收掉,如果有标记则清除标记。
- 终结(finalization):在清理阶段将有终结函数,且未被标记的对象都移到一个独立的列表中,这阶段就是遍历这个列表,并调用它们的终结函数。而这些未活的对象,实际上应该会在下一个 GC 周期才被回收掉。
在 Lua5.1 以上的版本,GC 被优化成增量式的,它和解释器交错运行,即每一个 GC 只执行一部分,这样就有效避免 GC 的时候解释器停顿太久。
弱表
下面的例子能说明弱表的行为:
1 |
|
终结函数
就是元表有指定 __gc
元方法,如:
1 |
|
如果一个对象(有终结函数)在程序结束的时候还未被回收,Lua 会在结束的时候调用该对象的终结函数,可以利用这个特性来监控程序结束。