垃圾回收

Garbage

Lua 会进行自动的内存管理。程序可以创建出对象(表、闭包等),却没有删除对象的功能。Lua 会使用 垃圾回收,garbage collection,自动删除成为了垃圾的对象。这使我们摆脱了内存管理的大部分负担,更重要的是,使我们摆脱了与内存管理相关的大部分错误,如悬空指针及内存泄漏等,dangling pointers and memory leaks。

在一种理想世界中,程序员是看不到垃圾回收器的,the garbage collector,他就像个优秀的清洁工,不会干扰其他工作人员的工作。不过,有时即使是更聪明的垃圾回收器,也需要我们的帮助。我们可能需要在某些性能关键时刻,停止他工作,或者只允许他在某些特定时间工作。此外,垃圾回收器只能回收他能确定是垃圾的东西,而不能猜度我们认为什么是垃圾。没有垃圾回收器,我们就无法摆脱资源管理方面的所有烦恼,比如内存和外部资源的囤积等,hoarding memory and external resources。

弱表,weak tables、终结器,finalizers,和函数 collectgarbage 三者,是我们在 Lua 中,帮助垃圾回收器可以使用的主要机制。弱表实现对程序仍可访问 Lua 对象的回收;终结器实现对不受垃圾回收器直接控制外部对象的回收。函数 collectgarbage 则允许我们控制垃圾回收器的运行速度。在本章中,我们将讨论这些机制。

弱表

Weak Tables

正如我们曾说过的,垃圾回收器不能猜度我们认为的垃圾是什么。典型例子就是堆栈,他是通过一个数组,以及一个指向顶部的索引实现的。我们知道,数组的有效部分只会到顶部,但 Lua 却不知道。如果我们通过简单地递减顶层索引,来弹出一个元素,那么对 Lua 来说,数组中剩下的对象就不是垃圾。同样,存储在某个全局变量中的任何对象,对 Lua 来说都不是垃圾,即使咱们的程序永远不再用到他。在这两种情况下,我们(即我们的程序)都需要为这些位置赋值 nil,这样他们就不会锁定本可丢弃的对象。

然而,只是清理咱们的引用,并非总是足够了。有的结构就需要程序和回收器之间,额外的协作。典型例子便是,当我们想要保留程序中某种存活对象(如文件)列表时。此任务看似简单:我们只需将每个新对象,插入该列表即可。但是,一旦对象成为该列表的一部分,他就永远不会被回收!即使没有其他对象指向他,列表也会指向他。除非我们告诉 Lua 没有对象指向他这一事实,否则 Lua 就无法知道,列表指向他这一引用,不应阻止该对象的回收。

弱表正是我们用来告诉 Lua,某个引用不应阻止对象回收的机制。弱引用,weak reference 是种垃圾回收器不会考虑的对象引用。如果指向某个对象的全部引用都是弱引用,那么垃圾回收器就会回收该对象,并删除这些弱引用。Lua 通过 弱表,实现的弱引用:弱表是种其条目均为弱条目的表。这意味着,如果某个对象只存在于弱表中,那么 Lua 最终将回收该对象。

表有着键与值,两者均可包含任何类型对象。正常情况下,垃圾回收器不会回收作为键或值,出现在某个可访问表中的对象。也就是说,键和值都是强引用,因为他们可以防止回收他们所引用的对象。在弱表中,键和值都可以是弱引用。这就意味着有三种弱表:键为弱的表、值为弱的表以及键和值都为弱的表。不管是哪种表,当键或值被回收时,整个条目都会从表中消失。

??? from here until ???END lines may have been inserted/deleted 表的弱性,是由其元表中的 __mode 字段所给出的。如果该字段存在且值为 "k",则表中的键为弱键;如果该字段值为 "v",则表中的值为弱值;如果该字段值为 "kv",则键和值都为弱值。下面的示例虽然是杜撰的,却说明了弱表的基本行为:

