GC
基本流程
Garbage Collection
lua 是 oop 语言,拥有 GC 机制
实现方式是基于引用标记的(incremental mark-and-sweep collector.
)
默认情况下,所有会使用 lua 内存的 reference
,都可以借助 nil
来清除引用标记,
当某个 reference
不再被任何其他 reference
引用时,这个对象就会作为 dead objects
等待释放,
GC 将定期进行收集
GC 收集的周期由 两个参数决定,两个参数都是百分比,比如 100 就代表 1倍:
- garbage-collector pause:表示GC周期结束过后,等待内存充满到什么程度才再次收集;
- garbage-collector step multiplier:表示GC收集的速度,并且不能填太慢,否则GC周期永远结束不了;但调太高会影响性能
两个参数都需要通过 collectgarbage
方法去改(在 C语言里是 lua_gc),默认情况下不需要改
方法
lua 内存分配默认情况下是自动的,你需要释放内存就得把 reference(引用)
标记为 nil
,然后这个对象就会作为 dead objects
等待释放
相对的,全局变量因为 lua 不确定你什么时候用,所以默认情况下是一直不会释放的
如果不借助内存分析之类的工具,不容易不出效果,需要你自己分析结果
手动GC可以调用 collectgarbage()
,
其参数是一些状态string:
- “collect”:默认策略,进行完整 GC
- “stop”:停止自动的CG,直到调用”restart”
- “restart”:重启自动GC
- “count”:返回当前 Lua 占用的总内存(单位是KB,所以可能有小数,要乘以1024)
- “step”:模拟 GC 步骤,需要追加一个数值参数,表示当 Lua 已经有申请到指定数额(单位KB)内存后才进行GC,返回 true|false 表示GC的收集循环是否结束
- “setpause”:设置 pause 参数(参考前面介绍)
- “setstepmul”:设置 mul 参数(参考前面介绍)
- “isrunning”: 检查 GC 是否在运行中
利用 gc 检查内存
你可以借助 count 参数来粗略地检查内存
collectgarbage("stop") -- 先停止自动GC
local memorySize = collectgarbage("count") -- 记录先前状态
local tb = {}
local newMemorySize = collectgarbage("count") -- 记录新的状态
print((newMemorySize-memorySize) * 1024)
-> 56
比如这里 table 返回是 56 字节,但实际上的大小跟 lua 的版本有关,所以仅供参考
但学习阶段我们可以借助这个机制来进行内存原理的探索
引用类型
和值类型比较
引用类型类似于传统 OOP 语言中的概念,在 lua 里也称为 对象类型(object)
和传统OOP一样,
- 引用类型只是传递指针
- 引用类型退出语法域的时候不会立即释放,需要等待GC
可以用前面 GC 的方法测试
collectgarbage("stop")
local gc_count = function()
return collectgarbage("count")
end
local memory = gc_count()
local tb
for i=1,10 do
tb = {} -- 重复申请 10次 table 的内存,会发现最终结果是 10倍内存(例如单个 table 56B 就得到 560B)
-- tb = 1 -- 而如果全部都只是赋值一个数字,则不会产生 GC
end
print((gc_count() - memory) * 1024)
值得一提的是, string
虽然也可以被 GC 收集,但是比较特殊
table 等对象在创建时,都是明确被指定为 引用类型,并且告知调用者内存地址,
而 string
在一般情况下是不占 GC 的,只有使用了 ..
这种连接符才会触发 GC 收集,
因此一般都认为 string 是值类型 1
内存占用
table 和 function 的每次创建,都会申请内存,
当引用计数为0时,会被 GC 收集
具体数额如果依赖 collectgarbage()
去分析,是比较困难的
测试时可以借助比较夸张的数据来抵消掉细微的误差,
collectgarbage("stop")
local function gc_count()
return collectgarbage("count")
end
local function gc_print(num)
print(string.format("%s B", num*1024))
end
collectgarbage() -- 先收集一次保证没有其他条件干扰
-- 为了追求尽量精确,不能在执行过程中 print,因为 print 本身会产生GC
local memory = gc_count() -- 这个语句本身,或者接收值的时候,疑似会产生 48B 的GC
-- 局部变量在退出语法域以后,会丢失引用,视为 dead object
-- 但它并不像值类型一样立刻被释放
for i=1, 50 do
local tb = {}
end
-- 外部局部变量,因为在该 chunk 中仍然保持着引用,所以不会被 GC 掉
local outerTable = {}
for i=1, 100 do
outerTable[i] = {}
end
-- outerTable = nil --可以分两次执行,第二次解开这个注释
-- 尝试计算大致的总内存
local memory2 = gc_count()
gc_print(memory2 - memory)
-- 尝试在收集垃圾后计算总内存
collectgarbage()
local memory3 = gc_count()
gc_print(memory3 - memory)
从结论上来说:
- table 的创建语句
table={}
会分配新的内存(无论在什么地方、什么作用域) - 当退出局部作用域,作为局部变量的 table 引用丢失,会在稍后进行 GC 时作为收集目标
- 同理,当指向 table 的引用(outerTable)被标记为 nil 使得引用丢失,也会在稍后触发 GC
如果在使用完之后立刻将底层的的引用置为 nil 可以一定程度上减缓 GC 压力(幅度很小)
而 function 的创建也是类似的道理,将示例中的:
local tb = {}
替换为
local fun = function()
end
跑一下就能看出效果
weak table
为了支持用户自己管理 GC,lua 拥有 weak reference
机制,也就是弱引用,
弱引用和 strong reference 强引用
相对应,
在 强引用 的环境下执行这样的代码
local tb = {}
for i=1,10 do
tb[{}] = i
end
collectgarbage()
for k, v in pairs(tb) do
print(v)
end
-> 1,2 ... 10
当你创建若干个 table,他们每个都拥有新的地址,lua 默认的强引用机制会把这些地址视为具体的值,让 table 和这些地址保持引用关系
在例子中创建了许多局部变量的 table,理论上退出作用域以后这些table就“无主了”,
但实际上这些 “无主的” table 目前仍然被 tb 持有,只是我们写逻辑的人认为它丢失了引用,而lua不这么认为,
GC 也不认为这些东西该被释放,所以后续流程里,tb仍然能遍历到这些“入口”
如果用户创建了一套内存管理机制,持有所有 lua 虚拟机中的变量,那么这个全局的超级 table 就时刻保持着对于所有变量的引用,使得 GC 直接停摆,
为了解决这种问题, lua 允许你设置 weak table 属性
weak table 用 metatable 的元方法 __mode 触发,可以参考 原文档
它的值应该是(包含) “k” 或者 “v”,分别表示:
- “k”:将 table 的 key 设为 weak
- “v”:将 table 的 value 设为 weak
例如,我们将刚才例子中 table 的 key 设为 weak,表示这个 table 并不想和目标产生正常的 “强引用” 关系:
local tb = {}
setmetatable(tb, {__mode="k"}) -- 新增
for i=1,10 do
tb[{}] = i
end
collectgarbage()
for k, v in pairs(tb) do
print(v)
end
->
这时,那些本应该因为退出作用域而释放的 局部的table,无视了外层 tb 的引用关系,被 GC 视为可收集的目标触发了回收
同样的,你也可以设置 value,或者 key 和 value 一起
local tb = {}
setmetatable(tb, {__mode="v"})
-- 只有 value 是弱引用,而 key 保持强引用,使得 GC 不会自动释放
for i=1,10 do
tb[{}] = i
end
collectgarbage()
for k, v in pairs(tb) do
print(k, v)
end
如果同时指定 key 和 value 是弱引用,只需要让字符串包含 ‘k’ ‘v’ 两个字符
setmetatable(tb, {__mode="kv"})
内存的连续性
next
TODO
len
len 操作符表达为 #
,
例如 print(#"123")
就会返回3,表示取字符串的 byte 数量(如果1字节代表1字符,那么就等价于字符串的字符数量)
lua 在判定长度的时候,是用的类似于二分查找的算法,因此对于容器的内容有一定限制
- 操作时,返回的值实际上是一个
边界(border)
位置,它需要满足:(border == 0 or t[border] ~= nil) and
(t[border + 1] == nil or border == math.maxinteger)
- 也就是:
- 情况1:边界为0
- 情况2:边界为table中的某个位置,满足:
- 当前位置有值
- 下个位置是 nil
换句话说,这个边界值并不一定是 table 的结尾
如果你进行取长度的容器并不是 连续的容器 —— 序列(sequence)
,
即容器被一些 nil
值截断,产生了若干 子序列,
这些造成截断后果的 nil 值被称为 洞(hole)
例如:
{nil, 20, 30, nil, nil, 60, nil}
1
2 2
-> 6
如图,当二分让最后三个元素成为子序列,无法继续二分;
而最后一位是 nil,导致提前判定结束
从结果上看:# 处理时会忽略 table 末尾的 nil
官方对于 array size 的建议是,使用 getn 和 setn,但这两个函数已经移除
一般来讲如果出于准确性考虑,可以自己实现一套去维护
- https://www.lua.org/pil/17.html These are implementation details. Thus, from the programmer’s point of view, strings are values, not objects. Therefore, like a number or a boolean, a string is not removed from weak tables (unless its associated value is collected). ↩