掌叔
2010-01-18 20:41:48
Lua自动进行内存的管理。程序只能创建对象(表,函数等),而没有执行删除对象的函数。通过使用垃圾收集技术,Lua会自动删除那些失效的对象。这可以使你从内存管理的负担中解脱出来。更重要的,可以让你从那些由此引发的大部分BUG中解脱出来,比如指针挂起(dangling pointers)和内存溢出。
和其他的不同,Lua的垃圾收集器不存在循环的问题。在使用循环性的数据结构的时候,你无须加入特殊的操作;他们会像其他数据一样被收集。当然,有些时候即使更智能化的收集器也需要你的帮助。没有任何的垃圾收集器可以让你忽略掉内存管理的所有问题。
垃圾收集器只能在确认对象失效之后才会进行收集;它是不会知道你对垃圾的定义的。一个典型的例子就是堆栈:有一个数组和指向栈顶的索引构成。你知道这个数组中有效的只是在顶端的那一部分,但Lua不那么认为。如果你通过简单的出栈操作提取一个数组元素,那么数组对象的其他部分对Lua来说仍然是有效的。同样的,任何在全局变量中声明的对象,都不是Lua认为的垃圾,即使你的程序中根本没有用到他们。这两种情况下,你应当自己处理它(你的程序),为这种对象赋 nil值,防止他们锁住其他的空闲对象。
然而,简单的清理你的声明并不总是足够的。有些语句需要你和收集器进行额外的合作。一个典型的例子发生在当你想在你的程序中对活动的对象(比如文件)进行收集的时候。那看起来是个简单的任务:你需要做的是在收集器中插入每一个新的对象。然而,一旦对象被插入了收集器,它就不会再被收集!即使没有其他的指针指向它,收集器也不会做什么的。Lua会认为这个引用是为了阻止对象被回收的,除非你告诉Lua怎么做。
Weak 表是一种用来告诉Lua一个引用不应该防止对象被回收的机制。一个weak引用是指一个不被Lua认为是垃圾的对象的引用。如果一个对象所有的引用指向都是weak,对象将被收集,而那些weak引用将会被删除。Lua通过weak tables来实现weak引用:一个weak tables是指所有引用都是weak的table。这意味着,如果一个对象只存在于weak tables中,Lua将会最终将它收集。
表有 keys和values,而这两者都可能包含任何类型的对象。在一般情况下,垃圾收集器并不会收集作为keys和values属性的对象。也就是说,keys和values都属于强引用,他们可以防止他们指向的对象被回收。在一个weak tables中,keys和vaules也可能是weak的。那意味着这里存在三种类型的weak tables:weak keys组成的tables;weak values组成的tables;以及纯weak tables类型,他们的keys和values都是weak的。与table本身的类型无关,当一个keys或者vaule被收集时,整个的入口(entry)都将从这个table中消失。
表的weak性由他的metatable的__mode域来指定的。在这个域存在的时候,必须是个字符串:如果这个字符串包含小写字母‘k’,这个table中的keys就是weak的;如果这个字符串包含小写字母‘v’,这个table中的 vaules就是weak的。下面是一个例子,虽然是人造的,但是可以阐明weak tables的基本应用:
a = {}
b = {}
setmetatable(a, b)
b.__mode = "k" -- now 'a' has weak keys
key = {} -- creates first key
a[key] = 1
key = {} -- creates second key
a[key] = 2
collectgarbage() -- forces a garbage collection cycle
for k, v in pairs(a) do print(v) end
--> 2
在这个例子中,第二个赋值语句key={}覆盖了第一个key的值。当垃圾收集器工作时,在其他地方没有指向第一个key的引用,所以它被收集了,因此相对应的table中的入口也同时被移除了。可是,第二个key,仍然是占用活动的变量key,所以它不会被收集。
要注意,只有对象才可以从一个weak table中被收集。比如数字和布尔值类型的值,都是不会被收集的。例如,如果我们在table中插入了一个数值型的key(在前面那个例子中),它将永远不会被收集器从table中移除。当然,如果对应于这个数值型key的vaule被收集,那么它的整个入口将会从weak table中被移除。
关于字符串的一些细微差别:从上面的实现来看,尽管字符串是可以被收集的,他们仍然跟其他可收集对象有所区别。其他对象,比如tables和函数,他们都是显示的被创建。比如,不管什么时候当Lua遇到{}时,它建立了一个新的table。任何时候这个 function()。。。end建立了一个新的函数(实际上是一个闭包)。然而,Lua见到“a”..“b”的时候会创建一个新的字符串?如果系统中已经有一个字符串“ab”的话怎么办?Lua会重新建立一个新的?编译器可以在程序运行之前创建字符串么?这无关紧要:这些是实现的细节。因此,从程序员的角度来看,字符串是值而不是对象。所以,就像数值或布尔值,一个字符串不会从weak tables中被移除(除非它所关联的vaule被收集)。
17.1 记忆函数
一个相当普遍的编程技术是用空间来换取时间。你可以通过记忆函数结果来进行优化,当你用同样的参数再次调用函数时,它可以自动返回记忆的结果。
想像一下一个通用的服务器,接收包含Lua代码的字符串请求。每当它收到一个请求,它调用loadstring加载字符串,然后调用函数进行处理。然而,loadstring是一个“巨大”的函数,一些命令在服务器中会频繁地使用。不需要反复调用loadstring和后面接着的 closeconnection(),服务器可以通过使用一个辅助table来记忆loadstring的结果。在调用loadstring之前,服务器会在这个table中寻找这个字符串是否已经有了翻译好的结果。如果没有找到,那么(而且只是这个情况)服务器会调用loadstring并把这次的结果存入辅助table。我们可以将这个操作包装为一个函数:
local results = {}
function mem_loadstring (s)
if results[s] then -- result available?
return results[s] -- reuse it
else
local res = loadstring(s) -- compute new result
results[s] = res -- save for later reuse
return res
end
end
这个方案的存储消耗可能是巨大的。尽管如此,它仍然可能会导致意料之外的数据冗余。尽管一些命令一遍遍的重复执行,但有些命令可能只运行一次。渐渐地,这个 table积累了服务器所有命令被调用处理后的结果;早晚有一天,它会挤爆服务器的内存。一个weak table提供了对于这个问题的简单解决方案。如果这个结果表中有weak值,每次的垃圾收集循环都会移除当前时间内所有未被使用的结果(通常是差不多全部):
local results = {}
setmetatable(results, {__mode = "v"}) -- make values weak
function mem_loadstring (s)
... -- as before
事实上,因为table的索引下标经常是字符串式的,如果愿意,我们可以将table全部置weak:
setmetatable(results, {__mode = "kv"})
最终结果是完全一样的。
记忆技术在保持一些类型对象的唯一性上同样有用。例如,假如一个系统将通过tables表达颜色,通过有一定组合方式的红色,绿色,蓝色。一个自然颜色调色器通过每一次新的请求产生新的颜色:
function createRGB (r, g, b)
return {red = r, green = g, blue = b}
end
使用记忆技术,我们可以将同样的颜色结果存储在同一个table中。为了建立每一种颜色唯一的key,我们简单的使用一个分隔符连接颜色索引下标:
local results = {}
setmetatable(results, {__mode = "v"}) -- make values weak
function createRGB (r, g, b)
local key = r .. "-" .. g .. "-" .. b
if results[key] then return results[key]
else
local newcolor = {red = r, green = g, blue = b}
results[key] = newcolor
return newcolor
end
end
一个有趣的后果就是,用户可以使用这个原始的等号运算符比对操作来辨别颜色,因为两个同时存在的颜色通过同一个的table来表达。要注意,同样的颜色可能在不同的时间通过不同的tales来表达,因为垃圾收集器一次次的在清理结果table。然而,只要给定的颜色正在被使用,它就不会从结果中被移除。所以,任何时候一个颜色在同其他颜色进行比较的时候存活的够久,它的结果镜像也同样存活。
17.2 关联对象属性
weak tables的另一个重要的应用就是和对象的属性关联。在一个对象上加入更多的属性是无时无刻都会发生的:函数名称,tables的缺省值,数组的大小,等等。
当对象是表的时候,我们可以使用一个合适的唯一key来将属性保存在表中。就像我们在前面说的那样,一个很简单并且可以防止错误的方法是建立一个新的对象(典型的比如table)然后把它当成key使用。然而,如果对象不是table,它就不能自己保存自身的属性。即使是tables,有些时候我们可能也不想把属性保存在原来的对象中去。例如,我们可能希望将属性作为私有的,或者我们不想在访问table中元素的时候受到这个额外的属性的干扰。在上述这些情况下,我们需要一个替代的方法来将属性和对象联系起来。当然,一个外部的table提供了一种理想化的方式来联系属性和对象(tables有时被称作联合数组并不偶然)。我们把这个对象当作key来使用,他们的属性作为vaule。一个外部的table可以保存任何类型对象的属性(就像Lua允许我们将任何对象看作key)。此外,保存在一个外部table的属性不会妨碍到其他的对象,并且可以像这个table本身一样私有化。
然而,这个看起来完美的解决方案有一个巨大的缺点:一旦我们在一个table中将一个对象使用为key,我们就将这个对象锁定为永久存在。Lua不能收集一个正在被当作 key使用的对象。如果我们使用一个普通的table来关联函数和名字,那么所有的这些函数将永远不会被收集。正如你所想的那样,我们可以通过使用 weak table来解决这个问题。这一次,我们需要weak keys。一旦没有其他地方的引用,weak keys并不会阻止任何的key被收集。从另一方面说,这个table不会存在weak vaules;否则,活动对象的属性就可能被收集了。
Lua 本身使用这种技术来保存数组的大小。像我们下面即将看到的那样,table库提供了一个函数来设定数组的大小,另一个函数来读取数组的大小。当你设定了一个数组的大小,Lua 将这个尺寸保存在一个私有的weak table,索引就是数组本身,而value就是它的尺寸。
17.3 重述带有默认值的表
在章节13.4.3,我们讨论了怎样使用非nil的默认值来实现表。我们提到一种特殊的技术并注释说另外两种技术需要使用weak tables,所以我们推迟在这里介绍他们。现在,介绍她们的时候了。就像我们说的那样,这两种默认值的技术实际上来源于我们前面提到的两种通用的技术的特殊应用:对象属性和记忆。
在第一种解决方案中,我们使用weak table来将默认vaules和每一个table相联系:
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
如果默认值没有weak的keys,它就会将所有的带有默认值的tables设定为永久存在。在第二种方法中,我们使用不同的metatables来保存不同的默认值,但当我们重复使用一个默认值的时候,重用同一个相同的metatable。这是一个典型的记忆技术的应用:
local metas = {}
setmetatable(metas, {__mode = "v"})
function setDefault (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
这种情况下,我们使用weak vaules,允许将不会被使用的metatables可以被回收。
把这两种方法放在一起,哪个更好?通常,取决于具体情况。它们都有相似的复杂性和相似的性能。第一种方法需要在每个默认值的tables中添加一些文字(一个默认的入口)。第二种方法需要在每个不同的默认值加入一些文字(一个新的表,一个新的闭包,metas中新增入口)。所以,如果你的程序有数千个 tables,而这些表只有很少数带有不同默认值的,第二种方法显然更优秀。另一方面,如果只有很少的tabels可以共享相同的默认vaules,那么你还是用第一种方法吧。