a = {}
mt = {__mode = "k"}
setmetatable(a, mt)     -- 现在 'a' 就有了弱键
key = {}                -- 创建出首个键
a[key] = 1
key = {}                -- 创建出第二个键
a[key] = 2
collectgarbage()        -- 强制进行一次垃圾回收周期
for k, v in pairs(a) do print(v) end
    --> 2

在这个示例中,第二个赋值 key = {} 覆盖了第一个 key 的引用。对 collectgarbage 的调用,会强制垃圾回收器进行一次完整的回收。由于不再有其他的对第一个键引用,Lua 就会回收这个键并删除该表中的相应条目。然而,第二个键仍锚定在变量 key 中,因此 Lua 不会回收他。

请注意,只有对象才能从弱表中删除。数字和布尔值等值,就不能被回收。例如,如果我们往这个表 a (上一示例中),插入一个数字键,那么收集器就永远不会删除他。当然,如果某个数字键对应的值,在某个有着弱值的表中被回收了,那么整个条目就会从该表中被移除。

在这里,字符串有个微妙之处:虽然从实现角度看,字符串是可回收的,但他们却与其他可回收对象不同。其他对象,如表和闭包,都是显式创建的。例如,每当 Lua 计算表达式 {} 时,他都会创建一个新表。但是,Lua 在计算 "a".."b" 时,会不会创建出新的字符串呢?如果系统中已经有个字符串 "ab" 会怎么办呢?Lua 是否会创建一个新的字符串?编译器能否在运行程序之前,创建这个字符串呢?这并不重要:这些都是实现细节。从程序员角度来看,字符串是值,而非对象。因此,与数字或布尔值一样,字符串键不会从弱表中删除,除非其关联的值被回收。

记忆函数

Memorize Functions

以空间换取时间,是种常见编程技巧。通过 记住 某个函数的结果,在以后我们用相同参数调用该函数时,这个函数就可以重复使用该结果1,如此我们就可以加快某个函数的运行。

1 尽管 “memorize”(记忆)这一既定的英文单词,恰好描述了我们想要做的事情,但编程界还是创造了个新词 -- memoize(记忆)-- 来描述这一技术。我(作者)将坚持使用原词。

设想某个通用服务器正以 Lua 代码,接收着字符串形式的请求。其每次收到请求时,他都会在该字符串上运行 load 函数,然后调用生成的函数。然而,load 是个开销昂贵的函数,且服务器收到的某些命令可能相当频繁。服务器无需在每次收到像是 "closeconnection()" 这样的常见命令时,重复调用 load,而是可以使用一个辅助表,来 记住 load 的结果。在调用 load 前,服务器会在该表中检查给定字符串是否已有转译。若找不到匹配的结果,服务器就会调用 load,并将结果存储到该表中。我们可以将这种行为,打包到一个新函数中:

local results = {}
function mem_loadstring (s)
    local res = results[s]
    if res == nil then                  -- 结果不存在?
        res = assert(load(s))           -- 计算新的结果
        results[s] = res                -- 保存用于后面的重用
    end
    return res
end

这种方案下可以节省大量开销。不过,他也可能造成意想不到的浪费。虽然有些命令会反复出现,但许多其他命令却只会出现一次。逐渐地,表 results 会累积下服务器收到过的所有命令及其各自的代码;足够长的时间后,这种行为会耗尽服务器内存。

弱值表为这一问题提供了一种简单解决方案。如果 results 表有着弱值,那么每个垃圾回收周期都会删除当时未用到所有转译(这意味着几乎所有转移):

local results = {}
setmetatable(results, {__mode = "v"})   -- 使值成为弱值
function mem_loadstring (s)
    as before

实际上,由于该表的索引始终是字符串,因此如果我们愿意,可以让这个表完全弱化:

setmetatable(results, {__mode = "kv"})

最终(净)结果是一样的,the net result is the same。

这种记忆技巧,对于确保某种对象的唯一性也很有用。例如,假设某个系统以表的形式表示颜色,其中的 redgreenblue 字段在一定范围内。那么一个简单的颜色工厂,就会为每个新请求生成一种新颜色:

