虽然垃圾收集器的目标是回收对象,但是它也可以帮助程序员来释放外部资源。出于这种目的,几种编程语言提供了析构器。析构器是一个与对象关联的函数,当该对象即将被回收时该函数会被调用。
Lua
语言通过元方法 __gc
实现析构器,如下所示:
o = {x = "hi"} setmetatable(o, { __gc = function (o) print(o.x) end }) o = nil collectgarbage() --> hi
在上例中,我们首先创建一个带有 __gc
元方法的元表的表。然后,抹去与这个表的唯一联系(全局变量),在强制进行一次完整的垃圾回收。在垃圾回收期间, Lua
语言发现表已经不再是可访问的了,因此调用表的析构器,也就是元方法 __gc
。
Lua
语言中,析构器的一个微妙之处在于“将一个对象标记为需要析构”的概念。通过给对象设置一个具有非空 __gc
元方法的元表,就可以把一个对象标记为需要进行析构处理。如果不标记对象,那么对象就不会被析构。我们编写的大多数代码会正常运行,但会发生某些奇怪的行为,比如:
o = {x = "hi"} mt = {} setmetatable(o, mt) mt.__gc = function (o) print(o.x) end o = nil collectgarbage() --> (print nothing)
这里,我们确实给对象 o
设置了元表,但是这个元表没有 _gc
元方法,因此对象没有被标记为需要进行析构处理。即使我们后续给元表增加了元方法 __gc
, Lua
语言也发现不了这种赋值的特殊之处,因此不会把对象标记为需要进行析构处理。
正如我们所提到的,这很少会有问题。在设置元表后,很少会改变元方法。如果真的需要在后续设置元方法,那么可以给字段 __gc
先赋一个任意值作为占位符:
o = {x = "hi"} mt = {__gc = true} setmetatable(o, mt) mt.__gc = function (o) print(o.x) end o = nil collectgarbage() --> hi
现在,由于元表有了 __gc
字段,因此对象会被正确地标记为需要析构器处理。如果后续再设置元方法也不会有问题,只要元方法是一个正确的函数, Lua
语言就能够调用它。
当垃圾收集器在同一个周期中析构多个对象时,它会按照对象被标记为需要析构处理的顺序逆序调用这些对象的析构器。请考虑如下示例,该示例创建了一个由带有析构器的对象所组成的链表:
mt = {__gc = function (o) print(o[1]) end} list = nil for i = 1, 3 do list = setmetatable({i, link = list}, mt) end list = nil collectgarbage() --> 3 --> 2 --> 1
第一个被析构的对象是 3
,也就是最后一个被标记的对象。
一种常见的误解是认为正在被回收的对象之间的关联会影响对象析构的顺序。例如,有些人可能认为上例的对象 2
必须在对象 1
之前被析构,因为存在从 2
到 1
的关联。但是,关联会形成环。所以,关联并不会影响析构器执行的顺序。
有关析构器的另一个微妙之处是复苏( resurrection
)。当一个析构器被调用时,它的参数是正在被析构的对象。因此,这个对象会至少在析构器期间重新变成活跃的。在析构器执行期间,我们无法阻止析构器把该对象储存在全局变量中,使得该对象在析构器返回后仍然可以访问。
复苏必须是可传递的。考虑如下代码:
A = {x = "this is A"} B = {f = A} setmetatable(B, {__gc = function (o) print(o.f.x) end}) A, B = nil collectgarbage() --> this is A
B
的析构器访问了 A
,因此 A
在 B
析构前不能被回收, Lua
语言在运行析构器之前必须同时复苏 B
和 A
。
由于复苏的存在, Lua
语言会在两个阶段中回收具有析构器的对象。当垃圾收集器首次发现某个具有析构器的对象不可达时,垃圾收集器就把这个对象复苏并将其放入等待被析构的队列中。一旦析构器开始执行, Lua
语言就将该对象标记为已被析构。当下一次垃圾收集又发现这个对象不可达时,它就将这个对象删除。如果想保证我们程序中的所有垃圾都被真正释放了的话,那么必须调用 collectgarbage
两次,第二次调用才会删除第一次调用中被析构的对象。
由于 Lua
语言在被析构对象上设置的标记,每一个对象的析构器都会精确地运行一次。如果一个对象知道程序运行结束还没有被回收,那么 Lua
语言就会在整个 Lua
虚拟机关闭后调用它的析构器。这种特性在 Lua
语言中实现了某种形式的 atexit
函数,即在程序终结前立即运行的函数。我们所要做的就是创建一个带有析构器的表,然后把它锚定在某处,例如锚定到全局表中:
local t = {__gc = function () -- 'atexit' 的代码位于此 print("finishing lua program") end} setmetatable(t, t) _G["*AA*"] = t
另外一个有趣的技巧会允许程序在每次完成垃圾回收后调用指定的函数。由于析构器只运行一次,所以这种技巧是让每个析构器创建一个用来运行下一个析构器的新对象,如下所示:
do local mt = {__gc = function (o) -- 要做的工作 print("new cycle") -- 为下一次垃圾收集创建对象 setmetatable({}, getmetatable(o)) end} -- 创建第一个对象 setmetatable({}, mt) end collectgarbage() --> 一次垃圾收集 collectgarbage() --> 一次垃圾收集 collectgarbage() --> 一次垃圾收集
具有析构器的对象和弱引用表之间的交互也有些微妙。在每个垃圾收集周期内,垃圾收集器会在调用析构器前清理弱引用表中的值,在调用析构器之后在清理键。这种行为的原理在与我们经常使用带有弱引用键的表来保存对象的属性(详见:对象属性)。因此,析构器可能需要访问那些属性。不过,我们也会使用具有弱引用值的表来重用活跃的对象,在这种情况下,正在被析构的对象就不再有用了。