Lua 内存管理基础
Lua 采用自动垃圾回收机制管理内存,程序员无需手动分配和释放内存。所有 Lua 对象(表、字符串、函数、协程等)都由垃圾回收器跟踪。当对象不再被任何活跃引用指向时,它就会被标记为垃圾并在下一次 GC 周期中被回收。
-- 查看当前内存使用情况
print(collectgarbage("count")) -- 当前占用内存(KB)
-- 手动触发一次完整 GC
collectgarbage("collect")
-- 停止 GC
collectgarbage("stop")
-- 重启 GC
collectgarbage("restart")
增量式垃圾回收
Lua 5.1 引入的增量式 GC 将回收工作分散到多个小步骤中执行,避免长时间的 GC 停顿。增量式 GC 使用经典的三色标记算法:
| 颜色 | 含义 |
|---|---|
| 白色 | 未访问对象,GC 结束时回收 |
| 灰色 | 已发现但未完成扫描的对象 |
| 黑色 | 已完成扫描的对象,本轮不会被回收 |
GC 周期分为三个阶段:
-- 标记阶段:从根对象开始,递归标记所有可达对象
-- 清扫阶段:回收所有白色对象
-- 暂停阶段:等待新对象分配,直到触发下一轮 GC
-- 控制增量 GC 的步进
collectgarbage("setpause", 200) -- 暂停阈值(默认200%)
collectgarbage("setstepmul", 200) -- 步进倍率(默认200%)
- setpause:控制 GC 在两次周期之间的等待时间。值 200 表示当内存使用达到上次的 2 倍时开始新周期
- setstepmul:控制每步 GC 的工作量。值 200 表示 GC 步进速度是内存分配速度的 2 倍
分代式垃圾回收
Lua 5.4 引入了分代式 GC(generational GC),基于弱代假说:大多数对象朝生夕灭,少数对象会长期存活。
-- 切换到分代模式
collectgarbage("generational")
-- 切换回增量模式
collectgarbage("incremental")
-- 分代模式参数
collectgarbage("generational", 20, 200)
-- minor_gc: 小回收频率(默认 20)
-- major_gc: 大回收阈值(默认 200)
分代 GC 将对象分为两代:
- 新生代(new generation):新创建的对象,频繁进行小回收
- 老生代(old generation):存活超过一定周期的对象,较少进行大回收
-- 分代 GC 的优势场景:大量短生命周期对象
for i = 1, 1000000 do
local t = {x = i, y = i * 2} -- 每次循环创建,下次就被回收
end
-- 分代模式下这些对象在 minor GC 中被快速回收
弱引用表
弱引用表允许 GC 回收其中的对象,是实现缓存和资源池的关键工具:
-- 创建弱值表
local cache = {}
setmetatable(cache, {__mode = "v"})
-- 创建弱键表
local registry = {}
setmetatable(registry, {__mode = "k"})
-- 创建弱键弱值表
local weak = {}
setmetatable(weak, {__mode = "kv"})
__mode 字段的值含义:
| 值 | 说明 |
|---|---|
"k" | 键是弱引用 |
"v" | 值是弱引用 |
"kv" | 键和值都是弱引用 |
使用弱引用实现对象缓存
local function make_cache(load_func)
local cache = {}
setmetatable(cache, {__mode = "v"})
return function(key)
local value = cache[key]
if value == nil then
value = load_func(key)
cache[key] = value
end
return value
end
end
-- 缓存图片资源
local get_image = make_cache(function(path)
print("加载图片: " .. path)
return {path = path, data = "image_data"}
end)
local img1 = get_image("bg.png") -- 加载图片: bg.png
local img2 = get_image("bg.png") -- 命中缓存,不打印
print(img1 == img2) -- true
-- 当 img1 和 img2 都不再被引用时,缓存条目会被 GC 回收
使用弱键表实现对象属性存储
local properties = {}
setmetatable(properties, {__mode = "k"})
function set_property(obj, key, value)
if not properties[obj] then
properties[obj] = {}
end
properties[obj][key] = value
end
function get_property(obj, key)
local props = properties[obj]
if props then return props[key] end
return nil
end
-- 当 obj 被回收时,properties 中对应的条目也会被自动清理
__gc 终结器的正确用法
Lua 5.2+ 支持表的 __gc 元方法,用于资源清理:
local FileHandle = {}
FileHandle.__index = FileHandle
function FileHandle.new(path, mode)
local self = setmetatable({
path = path,
handle = io.open(path, mode or "r")
}, FileHandle)
return self
end
function FileHandle:close()
if self.handle then
self.handle:close()
self.handle = nil
end
end
function FileHandle:__gc()
self:close()
end
function FileHandle:read()
return self.handle:read("*a")
end
-- 安全使用
local f = FileHandle.new("/tmp/test.txt", "w")
f.handle:write("hello")
f:close() -- 显式关闭是最佳实践
-- 如果忘记关闭,GC 时也会自动调用 __gc
内存优化实践
避免不必要的表创建
-- 不好:每次调用都创建新表
function bad_approach(x, y)
return {x = x, y = y}
end
-- 更好:复用表
local _vec = {x = 0, y = 0}
function good_approach(x, y)
_vec.x = x
_vec.y = y
return _vec
end
-- 更好:使用多返回值
function best_approach(x, y)
return x, y
end
使用数组代替哈希表
-- 不好:使用字符串键
local config = {
width = 800,
height = 600,
fps = 60
}
-- 更好:使用数字索引(数组部分)
local config = {800, 600, 60}
-- 或者使用常量索引
local WIDTH, HEIGHT, FPS = 1, 2, 3
local config = {800, 600, 60}
print(config[WIDTH]) -- 800
字符串池
local string_pool = {}
setmetatable(string_pool, {__mode = "v"})
function intern(s)
local pooled = string_pool[s]
if pooled then return pooled end
string_pool[s] = s
return s
end
-- 大量重复字符串时节省内存
local names = {}
for i = 1, 100000 do
names[i] = intern("common_status") -- 所有条目共享同一个字符串
end
预分配表空间
-- Lua 5.4 支持 table.new(narray, nhash)
-- 需要 require "table.new"
-- 通用做法:提前初始化
local function create_grid(rows, cols)
local grid = {}
for r = 1, rows do
grid[r] = {}
for c = 1, cols do
grid[r][c] = 0
end
end
return grid
end
GC 调优配置
-- 游戏场景:频繁创建销毁对象
collectgarbage("generational")
collectgarbage("generational", 10, 100)
-- 服务器场景:长期运行,内存稳定
collectgarbage("incremental")
collectgarbage("setpause", 150) -- 内存增长50%就开始GC
collectgarbage("setstepmul", 300) -- GC速度是分配速度的3倍
-- 分帧手动步进 GC(游戏主循环中)
function game_loop()
-- 游戏逻辑
update()
render()
-- 每帧执行一小步 GC
collectgarbage("step", 10) -- 步进 10KB
end
监控内存使用
local function mem_report(label)
collectgarbage("collect")
local mem = collectgarbage("count")
print(string.format("[%s] 内存: %.2f KB", label, mem))
end
mem_report("启动")
-- 业务代码
local data = {}
for i = 1, 100000 do
data[i] = {id = i, name = "item_" .. i}
end
mem_report("加载数据后")
data = nil
mem_report("释放引用后(未GC)")
collectgarbage("collect")
mem_report("GC 后")
注意事项
- 分代 GC 在 Lua 5.4 中是稳定特性,推荐在新项目中使用
- 避免在 GC 终结器中创建新对象,会导致 GC 效率下降
- 弱引用表中可能存入
false和nil值,注意区分"键不存在"和"值为假" collectgarbage("step")的参数单位在 Lua 5.4 中变为字节,之前版本为 KB- 频繁调用
collectgarbage("collect")会严重影响性能,仅用于调试 - LuaJIT 的 GC 实现与标准 Lua 有所不同,JIT 下应关注其特定的 GC 行为
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。