function createRGB (r, g, b)
    return {red = r, green = g, blue = b}
end

运用记忆术,我们可以对相同颜色重用同一个表。要为每种颜色创建一个唯一键,我们只需将颜色索引连接起来,其间用分隔符隔开即可:

local results = {}
setmetatable(results, {__mode = "v"})   -- 使值成为弱值
function createRGB (r, g, b)
    local key = string.format("%d-%d-%d", r, g, b)
    local color = results[key]
    if color == nil then
        color = {red = r, green = g, blue = b}
        results[key] = color
    end
    return color
end

这种实现方式的一个有趣结果是,用户可以使用原始相等运算符,the primitive equality operator,==,来比较颜色,这是因为两种共存的相等颜色,总是由同一个表来表示的。任何给定颜色,都可以在不同时间由不同表来表示,因为垃圾回收器会不时清除那个 results 表。不过,只要给定颜色仍在使用中,其就不会从 results 中删除。因此,只要某种颜色存活时间足够长,其就可以与某种新的颜色进行比较,其的表示也就存活了足够长的时间,可以被新颜色重用。

对象属性

Object Attributes

弱表的另一重要用途,是将属性与对象关联起来。很多情况下,我们都需要为对象附加一些属性:函数的名字、表格的默认值及数组的大小等等。

当对象是个表时,我们就可以适当的唯一键,在表中存储属性。(正如我们之前所看到的,创建唯一键的一种简单且不会出错的方法,便是创建一个新表并将其作为键)。但是,如果对象不是表,其就不能保留自己的属性。即使是表,有时我们可能也不想在原始对象中存储属性。例如,我们可能打算将属性保持私有,或者不想让属性干扰表的遍历等等。在所有这些情况下,我们就需要某种将属性映射到对象的替代方法。

当然,外部表提供了一种将属性映射到对象的理想方式。这就是我们在 “双重表示法” 小节中,所说的 双重表示法,dual representation。我们将对象用作键,将其属性作为值。由于 Lua 允许我们使用任何类型对象作为键,因此外部表就可以保存任何类型对象的属性。此外,外部表中保存的属性,还不会干扰其他对象,并且可以像表本身一样私有。

然而,这个看似完美的解决方案,却有个巨大的缺陷:一旦我们将某个对象用作某个表的键,我们就会锁定该对象的存在。Lua 无法回收被用作键的对象。例如,如果我们使用某个常规表,将函数映射到他们的名字,那么这些函数将永远不会被回收。正如咱们所料,我们可以通过使用弱表,来避免这一缺点。不过,这次我们需要弱键。弱键的使用,不会阻止任何键被回收,直到没有对该键的任何引用,the use of weak keys does not prevent any key from being collected, once there are no other references to it。另一方面,该表不能有弱值;否则,存活对象的属性会被回收掉。

重温带默认值的表

Revisiting Tables with Default Values

在名为 带默认值的表 小节,我们讨论过如何实现带非空,non-nil,缺省值的表。我们曾见识到一种特别技巧,并提到另外两种技巧需要用到弱表,因此我们推迟了对这两种技巧的讨论。现在是重新讨论这个问题的时候了。我们将看到,这两种缺省值技巧,实际上是我们刚讨论过的两种一般技巧:双重表示法和记忆术,的特定应用。

在首个方案中,我们使用一个弱表,来将每个表映射到其默认值:

local defaults = {}
setmetatable(defaults, {__mode = "k"})

local mt = {__index = function (t) return defaults[t] end}

function setDefault (t, d)
    defaults[t] = d
    setmetatable(t, mt)
end

这是双重表示法的典型用途,我们使用 defaults[t] 表示 t.default。如果 defaults 表没有弱键,他就会将所有带缺省值的表,锚定为永久存在。

