Lua中的垃圾回收机制:弱引用表,析构器和函数collectgarbage

垃圾收集:

Lua语言使用自动内存管理,程序可以创建对象(表,闭包等),但却没有函数来删除对象。Lua语言通过垃圾收集自动的删除成为垃圾的对象。

弱引用表,析构器和函数collectgarbage是在Lua语言中用来辅助垃圾收集器的主要机制。弱引用表允许收集Lua语言中还可以被程序访问的对象;析构器允许收集不在垃圾收集器直接控制下的外部对象;函数collectgarbage则允许我们控制垃圾收集器的步长。

弱引用表:

垃圾收集器不能猜测我们认为哪些是垃圾,程序不会再用到的、存储在全局变量中的对象,对于Lua语言来说不是垃圾,需要我们的代码将这些对象所在的位置赋为nil,以便这些位置不会锁定可释放的对象。

简单的清除引用并不够,有些情况下,还需要程序和垃圾收集器之间的协作——当我们要保存某种类型的活跃对象的列表时,只需要将新对象插入数组即可;但是,一旦一个对象成为了数组的一部分,即使没有其他任何地方在引用它,但数组依然在引用它,无法被回收。

弱引用表就是这样一种用来告知Lua语言一个引用不应阻止对一个对象回收的的机制。

所谓弱引用是一种不在垃圾收集器考虑范围内的对象引用。如果对一个对象的所有引用都是弱引用,那么垃圾收集器将会回收这个对象并删除这些弱引用。Lua通过弱引用表来实现弱引用,弱引用表就是元素均为弱引用的表,这意味着如果一个对象只被一个弱引用表持有,那么Lua语言最终会回收这个对象。

表由键值对组成,其两者都可以容纳任意类型的对象。在正常情况下,垃圾收集器不会回收一个在可访问表中作为键和值的对象。也就是说,键和值都是引用,他们会阻止对其所指向对象的回收。在一个弱引用表中,键和值都可以是弱引用的。这就意味着有三种类型的弱引用表:具有弱引用键的表具有弱引用值的表同时具有弱引用键和值的表。不论是哪种类型的弱引用表,只要有一个键或值被回收了,那么对应的整个键值对都会被从表中删除。

一个表是否为弱引用表是由其元表中的__mode字段所决定的。当这个字段存在时,其值应为一个字符串:如果这个字符串是”k”,则说明这个表的键(key)是弱引用的,如果这个字符串是”v”,那么这个表的值是弱引用的;如果这个字符串是“kv”,那么这个表的键和值都是弱引用的。

只有对象可以从弱引用表中被移除,而像数字和布尔这样的“值”是不可回收的。

析构器:

析构器是一个与对象关联的函数,当该对象即将被回收时该函数会被调用。

Lua语言通过元方法__gc实现析构器:

1
2
3
4
o = {x = "hi"}
setmetatable(o,{__gc = function(o) print(o.x) end})
o = nil
collectgarbage() --> hi

通过给对象设置一个具有非空__gc元方法的元表,就可以将一个对象标记为需要进行析构处理,如果不标记对象,那么对象就不会被析构。

当垃圾收集器在同一个周期中析构多个对象时,它会按照对象被标记为需要析构处理的顺序逆序调用这些对象的析构器。

析构器的另一个特点是复苏:当一个析构器被调用时,它的参数是正在被析构的对象。因此,这个对象会至少在析构期间重新变成活跃的,这叫做“临时复苏”;在析构器执行期间,我们无法阻止析构器把该对象存储在全局变量中,使得该对象在析构器返回后仍然可访问,这称为“永久复苏”。

由于复苏的存在,Lua语言会在两个阶段中回收具有析构器的对象。当垃圾收集器首次发现某个具有析构器的对象不可达的时候,垃圾收集器就把这个对象复苏,并将其放入等待被析构的队列中。一旦析构器开始执行,Lua语言就将该对象标记为已被析构,当下一次垃圾收集器又发现这个对象不可达时,它就将这个对象删除。

如果想保证我们程序中的所有垃圾都被真正的释放了的话,那么必须调用collectgarbage两次,第二次调用才会删除第一次调用中被析构的对象。

由于Lua语言在被析构对象上设置的标记,每一个对象的析构器都会精确的运行一次,如果一个对象直到程序运行结束还没有被回收,那么Lua语言就会在整个Lua虚拟机关闭后调用它的析构器。

另一个有趣的技巧会允许程序在每次完成垃圾回收后调用指定的函数。由于析构器只运行一次,所以这种技巧是让每个析构器创建一个用来运行下一个析构器的新对象。

具有析构器的对象和弱引用表之间的交互也有些微妙:在每个垃圾收集周期内,垃圾收集器会在调用析构器前清理弱引用表的值,在调用析构器之后再清理键。这种行为的原理在于我们经常使用带有弱引用键来保存对象的属性,因此,析构器可能需要访问那些属性。不过,我们也会使用具有弱引用值的表来重用活跃的对象,在这种情况下,正在被析构的对象就不再有用了。

垃圾收集器:

一直到Lua5.0,Lua语言使用的都是一个简单的标记-清除式垃圾收集器,这种收集器又被称为全局暂停式的收集器,意味着Lua语言会时不时地停止主程序的运行来执行一次完整的垃圾收集周期。每一个垃圾收集周期由四个阶段组成:标记、清理、清除和析构。

  • 标记阶段:把根结点集合(由Lua语言可以直接访问的对象组成)标记为活跃。在Lua语言中,这个集合只包括C注册表。保存在一个活跃对象中的对象是程序可达的,因此也会被标记为活跃(弱引用表中的内容除外),当所有可达对象都被标记为活跃后,标记阶段完成。
  • 清理阶段:首先,Lua语言遍历所有被标记为需要进行析构、但又没有被标记为活跃状态的对象。这些没有被标记为活跃状态的对象会被标记为活跃(复苏),并被放在一个单独的列表中,这个列表会在析构阶段用到。然后,Lua语言遍历弱引用表并从中移除键或值未被标记的元素。
  • 清除阶段:遍历所有对象(Lua 语言把所有创建的对象放在一个链表中),如果一个对象没有被标记为活跃,Lua语言就将其回收。否则,Lua语言清理标记,然后准备进行下一个清理周期。
  • 析构阶段:Lua语言调用清理阶段被分离出的对象的析构器。

Lua5.1使用了增量式垃圾收集器。这种垃圾收集器像老版的垃圾收集器一样执行相同的步骤,但是不需要在垃圾收集期间停止主程序的运行:它与解释器一起交替运行,每当解释器分配了一定数量的内存时,垃圾收集器也执行一小步。

Lua5.2引入了紧急垃圾收集,当内存分配失败时,Lua语言会强制进行一次完整的垃圾收集,然后再次尝试分配。这些紧急情况可以发生在Lua语言进行内存分配的任意时刻,包括Lua语言处于不一致的代码执行状态时,因此,这些收集动作不能运行析构器。