Lua垃圾回收机制与优化实践

深入理解Lua垃圾回收器工作原理,掌握增量式GC、分代GC机制和内存优化最佳实践。

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 效率下降
  • 弱引用表中可能存入 falsenil 值,注意区分"键不存在"和"值为假"
  • collectgarbage("step") 的参数单位在 Lua 5.4 中变为字节,之前版本为 KB
  • 频繁调用 collectgarbage("collect") 会严重影响性能,仅用于调试
  • LuaJIT 的 GC 实现与标准 Lua 有所不同,JIT 下应关注其特定的 GC 行为

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页