在第二种解决方案中,我们为各异的默认值使用不同元表,不过每当我们重复某个默认值时,都会重用同一个元表。这就是记忆术的典型应用:

local metas = {}
setmetable(metas, {__mode = "v"})

function setDefaults (t, d)
    local mt = metas[d]
    if mt == nil then
        mt = {__index = function () return d end}
        metas[d] = mt       -- memoize
    end
    setmetatable(t, mt)
end

在此情形下,我们使用了弱值,来回收那些不再使用的元表。

有了这两种默认值实现方法,然而哪种最好呢?和往常一样,这取决于具体情况。两者的复杂度和性能都差不多。第一种实现方式需要为每个带有默认值的表(defaults 表中的条目)提供少许几个内存字,a few memory words。第二种实现方式则需要数十个内存字,来处理每个不同默认值(一个新表、一个新闭包,外加 metas 表中的一个条目)。因此,如果咱们的应用程序有数千个都有数个不同默认值的表,那么第二种实现方式显然更胜一筹。另一方面,如果只有少数几个共用了共同默认值的表,那么咱们应倾向于第一种实现方式。

星历表

Ephemeron Tables

在有着弱键的表中,当出现某个值指向他自己键时,就会出现棘手的情况。

这种情况比想象中更为常见。一个典型的例子,便是常量函数工厂,a constant-function factor。这种工厂会取个对象,并返回一个每次被调用时,都会返回该对象的函数:

function factory (o)
    return (function () return o end)
end

这种工厂非常适合记忆化,避免了在已有闭包情况下,创建出新的闭包。下图 23.1 “带有记忆功能的恒定函数工厂” 展示了这种改进。

图 23.1,带有记忆术的常量函数工厂,constant-function factory with memorization

do
    local mem = {}      -- 记忆表
    setmetatable(mem, {__mode "k"})
    
    function factory (o)
        local res = mem[o]
        if not res then
            res = (function () then o end)
            mem[o] = res
        end
        return res
    end
end

不过,这里有个问题。请注意,mem 中与对象相关联的值(那个常量函数),指向回了其自身的键(对象本身)。虽然表中的键是弱的,但值却不是。根据弱表的标准解释,那个记忆表中的任何东西都不会被删除。因为值不是弱值,所以每个函数都有个强引用,a string reference。每个函数都指向了其对应的对象,因此每个键都有个强引用。因此,尽管存在弱键,这些对象也不会被回收。

然而,这种严格解释并不十分有用。大多数人都认为,表中的值只能通过相应的键来访问。我们可以把上述情况,看作是一种循环,其中闭包指向的对象,又(通过记忆表)又指向回了闭包。

Lua 通过星历表的概念,解决了上述问题2。在 Lua 中,有着弱键与强值的表,就是 星历表,ephemeron table。在星历表中,键的可访问性,控制着其对应值的可访问性。更具体地说,请设想某星历表中的条目 (k,v)。只有当 k 存在别的外部引用时,对 v 的引用才是强引用。否则,垃圾回收器最终将回收 k 并从表中删除条目,即使 v 直接或间接地引用了 k

2 星历表是在 Lua 5.2 中引入的。Lua 5.1 仍然存在这里我们描述的问题。

终结器

Finalizers

虽然垃圾回收器的目标是回收 Lua 对象,但他也可以帮助程序释放外部资源。为此,一些编程语言提供了 终结器,finalizer。终结器是个与对象相关联的函数,在对象即将被回收时被调用。

Lua 通过元方法 __gc 实现了终结器,如下面的示例所示:

o = {x = "hi"}
setmetatable(o, {__gc = function (o) print(o.x) end})

o = nil
collectgarbage()    --> hi

在本例中,我们首先创建了个表,并赋予他一个有着 __gc 元方法的元表。然后,我们擦除了与该表的唯一链接(全局变量 o),并强制进行了一次完全的垃圾回收。在回收过程中,Lua 检测到该表已不再可访问,因此就会调用他的终结器 -- __gc 元方法。

