Lua 语言使用自动内存管理, 无需像 C、C++ 语言进行手动内存管理。
但是和 java、kotlin 一样,有时也需要外部进行辅助回收,区分哪些资源是可以进行回收的。
在 Lua 种主要用来辅助垃圾回收的主要机制有:
在 java、kotlin 中有 “强软弱虚” 四种引用类型,Lua 则有 “强弱” 引用类型。
Lua 中正常设置的值,即有变量直接引用的值,则为强引用,垃圾回收机制不会对其进行回收。
Lua 的另一种引用类型则是弱引用,垃圾回收机制只要运行就会对弱引用进行回收,而 Lua 的弱引用以弱引用表的形式存在。
Lua 的这两种引用类型和 java、kotlin 中对应的 “强”、“弱” 引用的生命周期是一致的
表 table 有键值对,只要键值对中有一个为弱引用,则该表就为弱引用表,所以简单来说弱引用表有三种存在形式:弱引用键的表、弱引用值的表、弱引用键弱引用值的表。
弱引用表的声明是通过元表,元表中设置 “__mode” 字段,对应关系如下表
弱引用表类型 | __mode 取值 |
---|---|
弱引用键的表 | “k” |
弱引用值的表 | “v” |
弱引用键弱引用值的表 | “kv” |
值得说明的是,弱引用表只要键值对中有一个值没有被外部其他的非弱引用进行引用,则该键值对就被垃圾回收机制回收掉。 即只要存在强引用的话,该被引用的对象就还是不会被回收,如果该对象只是被多个弱引用进行引用的话,则会被回收。
举个例子:
如果不使用弱引用表,即使运行 collectgargabe
函数进行垃圾回收,也不会释放
local a = {}
local k1 = {}
a[k1] = 10
local k2 = {}
a[k2] = 20
print("移除引用前:")
for i, v in pairs(a) do
print(i, v)
end
--> 移除引用前:
--> table: 0x60000200c9c0 10
--> table: 0x60000200c980 20
print("移除引用后:")
k1 = nil
k2 = nil
-- 垃圾回收
collectgarbage()
for i, v in pairs(a) do
print(i, v)
end
--> 移除引用后:
--> table: 0x60000200c9c0 10
--> table: 0x60000200c980 20
使用了弱引用表:
这里会对 k1 赋值为 nil
,即中断了 k1 对 table 的强引用,只剩下弱引用对该 table 的引用,所以在使用 collectgarbage
进行回收,弱引用表也会中断了该 table 的引用,使其被回收。
local a = {}
-- 创建一个 key 为弱引用的表
local mt = { __mode = "k" }
setmetatable(a, mt)
local k1 = {}
a[k1] = 10
local k2 = {}
a[k2] = 20
print("移除引用前:")
for i, v in pairs(a) do
print(i, v)
end
--> 移除引用前:
--> table: 0x60000200ca80 10
--> table: 0x60000200cac0 20
print("移除引用后:")
-- 删除值
k1 = nil
-- 垃圾回收
collectgarbage()
for i, v in pairs(a) do
print(i, v)
end
--> 移除引用后:
--> table: 0x60000200cac0 20
值得注意的是,Lua 的弱引用表只对对象有效果,对于字符串、数值、布尔值都是无效的, 通过下面代码感受下:
local a = {}
-- 创建一个 key 为弱引用的表
local mt = { __mode = "k" }
setmetatable(a, mt)
a["name"] = "jiang"
a[11] = "11"
a[true] = "true"
a[{}] = "table"
print("触发垃圾回收前:")
for i, v in pairs(a) do
print(i, v)
end
--> 触发垃圾回收前:
--> name jiang
--> true true
--> 11 11
--> table: 0x60000200c980 table
-- 即使是弱引用表数值、布尔值、字符串并不会被回收
collectgarbage()
print("触发垃圾回收后:")
for i, v in pairs(a) do
print(i, v)
end
--> 触发垃圾回收后:
--> name jiang
--> true true
--> 11 11
下图中 table 为弱引用键的表。当存入一对键值对,键(key)是一个对象,而值(value)是一个函数或一个表或是就是键本身。如果为函数时,函数内部强引用了键对象;如果为表时,表的键值对又引用了该键对象。
基于我们之前的理论,这个弱引用表就形同虚设了,因为键一直被自身的值强引用着。可以借助下图理解这种环的问题:
由于这种问题的存在,Lua 认定这种表为一种瞬表,这种瞬表的键控制着对应值的可访问性,即会忽视值 value 中对键的引用,只有其他地方对键的强引用才会被真正强引用,不被回收。
举两个例子:
方法中引用了弱引用的值
local mem = {}
setmetatable(mem, { __mode = "k" })
function factory(o)
local res = mem[o]
if not res then
res = (function()
return o
end)
mem[o] = res
end
return res
end
print("触发垃圾回收前:")
info = {}
f = factory(info)
for i, v in pairs(mem) do
print(i, v)
end
--> 触发垃圾回收前:
--> table: 0x600001ad0b40 function: 0x6000001d0d50
info = nil
f = nil
-- 垃圾回收
collectgarbage()
print("触发垃圾回收后:")
for i, v in pairs(mem) do
print(i, v)
end
--> 触发垃圾回收后:
--> // 循环没有任何输出,因为被清空了,环已经被打破
表中引用了弱引用的值
local mem = {}
setmetatable(mem, { __mode = "k" })
function factory(o)
local res = mem[o]
if not res then
res = {
name = "jiang",
obj = o,
}
mem[o] = res
end
return res
end
print("触发垃圾回收前:")
info = {}
f = factory(info)
for i, v in pairs(mem) do
print(i, v)
end
--> 触发垃圾回收前:
--> table: 0x600001ad0b40 table: 0x600001ad09c0
info = nil
f = nil
-- 垃圾回收
collectgarbage()
print("触发垃圾回收后:")
for i, v in pairs(mem) do
print(i, v)
end
--> 触发垃圾回收后:
--> // 循环没有任何输出,因为被清空了,环已经被打破
这种做法只有在 Lua 5.2 之后才有效
析构器是一个与被回收对象关联的函数,当对象被回收时,则调用该函数。
析构函数的设置通过设置一个带有 __gc
元方法的元表。
o = { name = "江澎涌" }
setmetatable(o, { __gc = function(o)
print("o gc", o.name)
end })
o = nil
collectgarbage() --> o gc 江澎涌
值得注意的是,__gc 元方法必须在给对象设置元表时就存在,否则即使后面再增加该元方法也是没有作用的。 可以先用一个布尔值或是数值进行占坑,然后后面再设置的话则是可以的。
info = { name = "江澎涌" }
mt = {
-- 一定要先占坑,否则后续加的 __gc 会无效
__gc = true
}
setmetatable(info, mt)
mt. __gc = function(o)
print("info gc", o.name)
end
info = nil
collectgarbage() --> info gc 江澎涌
析构时会按照需要被析构的顺序 “逆序” 调用这些对象的析构器。
local 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
析构函数中,函数的第一个参数可以获取到被析构的对象。在这期间被析构的对象又会被变得 “活跃” ,既短暂复苏,如果我们在这期间将这个被析构的对象保存到全局变量中,则将其 “复活” 了。
local user = { name = "小朋友" }
local mt = {
__gc = function(o)
print("user gc", o.name)
user1 = o
end
}
setmetatable(user, mt)
print(user) --> table: 0x60000304ca00
user = nil
collectgarbage() --> user gc 小朋友
print(user1.name) --> 小朋友
print(user1) --> table: 0x60000304ca00
setmetatable(user1, mt)
collectgarbage() --> user gc 小朋友
可以利用析构器的特性,程序在运行结束时,会对程序中所有还未调用析构函数的对象进行调用析构函数,我们可以利用这特性进行监听程序退出。
而如果想要最后调用该对象的析构函数,我们可以在程序的最开始处调用即可。
只需要绑定在一个对象上即可
local mt = {
__gc = function()
print("finishing Lua Program.")
end
}
setmetatable(mt, mt)
_G[{}] = mt --> finishing Lua Program.
可以利用复苏的机制,在每次监听到 gc 时,就重新添加一个可回收对象,在下次 gc 时就又会监听到了。
do
local mt = {
__gc = function(o)
print("new cycle", o)
setmetatable({}, getmetatable(o))
end
}
setmetatable({}, mt)
end
print("---- 1 -----")
collectgarbage()
print("---- 2 -----")
collectgarbage()
print("---- 3 -----")
collectgarbage()
print("---- 4 -----")
collectgarbage()
print("---- 5 -----")
collectgarbage()
print("---- 结束 -----")
--> ---- 1 -----
--> new cycle table: 0x600001a988c0
--> ---- 2 -----
--> new cycle table: 0x600001a989c0
--> ---- 3 -----
--> new cycle table: 0x600001a988c0
--> ---- 4 -----
--> new cycle table: 0x600001a989c0
--> ---- 5 -----
--> new cycle table: 0x600001a988c0
--> ---- 结束 -----
--> new cycle table: 0x600001a989c0
Lua 5.0 之前(包括 Lua 5.0),Lua 都是使用标记——清除(mark-and-sweep)式垃圾收集器,他会时不时的停止主程序的运行执行一次完整的垃圾收集周期,每个周期由四个阶段组成:标记、清理、清除、析构
标记阶段:会把根结点集合标记为活跃,根结点集合由 Lua 语言可以直接访问的对象组成。
清理阶段:Lua 语言遍历所有被标记为需要进行析构(就是有设置析构函数的对象)、但又没有被标记为活跃状态的对象。把这些对象标记为活跃(即我们前面提及的复苏),并被保存在一个独立的列表中,析构阶段会用到这个列表。然后,Lua 语言遍历弱引用表并从中移除键或值未被标记的元素。
清除阶段:遍历所有对象(Lua 把所有创建的对象放在一个链表中方便遍历),把没有被标记为活跃的对象进行回收,否则清理活跃标记,让下次清理周期可以进行。
析构阶段:Lua 调用清理阶段被分离出的对象的析构器。
值得注意的是,即使是成环也可以正常被清理。
Lua 5.1 引入了增量式垃圾收集器(incremental collector) ,和标记——清除(mark-and-sweep)式垃圾收集器一样执行相同的步骤,但是不需要在垃圾收集期间停止主程序的运行。他与解释器一起交替运行。
Lua 5.2 引入了紧急垃圾收集(emergency collector) 。当内存分配失败时,Lua 会强制进行一次完整的垃圾收集,然后再次尝试分配。这些紧急情况发生在 Lua 进行内存分配的任意时刻,但这些收集动作不能运行析构器。
这个函数是垃圾收集器的通用接口。它根据其第一个参数 opt
执行不同的功能 , opt
可以选择以下参数:
opt | 作用 |
---|---|
“stop” | 停止垃圾收集器,直到使用选项 “restart” 再次调用 collectgarbage |
“restart” | 重启垃圾收集器 |
“collect” | 执行一次完整的垃圾收集,回收和析构所有不可达的对象。默认选项 |
“step” | 执行垃圾收集工作,第二个参数 arg 指明字节数,即分配了 arg 个字节后垃圾收集器就执行垃圾收集工作。如果为 arg = 0 ,则垃圾收集器执行一次完整步骤。如果该步骤完成了一个收集周期,则返回 true 。 |
“count” | 以 KB 为单位返回当前已用内存数,该结果为一个浮点数,乘以 1024 得到的就是精确的字节数。该值包括了尚未被回收的死对象(可能会溢出) |
“setpause” | 设置收集器的 pause 参数(间歇率)。参数 arg 以百分比为单位给出要设定的新值:当 arg 为 100 时,参数被设为 1(即 100%),会返回之前设置的 pause 值。 |
“setstepmul” | 设置收集器的 stepmul 参数(步进倍率,step multiplier)。参数 arg 给出新值,也是百分比为单位 |
“isrunning” | 返回一个布尔值,指示收集器是否正在运行(即未停止) |
“incremental” | 将收集器模式更改为增量。 arg 可以设置三个数值:垃圾收集器暂停、步长乘数和步长。 |
“generational” | 将收集器模式更改为 generational。 arg 可以设置两个数值:垃圾收集器次要乘数和主要乘数。 |
使用 setpause 的 pause 参数,用于控制垃圾收集器在一次收集完成后等待多久再开始新的一次收集。如果想要消耗更多的 CPU 时间换取更低的内存消耗,可以把值设置的低一些,通常这个在 0-200% 间。
使用 setstepmul 的 stepmul 参数,用于控制对分配 1 KB 内存,垃圾收集器应该进行多少工作。值越高,垃圾收集器使用的增量越小。如果设置一个像 10000000% 一样很大的值,会让收集器表现得想一个非增量的垃圾收集器。默认值为 200% ,低于 100% 的值会让垃圾收集器运行的很慢,以至于可能一次收集也完不成。
Lua 项目地址:Github传送门 (如果对你有所帮助或喜欢的话,赏个star吧,码字不易,请多多支持)
如果觉得本篇博文对你有所启发或是解决了困惑,点个赞或关注我呀。
公众号搜索 “江澎涌”,更多优质文章会第一时间分享与你。