Lua 中的终结器的一个微妙之处在于,标记终结对象的概念。通过给某个对象设置一个有着非空 __gc 元方法的元表,我们标记出要终结的对象。如果我们没有标记对象,那么他就不会被最终化。我们编写的大多数代码,都能自然地工作,但也会出现一些奇怪的情况,比如这里:

o = {x = "hi"}
mt = {}
setmetatable(o, mt)

mt.__gc = function (o) print(o.x) end

o = nil
collectgarbage()    --> (什么也不会打印)

在这里,我们给 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 字段,o 就会被正确地标记为终结。即使咱们以后未设置某个元方法,也不会有问题;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 有个链接。但是,链接会形成循环。因此,链接不会对终结器施加任何顺序,therefore, they do not impose any order to finalizers。

终结器的另一棘手问题是 复活,resurrection。当某个终结器被调用时,他会获取被终结对象作为参数。因此,至少在终结过程中,对象会再次复活。我(作者)把这称为 短暂复活,transient resurrection。而比如在终结器运行时,没有什么能阻止他将对象存储到全局变量中,如此在终结器返回后,对象就仍然可以被访问。我(作者)称之为 永久复活,permanent resurrection

复活必须是传递性的,must be transitive。请看下面这段代码:

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 必须在运行终结器之前,同时复活 BA

由于存在复活机制,Lua 会分两个阶段,回收带有终结器的对象。回收器在首次检测到某个带有终结器的对象不能触及时,就会复活该对象并将其排入要终结的队列。一旦该对象的终结器运行,Lua 就会将该对象标记为被终结。在垃圾回收器下一次检测到该对象不可及时,就会删除该对象。如果我们要确保程序中的所有垃圾都已被真正释放,就必须调用 collectgarbage 两次;第二次调用将删除第一次调用时被终结的对象。

由于 Lua 会在终结对象上打上标记,故每个对象的终结器都只会运行一次。如果某个对象直到程序结束时才被回收,那么当整个 Lua 状态关闭时,Lua 将调用其终结器。这一最后特性允许在 Lua 中使用一种 atexit 的函数,即在程序结束前,立即运行的函数。我们所要做的,就是创建一个带终结器的表,并将其锚定在某个地方,例如某个全局变量中:

atexit_demo.lua

local t = {__gc = function ()
    -- 咱们的 'atexit' 代码在这里
    print('结束 Lua 程序')
end}

setmetatable(t, t)
_G["*AA*"] = t

运行该程序如下:

$ lua atexit_demo.lua
结束 Lua 程序

另一种有趣技术,则允许程序在每次 Lua 完成一个垃圾回收周期时,调用某个给定函数。由于终结器仅运行一次,因此这里的技巧就是,让每个终结器都创建一个,用来运行下一个终结器的新对象,如下图 23.2 “在每个 GC 周期运行一个函数” 所示。

图 23.2,在每个 GC 周期运行一个函数

do
    local mt = {__gc = function (o)
        -- 咱们要完成的事情
        print("新的周期")
        -- 为下一周期创建新的对象
        setmetatable({}, getmetatable(o))
    end}
    -- 创建首个对象
    setmetatable({}, mt)
end

collectgarbage()    --> 新的周期
collectgarbage()    --> 新的周期
collectgarbage()    --> 新的周期

带终结器对象与弱表间之间的交互,也有微妙之处。在每个垃圾回收循环中,回收器会在调用终结器之前,清除弱表中的值,而在调用终结器之后清除键。这种行为的理由是,我们会经常使用带有弱键的表,来保存对象属性(正如我们在 “对象属性” 小节中所讨论的),因此终结器就可能需要访问这些属性。然而,我们会使用带弱值的表,来重用存活对象;在这种情况下,被终结的对象就不再有用了。

关于垃圾回收器

The Garbage Collector

在 5.0 版本之前,Lua 都使用的是一种简单的 标记-清扫式 垃圾回收器,a simple mark-and-sweep garbage collector(GC)。这种回收器有时被称为 “stop-the-world” 回收器。这意味着,Lua 会不时停止运行主程序,以执行一次完整垃圾回收周期。每个周期包括四个阶段:标记清理清扫 以及 完成mark, cleaning, sweep and finalization

回收器通过将其 根集,root set 标记为存活,开始标记阶段,根集是由 Lua 可以直接访问的对象组成。在 Lua 中,此集合就是个 C 注册表。(我们将在 “注册表” 小节中看到,主线程和全局环境,都是该注册表中的一些预定义条目)。

存储在存活对象中的任何对象,都是程序可以访问的,因此也会被标记为存活。(当然,弱表中的条目不会遵循这一规则。)当所有可达对象都被标记为存活时,标记阶段结束。

在开始清扫阶段前,Lua 会执行清理阶段,其间处理终结器与弱表。首先,他会遍历所有标记为终结的对象,寻找未标记的对象。这些对象会被标记为存活(复活),并被放入一个单独的清单中,以便在终结阶段使用。然后,Lua 会遍历他的那些弱表,删除所有其中键或值未被标记的条目。

清扫阶段会遍历所有 Lua 对象。(为实现这种遍历,Lua 将其创建的所有对象,都保存在一个链表中。)如果某个对象没有被标记为存活,Lua 就会回收他。否则,Lua 会清除其标记,为下一循环做准备。

最后,在完成阶段,Lua 会调用在清理阶段,分离出来的对象的终结器。

这种真正垃圾回收器的应用,意味着 Lua 不会出现对象引用循环问题。在用到循环数据结构,cyclic data structures,时,我们不需要采取任何特殊措施;他们会像其他数据一样被回收。

在 5.1 版中,Lua 获得了一种 增量回收器,an incremental collector。这种回收器会执行与旧回收器同样的步骤,但在其运行时无需停止整个世界(主程序)。相反,他会与解释器交错运行。每当解释器分配了一定量内存时,回收器就运行一小步。(这意味着,当回收器工作时,解释器可能会改变对象的可达性。为了确保回收器的正确性,解释器中的某些操作,就有了检测危险变化并纠正相关对象标记的屏障)。

Lua 5.2 引入了 紧急回收,emergency collection 特性。当某次内存分配失败时,Lua 会强制执行一个完整的回收循环,并再次尝试分配。这些紧急情况,可能发生在 Lua 分配内存的任何时候,包括 Lua 无法以一致状态运行代码的时刻;因此,这些回收是无法运行终结器的。

控制回收步调

Controlling the Pace of Collection

函数 collectgarbage 允许我们对垃圾回收器进行一些控制。他实际上是几个函数的合体:其可选的第一个参数是个指定了要做什么的字符串。其中一些选项的第二个参数,是个整数,我们称之为 data

collectgarbage 函数第一个参数的选项有:

  • stop:停止回收器,直到另一带有 restart 选项的 collectgarbage 调用;

  • restart:重启回收器;

  • collect:执行一次完整的垃圾回收周期,因此所有不可达对象都会被回收及终结。这是默认选项;

  • step:执行部分垃圾回收工作。第二个参数,即 data,指定回收工作数量,等同于回收器在 data 个数据字节后,要做些什么;

  • count:返回 Lua 当前使用的内存千字节数。该结果是个浮点数,乘以 1024 即可得出准确的字节总数。计数包括尚未被回收的死对象;

  • setpause:设置回收器的 pause 参数。其 data 参数以百分点形式给到新值:当 data 为 100 时,该参数就被设置为 1(100%);

  • setstepmul:设置回收器的步进倍数,the collector's step multiplier, stepmul,参数。新值由 data 给出,单位也是百分点。

pausestepmul 两个参数,控制着回收器的特性。任何垃圾回收器,都以内存换取 CPU 时间。在某种极端情况下,回收器可能根本不会运行。他耗费 CPU 时间为零,代价就是会消耗大量内存。另一个极端则是,垃圾回收器可能会在每次内存分配后,运行一个完整的周期。程序将使用所需的最小内存,但代价是消耗大量 CPU。pausestepmul 的默认值,试图在这两个极端之间找到平衡,并对大多数应用来说是足够好的。不过,在某些情况下,还是值得尝试对其进行优化。

pause 参数控制着回收器在完成一次回收,与开始一次新回收之间的等待时间。零的暂停,会让 Lua 在前一次回收结束后,立即启动新的回收。如果暂停为 200%,则会等待内存使用量翻倍后,再重新启动回收器。如果我们想用更多的 CPU 时间,换取更低的内存使用率,就可以设置更低的暂停时间。通常情况下,我们应将此值,保持在 0 到 200% 之间。

步进倍数参数 (stepmul) ,控制着回收器为分配到的每千字节内存做多少工作。该值越大,回收器的增量越小。100000000% 这样的大值,会使回收器像非增量回收器,a non-incremental collector,一样工作。默认值为 200%。低于 100% 的值会使回收器的运行速度非常慢,以至于永远无法完成回收。

collectgarbage 的其他选项,让我们可以控制回收器于何时运行。同样,默认的控制对于大多数程序来说,就已经足够好了,但某些特定应用程序,则可能需要手动控制。游戏就通常需要这类控制。例如,如果我们不想在某些时段进行任何垃圾回收工作,我们可以调用 collectgarbage(“stop”) 停止他,然后再调用 collectgarbage(“restart”) 重新启动他。在一些有着周期性空闲阶段,periodic idle phases,的系统中,我们可以保持回收器停止运行,并在空闲时间调用 collectgarbage(“step”, n)。要设定每个空闲期的工作数量,我们可以通过实验性地为 n 选择一个合适值,或者将 n 设为零(即最小步数)下循环调用 collectgarbage,直到空闲期结束。

练习

练习 23.1:请编写一个实验,来确定 Lua 是否真正实现了星历表。(如有可能,请同时在 Lua 5.1 和 Lua 5.2/5.3 中尝试咱们的代码,以了解二者的区别。

练习 23.2:请思考 “终结器” 小节 中的首个示例,该示例创建了一个带有终结器的表,该终结器只在被激活时打印一条信息。如果程序在没有垃圾回收循环的情况下结束,会发生什么情况呢?如果程序调用 os.exit 会怎样呢?如果程序以错误结束又会怎样呢?

练习 23.3:请设想一下,咱们必须为一个从字符串到字符串的函数,实现一个记忆表。由于弱表不会将字符串视为可回收对象,因此弱表就无法删除条目。在这种情况下,该如何实现记忆呢?

练习 23.4:请解释下图 23.3 “终结器和内存” 中的程序输出。

图 23.4,终结器与内存

local count = 0

local mt = {__gc = function () count = count - 1 end}
local a = {}

for i = 1, 10000 do
    count = count + 1
    a[i] = setmetatable({}, mt)
end

collectgarbage()
print(collectgarbage("count") * 1024, count)
a = nil
collectgarbage()
print(collectgarbage("count") * 1024, count)
collectgarbage()
print(collectgarbage("count") * 1024, count)

输出为


844277.0        10000
582141.0        0
22141.0 0

练习 23.5:这个本练习中,咱们至少需要一个用到大量内存的 Lua 脚本。如果没有,请编写一个。(可以是创建出表的简单循环。)

  • 使用不同的 pausestepmul 值运行咱们的脚本。他们对脚本性能与内存使用有怎样的影响?如果将 pause 设置为零会怎样?如果将 pause 值为 1000 会怎样?如果将步进倍数设置为零会怎样?如果将步进倍数为 1000000 会怎样?

  • 调整咱们的脚本,使其能够完全控制垃圾回收器。他应该保持垃圾回收器停止运行,并不时地调用他来完成一些工作。咱们能用这种方法,提高脚本的性能吗?

Last change: 2024-11-01, commit: d51c9bb