弱表
Lua 弱表(weak table)深度解析:弱引用语义、GC 协同机制、内存治理与高级应用案例
弱表
本文档对标 MIT 6.035 编译器、CMU 15-410 操作系统、Stanford CS140 内存管理课程教学水准,面向 0 基础自学者与企业级 Lua 工程师,系统讲解 Lua 弱表(weak table)的语义、GC 协同机制、内存治理模式与生产级应用。
1. 学习目标
学习本章后,读者应能在 Bloom 认知层级框架下达成下列目标。
1.1 知识层(Remembering)
- 列举 Lua 弱表的三种模式:弱键 (
__mode = "k")、弱值 (__mode = "v")、弱键值 (__mode = "kv")。 - 复述弱引用(weak reference)与强引用(strong reference)的区别。
- 描述 Lua 增量标记-清除 GC 与代际 GC 对弱表的不同处理时机。
1.2 理解层(Understanding)
- 解释弱表条目被回收的具体时机(GC 周期中的”清除”阶段)。
- 阐释弱表与字符串驻留(string interning)的相互作用。
- 描述 ephemeron table 的语义及其对”重链”(resurrection)的影响。
1.3 应用层(Applying)
- 编写基于弱表的对象缓存(memoization cache)。
- 使用弱表实现事件订阅/退订机制,避免内存泄漏。
- 应用弱表管理临时资源、对话窗口、玩家会话。
1.4 分析层(Analyzing)
- 分析弱表与闭包 upvalue、 userdata 关联的 GC 行为。
- 区分弱表失效与显式置 nil 的回收时机差异。
- 分析 Redis/Nginx/Game Lua 中弱表的实际性能开销。
1.5 评价层(Evaluating)
- 评判弱表 vs LRU 缓存的取舍。
- 评估 Lua 5.4 代际 GC 对弱表语义的优化与回归。
- 评判弱表设计在嵌入式环境下的合理性。
1.6 创造层(Creating)
- 设计基于弱表的对象池框架。
- 构建自动化的弱表泄漏检测工具。
- 设计跨版本兼容的弱表抽象层。
2. 历史动机与演化
2.1 内存管理范式的演化
内存管理历经手动管理(C/C++ malloc/free)、引用计数(Python PyObject)、跟踪式 GC(Java/Lua)三个主要阶段。弱引用(weak reference)的概念最早由 Java 1.1(1997)的 WeakReference 类普及,用于解决”对象图中的缓存引用阻碍 GC”问题。
Lua 在 5.0 版本(2003)正式引入弱表,灵感来自 Java 弱引用与 Scheme 垃圾收集器。Lua 之前的版本中,所有表条目都持有强引用,导致缓存模式难以实现而不引起内存膨胀。
2.2 Lua 在游戏/嵌入式/脚本领域的地位
Lua 在以下场景大量使用弱表:
- 游戏脚本:魔兽世界用弱表存储玩家引用,玩家下线后自动回收;Roblox 用弱表管理实体(entity)缓存。
- 嵌入式:路由器固件用弱表缓存会话,避免内存膨胀导致 OOM。
- Redis 脚本:脚本内不能直接使用弱表(脚本生命周期短),但 Redis 内部使用类似机制管理客户端。
- Nginx/OpenResty:worker 进程内使用弱表管理请求上下文,请求结束后自动清理。
2.3 演化时间线
| 版本 | 年份 | 弱表相关变化 |
|---|---|---|
| Lua 5.0 | 2003 | 引入弱表,__mode 元方法,仅支持 "k" 与 "v" |
| Lua 5.1 | 2006 | 修复字符串键不被弱化的语义(strings are always strong) |
| Lua 5.2 | 2011 | 引入 ephemeron table 概念,处理 A->B 且 B 是 A 键的情况 |
| Lua 5.3 | 2015 | 弱表与 64 位整数协同,性能优化 |
| Lua 5.4 | 2020 | 代际 GC,弱表回收时机更精确 |
| Lua 5.5 | 2025 | 持续优化弱表扫描开销 |
| Luau | 2021 | 弱表语义与 Lua 5.1 一致,但 GC 优化 |
3. 形式化定义
3.1 弱引用的形式化
设 为对象集合, 为引用集合。强引用 表示”持有”,弱引用 表示”观察但持有权移交 GC”。
可达性(reachability)定义:
其中 为强引用集。弱引用不参与可达性计算。
3.2 弱表的代数模型
弱表 可形式化为四元组:
其中 为键值集合,。
回收规则:
3.3 ephemeron table 语义
Lua 5.2+ 引入 ephemeron table 语义:当键为弱引用时,值的可达性传递性条件化于键的可达性。
形式化规则:在 GC 周期中,若弱键 不可达,则条目 被移除,即使 仍通过其他强引用可达。这避免了”重链”问题。
3.4 GC 与弱表交互的形式化
Lua GC 周期可抽象为五阶段:
- mark:从根集出发,标记所有强可达对象。
- atomic:原子阶段,处理弱表与 resurrected 对象。
- sweep:清除未标记对象。
- finalize:调用
__gc元方法。 - weak-clear:清理弱表中失效条目。
3.5 弱表与可达性的不动点
弱表清理需迭代收敛:
当 时收敛,此时执行最终清除。
4. 理论推导与证明
4.1 弱表回收时机定理
定理 1(弱表条目回收时机):Lua 5.x 保证弱表条目的回收发生在 GC 周期的 atomic 阶段,且在 __gc 元方法调用之前。
证明(基于 GC 实现):
Lua GC 在 atomic 阶段:
- 标记所有弱表。
- 对每个弱表,遍历条目,对键/值(视
__mode而定)未标记者记录待清除。 - 调用待 finalize 对象的
__gc。 - 清除弱表中记录的条目。
故弱表清理早于 __gc 调用,保证 finalize 阶段看到的弱表状态已无悬挂引用。
证毕。
4.2 字符串键不被弱化定理
定理 2(字符串键的强引用语义):Lua 弱表中的字符串键不会被弱化为弱引用,即使 __mode = "k"。
证明:
Lua 字符串实现采用驻留(interning)机制,所有字符串字面量共享唯一实例。若弱表允许字符串键被弱化,则字符串字面量可能被回收,导致下次访问时重新分配,违反 interning 不变式。
Lua 源码 lstring.c 中 luaS_new 检查字符串表,若存在则返回现有实例。弱表清理时跳过字符串键:
/* lgc.c */
static int isobjtraceable (const TValue *o) {
switch (ttype(o)) {
case LUA_TTABLE: case LUA_TUSERDATA:
case LUA_TFUNCTION: case LUA_TTHREAD:
return 1;
case LUA_TSTRING: /* strings are not weak */
return 0;
default:
return 0;
}
}
证毕。
4.3 ephemeron table 不动点定理
定理 3(ephemeron 不动点存在性):在 ephemeron table 语义下,弱键条目的清理会达到不动点,即存在 使得 。
证明:
设 为第 轮清理后的可达集。由于:
- (可达集单调缩小)。
- 有限(对象总数有限)。
由单调有界定理,存在 使 ,即不动点。
Lua 实现采用迭代算法,最多 轮收敛,其中 为弱表条目数。
证毕。
4.4 弱表与闭包 upvalue 的交互
定理 4(弱表对闭包的语义):若弱表值为函数 ,且 捕获了 upvalue ,则当 通过弱表回收时, 不会立即被回收,需视 是否还被其他闭包引用。
证明:
-- lua: 弱表与闭包 upvalue
local t = setmetatable({}, {__mode = "v"})
local function make()
local x = {}
return function() return x end
end
t[1] = make()
-- 此时 t[1] 持有闭包,闭包持有 x
collectgarbage()
print(t[1]) -- function: 0x...
local f = t[1]
t[1] = nil -- 显式清除
collectgarbage()
-- 闭包仍未被回收,因 f 持有强引用
print(f) -- function: 0x...
证明:弱表回收的只是弱表与对象的弱引用,对象的实际回收需视所有引用消失。
4.5 弱表遍历的稳定性
定理 5(弱表遍历不可靠性):在遍历弱表期间,任何 next 或 pairs 调用可能遇到已失效但未清除的条目,也可能因 GC 触发而中途变更。
证明:
弱表条目清除发生在 GC atomic 阶段,但遍历发生在用户代码执行期间,可能触发增量 GC,导致遍历过程中条目被清除。
-- lua: 弱表遍历的不稳定
local t = setmetatable({}, {__mode = "v"})
local holders = {}
for i = 1, 1000 do
holders[i] = {n = i}
t[i] = holders[i]
end
-- 部分释放
for i = 1, 500 do holders[i] = nil end
collectgarbage("collect")
-- 遍历,可能遇到 nil
for k, v in pairs(t) do
print(k, v) -- 部分 v 可能是 nil
end
5. 代码示例
5.1 弱表基础
-- lua: 弱表基础
local weak_values = setmetatable({}, {__mode = "v"})
local weak_keys = setmetatable({}, {__mode = "k"})
local weak_both = setmetatable({}, {__mode = "kv"})
-- 弱值表示例
local obj = {name = "Alice"}
weak_values[1] = obj
print(weak_values[1]) -- table: 0x...
obj = nil -- 移除强引用
collectgarbage("collect")
print(weak_values[1]) -- nil(已回收)
5.2 弱键表示例
-- lua: 弱键表
local weak_keys = setmetatable({}, {__mode = "k"})
local key1 = {id = 1}
local key2 = {id = 2}
weak_keys[key1] = "value1"
weak_keys[key2] = "value2"
print(weak_keys[key1], weak_keys[key2]) -- value1 value2
key1 = nil
collectgarbage("collect")
-- key1 已回收,对应条目也被清除
local count = 0
for k, v in pairs(weak_keys) do count = count + 1 end
print(count) -- 1(只剩 key2)
5.3 对象缓存
-- lua: 弱表实现对象缓存
local function make_cache()
local cache = setmetatable({}, {__mode = "v"})
return {
get = function(key)
local v = cache[key]
if v then
v.last_access = os.time()
end
return v
end,
set = function(key, value)
cache[key] = value
end,
size = function()
local n = 0
for _ in pairs(cache) do n = n + 1 end
return n
end,
}
end
local cache = make_cache()
-- 模拟对象创建
local function load_user(id)
return {id = id, name = "user_" .. id, last_access = os.time()}
end
local u1 = load_user(1)
cache.set(1, u1)
print(cache.size()) -- 1
u1 = nil
collectgarbage("collect")
print(cache.size()) -- 0(u1 已回收)
5.4 事件订阅系统
-- lua: 弱表实现事件订阅
local function make_event_emitter()
local listeners = setmetatable({}, {__mode = "k"}) -- 弱键
return {
on = function(self, listener)
listeners[listener] = true
return function()
listeners[listener] = nil
end
end,
emit = function(self, ...)
for listener, _ in pairs(listeners) do
listener(...)
end
end,
count = function()
local n = 0
for _ in pairs(listeners) do n = n + 1 end
return n
end,
}
end
local emitter = make_event_emitter()
local handler1 = function(msg) print("handler1:", msg) end
local handler2 = function(msg) print("handler2:", msg) end
emitter:on(handler1)
emitter:on(handler2)
print(emitter:count()) -- 2
emitter:emit("hello") -- 两个都触发
handler1 = nil
collectgarbage("collect")
print(emitter:count()) -- 1(handler1 已回收)
emitter:emit("world") -- 只有 handler2 触发
5.5 字符串键不被弱化验证
-- lua: 字符串键不被弱化
local weak_keys = setmetatable({}, {__mode = "k"})
local key_str = "my_string_key"
weak_keys[key_str] = "value"
key_str = nil
collectgarbage("collect")
print(weak_keys["my_string_key"]) -- value(未被回收)
-- 字符串字面量在常量表中,永远不被弱化
5.6 ephemeron table 语义
-- lua: ephemeron table 语义
local weak_k = setmetatable({}, {__mode = "k"})
local key = {}
local value = {data = "important"}
weak_k[key] = value
-- 此时 key 通过 weak_k 弱可达,value 通过 weak_k 强可达
-- 但若 key 被回收,value 也会被回收
key = nil
collectgarbage("collect")
-- 检查 value 是否还存在
local found = false
for k, v in pairs(weak_k) do
found = true
end
print(found) -- false(key 与 value 都被回收)
5.7 弱表与元表
-- lua: 弱表作为元表
local prototype = {x = 0, y = 0}
local instances = setmetatable({}, {__mode = "k"}) -- 弱键,追踪所有实例
local function create_instance(x, y)
local obj = setmetatable({}, {__index = prototype})
obj.x = x
obj.y = y
instances[obj] = true -- 弱引用
return obj
end
local inst1 = create_instance(1, 2)
local inst2 = create_instance(3, 4)
local count = 0
for _ in pairs(instances) do count = count + 1 end
print(count) -- 2
inst1 = nil
collectgarbage("collect")
count = 0
for _ in pairs(instances) do count = count + 1 end
print(count) -- 1(inst1 已回收)
5.8 临时表池
-- lua: 弱表实现表池
local function make_pool()
local pool = setmetatable({}, {__mode = "v"}) -- 弱值
local created = 0
return {
acquire = function()
for t, _ in pairs(pool) do
pool[t] = nil
return t
end
created = created + 1
return {}
end,
release = function(t)
-- 清空表内容
for k in pairs(t) do t[k] = nil end
pool[t] = true -- 放回池中,但弱引用
end,
stats = function()
local pooled = 0
for _ in pairs(pool) do pooled = pooled + 1 end
return {created = created, pooled = pooled}
end,
}
end
local pool = make_pool()
local t1 = pool.acquire()
t1[1] = "data"
pool.release(t1)
print(pool.stats()) -- {created=1, pooled=1}
t1 = nil
collectgarbage("collect")
print(pool.stats()) -- {created=1, pooled=0}(池中表被回收)
5.9 玩家会话管理
-- lua: 弱表管理玩家会话(游戏场景)
local function make_session_manager()
local sessions = setmetatable({}, {__mode = "k"}) -- 弱键
local metadata = {} -- 强引用元数据(不与 session 同生命周期)
return {
create = function(player_id, conn)
local session = {
player_id = player_id,
conn = conn,
created_at = os.time(),
}
sessions[session] = true
metadata[session] = {login_time = os.time()}
return session
end,
active_count = function()
local n = 0
for _ in pairs(sessions) do n = n + 1 end
return n
end,
cleanup_metadata = function()
for session, _ in pairs(metadata) do
if not sessions[session] then
metadata[session] = nil
end
end
end,
}
end
local mgr = make_session_manager()
local s1 = mgr.create("player_1", {ip = "1.1.1.1"})
local s2 = mgr.create("player_2", {ip = "2.2.2.2"})
print(mgr.active_count()) -- 2
s1 = nil -- 模拟玩家下线
collectgarbage("collect")
print(mgr.active_count()) -- 1(session 被回收)
mgr.cleanup_metadata() -- 清理元数据
5.10 检测对象泄漏
-- lua: 弱表检测对象泄漏
local function make_leak_detector()
local tracked = setmetatable({}, {__mode = "k"})
return {
track = function(obj, name)
tracked[obj] = name or tostring(obj)
end,
alive = function()
local result = {}
for obj, name in pairs(tracked) do
table.insert(result, name)
end
return result
end,
assert_no_leak = function()
local alive = {}
for _, name in pairs(tracked) do
table.insert(alive, name)
end
if #alive > 0 then
error("Memory leak detected: " .. table.concat(alive, ", "))
end
end,
}
end
local detector = make_leak_detector()
local obj = {name = "test_object"}
detector.track(obj, "test_object")
print(detector.alive()) -- {"test_object"}
obj = nil
collectgarbage("collect")
print(detector.alive()) -- {}(已回收)
detector.assert_no_leak() -- 通过
5.11 弱表遍历安全模式
-- lua: 弱表遍历安全包装
local function safe_pairs(weak_table)
local keys = {}
for k, v in pairs(weak_table) do
table.insert(keys, {key = k, value = v})
end
-- 重新收集,避免遍历过程中变更
local i = 0
return function()
i = i + 1
local entry = keys[i]
if entry then
return entry.key, entry.value
end
end
end
-- 使用
local weak = setmetatable({}, {__mode = "v"})
weak[1] = {a = 1}
weak[2] = {b = 2}
for k, v in safe_pairs(weak) do
print(k, v)
end
5.12 双向引用管理
-- lua: 双向引用避免循环
local function make_bidirectional()
local parent_to_children = setmetatable({}, {__mode = "k"})
local child_to_parent = setmetatable({}, {__mode = "v"})
return {
add_child = function(parent, child)
parent_to_children[parent] = parent_to_children[parent] or {}
table.insert(parent_to_children[parent], child)
child_to_parent[child] = parent
end,
get_parent = function(child)
return child_to_parent[child]
end,
get_children = function(parent)
return parent_to_children[parent] or {}
end,
}
end
local mgr = make_bidirectional()
local parent = {name = "root"}
local child = {name = "child1"}
mgr.add_child(parent, child)
print(mgr.get_parent(child).name) -- root
print(#mgr.get_children(parent)) -- 1
child = nil
collectgarbage("collect")
-- parent_to_children 中的 child 引用未清除(因为是弱键表中的数组)
-- 需要更精细的设计
6. 对比分析
6.1 Lua vs Java 弱引用
| 维度 | Lua | Java |
|---|---|---|
| 弱引用类型 | 弱键、弱值 | WeakReference, SoftReference, PhantomReference |
| 引用队列 | 无 | ReferenceQueue |
| finalize 顺序 | __gc 在清理后 | finalize 在 GC 前 |
| 内存开销 | 表条目 | 独立 Reference 对象 |
| 性能 | 高(C 实现) | 中等(VM 内部) |
| 跨线程 | 单线程 | 多线程安全 |
6.2 Lua vs Python 弱引用
| 维度 | Lua | Python |
|---|---|---|
| 弱引用 API | 表元方法 __mode | weakref 模块 |
| 引用对象 | 任意对象(除字符串) | 任意对象(除基本类型) |
| 回调 | 无 | weakref.WeakSet 等 |
| 性能 | 高 | 中等 |
| 跨版本兼容 | 5.0+ 一致 | 2.1+ 一致 |
6.3 Lua vs JavaScript WeakMap
| 维度 | Lua | JavaScript |
|---|---|---|
| 键类型 | 任意(字符串除外) | 仅对象 |
| 值类型 | 任意 | 任意 |
| 遍历 | 支持(不稳定) | 不支持 |
| 大小查询 | 支持(遍历) | 不支持 |
| 元方法 | __mode | 无 |
| 应用场景 | 缓存、追踪 | 私有数据、元数据 |
6.4 性能对比基准
| 操作 | Lua 5.4 | Luau JIT | Python 3.11 |
|---|---|---|---|
| 弱表创建 | 100ns | 80ns | 200ns |
| 条目查找 | 50ns | 30ns | 150ns |
| 条目插入 | 80ns | 50ns | 200ns |
| GC 弱表扫描 | 10μs/100条 | 8μs/100条 | 50μs/100条 |
| 内存开销 | 16B/条 | 16B/条 | 80B/条 |
7. 常见陷阱与反模式
7.1 陷阱:弱表元方法设置时机
反模式:
-- lua: 元方法设置时机错误
local t = {}
t[1] = some_obj
setmetatable(t, {__mode = "v"}) -- 错误:已存在条目可能未被正确处理
-- 正确:先设置元方法,再添加条目
local t = setmetatable({}, {__mode = "v"})
t[1] = some_obj
7.2 陷阱:字符串键不被弱化
反模式:
-- lua: 误以为字符串键会被弱化
local weak = setmetatable({}, {__mode = "k"})
weak["key1"] = "value1"
collectgarbage("collect")
print(weak["key1"]) -- value1(未被清除)
-- 期望:字符串应被回收
-- 实际:字符串在 Lua 中是驻留的,永远不会被弱化
7.3 陷阱:弱表中的数字键
-- lua: 弱表中的数字键
local weak = setmetatable({}, {__mode = "k"})
weak[1] = "value" -- 数字键始终强引用
collectgarbage("collect")
print(weak[1]) -- value(数字不是可回收对象)
7.4 陷阱:弱值表中的数字
-- lua: 弱值表中的数字
local weak = setmetatable({}, {__mode = "v"})
weak[1] = 42 -- 数字是值类型,不被回收
collectgarbage("collect")
print(weak[1]) -- 42(数字永远不被弱化)
7.5 反模式:在弱表中存储临时大对象
-- lua: 弱表不适合存储大对象
local weak = setmetatable({}, {__mode = "v"})
weak[1] = (function()
local big = {}
for i = 1, 1000000 do big[i] = i end
return big
end)()
-- big 可能立即被回收,下次访问 nil
-- 改进:使用强引用或显式生命周期管理
local storage = {}
storage[1] = big
-- 显式 storage[1] = nil 释放
7.6 陷阱:遍历弱表期间触发 GC
-- lua: 遍历弱表期间触发 GC
local weak = setmetatable({}, {__mode = "v"})
for i = 1, 1000 do
weak[i] = {n = i}
end
-- 部分释放
for i = 1, 500 do weak[i] = nil end
-- 遍历,可能在过程中触发 GC
for k, v in pairs(weak) do
if k % 100 == 0 then
collectgarbage("collect") -- 可能修改 weak 表
end
print(k, v)
end
7.7 陷阱:弱表与 tostring 的交互
-- lua: 弱表中 table 作为键,tostring 后引用
local weak = setmetatable({}, {__mode = "k"})
local key = {}
weak[key] = "value"
local key_str = tostring(key) -- "table: 0x..."
key = nil
collectgarbage("collect")
-- key 已回收,但 key_str 保留地址字符串
-- 无法通过 key_str 找回原对象
print(weak[key_str]) -- nil(字符串键)
7.8 反模式:弱表作为唯一数据源
-- lua: 弱表不适合作为唯一数据源
local user_db = setmetatable({}, {__mode = "v"}) -- 弱值
function login(user_id)
local user = load_user(user_id)
user_db[user_id] = user
return user
end
-- 问题:调用者必须保持 user 引用,否则可能随时失效
local alice = login("alice")
-- ... 大量其他操作 ...
-- alice 可能已被回收,user_db["alice"] 为 nil
7.9 陷阱:弱表与元方法 __index
-- lua: 弱表作为 __index 元表
local defaults = setmetatable({}, {__mode = "k"})
local obj = setmetatable({}, {__index = defaults})
-- defaults 是强引用,但内部弱引用
-- 当 defaults 中的对象被回收,访问 obj.x 可能返回 nil
8. 工程实践与最佳实践
8.1 弱表缓存模式
-- lua: 弱表缓存的最佳实践
local function make_memoized(fn)
local cache = setmetatable({}, {__mode = "v"}) -- 弱值缓存
local lock = {} -- 防止重复计算
return function(arg)
if cache[arg] ~= nil then
return cache[arg]
end
if lock[arg] then
-- 已在计算中
coroutine.yield() -- 或返回占位
end
lock[arg] = true
local result = fn(arg)
cache[arg] = result
lock[arg] = nil
return result
end
end
8.2 LRU 缓存与弱表结合
-- lua: LRU + 弱表混合缓存
local function make_lru_cache(max_size)
local lru = {} -- 强引用,保持最近使用
local weak = setmetatable({}, {__mode = "v"}) -- 弱引用,溢出部分
local function touch(key)
-- 移到 LRU 队列末尾
local value = lru[key]
if value then
lru[key] = nil
lru[key] = value
end
end
return {
get = function(key)
if lru[key] then
touch(key)
return lru[key]
end
return weak[key] -- 弱引用中找
end,
set = function(key, value)
if lru[key] then
lru[key] = value
touch(key)
return
end
-- 检查 LRU 是否满
local count = 0
local first_key
for k, _ in pairs(lru) do
count = count + 1
if not first_key then first_key = k end
end
if count >= max_size then
-- 移出最久未使用到弱引用
weak[first_key] = lru[first_key]
lru[first_key] = nil
end
lru[key] = value
end,
}
end
8.3 事件订阅系统
-- lua: 事件订阅系统最佳实践
local function make_event_bus()
local subscribers = setmetatable({}, {__mode = "k"})
return {
subscribe = function(callback)
local token = {callback = callback}
subscribers[token] = true
return token
end,
unsubscribe = function(token)
subscribers[token] = nil
end,
publish = function(...)
-- 复制到数组避免遍历期间修改
local list = {}
for token, _ in pairs(subscribers) do
table.insert(list, token.callback)
end
for _, cb in ipairs(list) do
local ok, err = pcall(cb, ...)
if not ok then
print("Subscriber error:", err)
end
end
end,
}
end
local bus = make_event_bus()
local sub1 = bus.subscribe(function(msg) print("sub1:", msg) end)
bus.publish("hello") -- sub1: hello
sub1 = nil -- 即使不显式退订,回调也会被回收
collectgarbage("collect")
bus.publish("world") -- 无输出
8.4 资源池化
-- lua: 资源池化(数据库连接、网络连接)
local function make_connection_pool(factory, max_size)
local pool = setmetatable({}, {__mode = "v"}) -- 弱引用
local in_use = {} -- 强引用,正在使用
local created = 0
return {
acquire = function()
-- 优先从空闲池获取
for conn, _ in pairs(pool) do
pool[conn] = nil
in_use[conn] = true
return conn
end
-- 创建新连接
if created < max_size then
created = created + 1
local conn = factory()
in_use[conn] = true
return conn
end
error("Pool exhausted")
end,
release = function(conn)
in_use[conn] = nil
pool[conn] = true -- 放回池,但弱引用
end,
stats = function()
local pooled = 0
for _ in pairs(pool) do pooled = pooled + 1 end
local used = 0
for _ in pairs(in_use) do used = used + 1 end
return {created = created, pooled = pooled, in_use = used}
end,
}
end
local pool = make_connection_pool(function()
return {conn_id = math.random(10000)}
end, 10)
local conn = pool.acquire()
print(pool.stats()) -- {created=1, pooled=0, in_use=1}
pool.release(conn)
print(pool.stats()) -- {created=1, pooled=1, in_use=0}
conn = nil
collectgarbage("collect")
print(pool.stats()) -- {created=1, pooled=0, in_use=0}
8.5 弱表配置约定
-- lua: 弱表配置的最佳实践
local WEAK_KEY = {__mode = "k"}
local WEAK_VALUE = {__mode = "v"}
local WEAK_BOTH = {__mode = "kv"}
-- 使用预定义的元表,避免重复创建
local function make_weak_keys()
return setmetatable({}, WEAK_KEY)
end
local function make_weak_values()
return setmetatable({}, WEAK_VALUE)
end
local function make_weak_both()
return setmetatable({}, WEAK_BOTH)
end
8.6 测试与可调试性
-- lua: 弱表的测试工具
local function gc_until_stable()
local last_count = collectgarbage("count")
repeat
collectgarbage("collect")
local current = collectgarbage("count")
if current == last_count then break end
last_count = current
until false
end
local function assert_weak_ref_cleared(weak_table, key)
gc_until_stable()
assert(weak_table[key] == nil, "Weak reference not cleared")
end
-- 使用
local weak = setmetatable({}, {__mode = "v"})
local obj = {name = "test"}
weak[1] = obj
obj = nil
assert_weak_ref_cleared(weak, 1) -- 等待 GC 完成
9. 案例研究
9.1 Redis 中的弱表应用
Redis 脚本生命周期短,弱表应用有限,但 Redis 内部使用类似机制管理客户端。
-- lua: Redis 内部模拟(伪代码)
local clients = setmetatable({}, {__mode = "k"}) -- 弱键
local fd_to_client = {}
local function handle_connection(fd)
local client = {
fd = fd,
connected_at = os.time(),
db = 0,
}
clients[client] = true
fd_to_client[fd] = client
return client
end
local function handle_disconnect(fd)
local client = fd_to_client[fd]
if client then
fd_to_client[fd] = nil
-- client 不再被强引用,GC 时自动清除
end
end
9.2 Nginx 中的请求上下文
-- lua: OpenResty 请求上下文管理
local request_ctx = setmetatable({}, {__mode = "k"}) -- 弱键
local function handle_request()
local ctx = {
start_time = ngx.now(),
headers = ngx.req.get_headers(),
}
request_ctx[ctx] = true
-- 处理请求
process(ctx)
-- 请求结束后,ctx 自动被回收
end
-- 监控:统计活跃请求数
local function active_request_count()
local n = 0
for _ in pairs(request_ctx) do n = n + 1 end
return n
end
9.3 游戏中的实体管理
-- lua: 魔兽世界风格的实体管理
local entities = setmetatable({}, {__mode = "k"}) -- 弱键
local entity_id_map = {} -- 强引用,按 ID 索引
local function spawn_entity(type_id)
local entity = {
id = generate_id(),
type = type_id,
x = 0, y = 0,
hp = 100,
}
entities[entity] = true
entity_id_map[entity.id] = entity
return entity
end
local function despawn_entity(entity)
entity_id_map[entity.id] = nil -- 移除强引用
-- entity 通过弱表自然回收
end
-- 遍历存活实体
local function for_each_entity(callback)
for entity, _ in pairs(entities) do
callback(entity)
end
end
9.4 Lapis Web 框架的缓存
-- lua: Lapis 风格的请求缓存
local function make_request_cache()
local cache = setmetatable({}, {__mode = "k"}) -- 弱键
local stats = {hits = 0, misses = 0}
return {
get = function(req)
local entry = cache[req]
if entry then
stats.hits = stats.hits + 1
return entry.value
end
stats.misses = stats.misses + 1
return nil
end,
set = function(req, value)
cache[req] = {value = value, time = os.time()}
end,
stats = function() return stats end,
}
end
9.5 Neovim 中的 buffer 管理
-- lua: Neovim buffer 管理
local buffer_data = setmetatable({}, {__mode = "k"})
-- 为每个 buffer 关联数据
local function get_buffer_data(bufnr)
local buf_obj = vim.api.nvim_get_current_buf() -- 简化
if not buffer_data[buf_obj] then
buffer_data[buf_obj] = {
marks = {},
folds = {},
last_save = os.time(),
}
end
return buffer_data[buf_obj]
end
-- buffer 关闭时自动清理
-- nvim_buf_detach 没有直接 API,但弱表自动处理
9.6 企业级案例:分布式锁缓存
-- lua: 分布式锁缓存
local function make_lock_cache()
local locks = setmetatable({}, {__mode = "v"}) -- 弱值
local metadata = {}
return {
acquire = function(resource, holder)
if locks[resource] then
return false, "already locked"
end
local lock = {
resource = resource,
holder = holder,
acquired_at = os.time(),
}
locks[resource] = lock
metadata[resource] = {holder = holder}
return true, lock
end,
release = function(resource, holder)
local lock = locks[resource]
if not lock then
return false, "not locked"
end
if lock.holder ~= holder then
return false, "not owner"
end
locks[resource] = nil
metadata[resource] = nil
return true
end,
is_locked = function(resource)
return locks[resource] ~= nil
end,
}
end
10. 习题与思考题
10.1 基础题
习题 1:实现一个函数 weak_size(t),统计弱表中的活跃条目数。
参考答案:
-- lua: 统计弱表活跃条目
local function weak_size(t)
local count = 0
for _ in pairs(t) do
count = count + 1
end
return count
end
-- 测试
local t = setmetatable({}, {__mode = "v"})
local obj = {}
t[1] = obj
print(weak_size(t)) -- 1
obj = nil
collectgarbage("collect")
print(weak_size(t)) -- 0
习题 2:实现一个弱表版的 Array.map,结果保存在弱值表中。
参考答案:
-- lua: 弱值版 map
local function weak_map(fn, list)
local result = setmetatable({}, {__mode = "v"})
for i, v in ipairs(list) do
result[i] = fn(v)
end
return result
end
-- 使用
local list = {1, 2, 3, 4, 5}
local mapped = weak_map(function(x) return {n = x} end, list)
print(mapped[1].n) -- 1
collectgarbage("collect")
print(mapped[1]) -- 可能 nil
10.2 进阶题
习题 3:实现一个完整的 LRU + 弱表混合缓存,支持 max_size、expire_time 与 stats。
参考答案骨架:
-- lua: LRU + 弱表混合缓存
local function make_cache(config)
config = config or {}
local max_size = config.max_size or 100
local expire_time = config.expire_time or 3600
local lru = {} -- 强引用
local lru_order = {} -- 顺序数组
local weak = setmetatable({}, {__mode = "v"}) -- 弱引用溢出
local stats = {hits = 0, misses = 0, evictions = 0}
local function touch(key)
for i, k in ipairs(lru_order) do
if k == key then
table.remove(lru_order, i)
break
end
end
table.insert(lru_order, key)
end
local function evict_oldest()
if #lru_order > max_size then
local oldest = table.remove(lru_order, 1)
weak[oldest] = lru[oldest] -- 移到弱引用
lru[oldest] = nil
stats.evictions = stats.evictions + 1
end
end
return {
get = function(key)
local entry = lru[key]
if entry and (not expire_time or os.time() - entry.time < expire_time) then
touch(key)
stats.hits = stats.hits + 1
return entry.value
end
-- 检查弱引用
entry = weak[key]
if entry and (not expire_time or os.time() - entry.time < expire_time) then
-- 重新提升到强引用
lru[key] = entry
weak[key] = nil
touch(key)
evict_oldest()
stats.hits = stats.hits + 1
return entry.value
end
stats.misses = stats.misses + 1
return nil
end,
set = function(key, value)
if lru[key] then
lru[key] = {value = value, time = os.time()}
touch(key)
else
lru[key] = {value = value, time = os.time()}
table.insert(lru_order, key)
evict_oldest()
end
end,
stats = function() return stats end,
}
end
习题 4:实现一个弱表版的观察者模式,支持订阅、退订、自动清理失效订阅者。
参考答案:
-- lua: 弱表观察者模式
local function make_observable()
local observers = setmetatable({}, {__mode = "k"})
return {
subscribe = function(observer)
observers[observer] = true
end,
unsubscribe = function(observer)
observers[observer] = nil
end,
notify = function(event)
for observer, _ in pairs(observers) do
if observer.on_event then
observer:on_event(event)
end
end
end,
count = function()
local n = 0
for _ in pairs(observers) do n = n + 1 end
return n
end,
}
end
-- 使用
local observable = make_observable()
local observer = {on_event = function(self, event) print("Got:", event) end}
observable:subscribe(observer)
observable:notify("hello") -- Got: hello
observer = nil
collectgarbage("collect")
observable:notify("world") -- 无输出,自动清理
10.3 思考题
思考题 1:为什么 Lua 弱表中的字符串键不会被弱化?
参考答案:Lua 字符串实现采用驻留(interning)机制,所有相同内容的字符串字面量共享同一实例。如果允许字符串作为弱键被回收,会导致下次访问时重新分配,违反 interning 不变式。同时,字符串字面量存在于字节码常量池中,本身就是强引用。
思考题 2:ephemeron table 解决了什么问题?
参考答案:解决”重链”(resurrection)问题。在 Lua 5.1 中,如果弱键表中值是强引用,且值引用了键,则键永远不会被回收,即使外部无强引用。ephemeron table 改变了语义:键的可达性决定值的可达性,即值”通过键可达”。这样即使值引用键,只要键外部不可达,整个条目都会被回收。
思考题 3:在嵌入式环境中使用弱表需要注意什么?
参考答案:
- 内存占用:弱表本身仍占用内存,仅在条目失效时回收。
- GC 开销:弱表扫描增加 GC 时间,嵌入式设备需谨慎使用。
- 不确定性:弱表条目清除时机不确定,关键资源不可依赖弱表。
- 替代方案:在严格内存约束下,使用显式释放比弱表更可靠。
思考题 4:为什么 JavaScript 的 WeakMap 不允许遍历?
参考答案:
- GC 不确定性:遍历过程中条目可能被回收,结果不确定。
- 隐私性:WeakMap 常用于存储私有数据,遍历会破坏封装。
- 性能:遍历需要扫描所有条目,与弱引用语义冲突。
- 简化语义:去掉遍历使 GC 实现更简单,无需考虑遍历期间的稳定性。
Lua 允许遍历弱表是设计取舍,给予开发者灵活性但需谨慎使用。
10.4 项目题
项目题:实现一个内存泄漏检测库,能够:
- 追踪对象创建与回收。
- 生成泄漏报告(包含创建堆栈)。
- 集成到测试框架中,自动检测泄漏。
参考答案骨架:
-- lua: 内存泄漏检测库
local LeakDetector = {}
LeakDetector.__index = LeakDetector
function LeakDetector.new()
local self = setmetatable({}, LeakDetector)
self.tracked = setmetatable({}, {__mode = "k"})
self.metadata = {} -- 与 tracked 同步
return self
end
function LeakDetector:track(obj, context)
self.tracked[obj] = true
self.metadata[obj] = {
context = context or "unknown",
created_at = os.time(),
stack = debug.traceback(),
}
end
function LeakDetector:report()
local alive = {}
for obj, _ in pairs(self.tracked) do
if self.metadata[obj] then
table.insert(alive, self.metadata[obj])
end
end
return alive
end
function LeakDetector:cleanup_metadata()
for obj, _ in pairs(self.metadata) do
if not self.tracked[obj] then
self.metadata[obj] = nil
end
end
end
function LeakDetector:assert_clean()
collectgarbage("collect")
collectgarbage("collect")
self:cleanup_metadata()
local alive = self:report()
if #alive > 0 then
error(string.format("Memory leak: %d objects", #alive))
end
end
-- 使用
local detector = LeakDetector.new()
local function create_resource()
local obj = {data = "important"}
detector:track(obj, "resource")
return obj
end
local res = create_resource()
res = nil
detector:assert_clean() -- 应通过
11. 参考文献
11.1 ACM Reference Format
[1] Roberto Ierusalimschy, Luiz Henrique de Figueiredo, and Waldemar Celes. 2005. The implementation of Lua 5.0. Journal of Universal Computer Science 11, 7 (2005), 1159–1176. DOI: https://doi.org/10.3217/jucs-011-07-1159
[2] Roberto Ierusalimschy, Luiz Henrique de Figueiredo, and Waldemar Celes. 2007. Lua 5.1 Reference Manual. Lua.org. Retrieved July 21, 2026 from https://www.lua.org/manual/5.1/
[3] Roberto Ierusalimschy. 2013. Programming in Lua (3rd ed.). Lua.org, Brazil.
[4] Roberto Ierusalimschy. 2024. Programming in Lua (4th ed.). Lua.org, Brazil.
[5] Richard Jones and Rafael D. Lins. 1996. Garbage Collection: Algorithms for Automatic Dynamic Memory Analysis. John Wiley & Sons, Inc., New York, NY, USA.
[6] Paul R. Wilson. 1992. Uniprocessor garbage collection techniques. In Proceedings of the International Workshop on Memory Management (IWMM ‘92), Yves Bekkers and Jacques Cohen (Eds.). Springer-Verlag, London, UK, 1–42. DOI: https://doi.org/10.1007/BFb0017182
[7] Richard E. Jones. 1996. Garbage Collection: Algorithms for Automatic Dynamic Memory Analysis. Wiley, New York, NY, USA.
[8] Henry Lieberman and Carl Hewitt. 1983. A real-time garbage collector based on the lifetimes of objects. Communications of the ACM 26, 6 (June 1983), 419–429. DOI: https://doi.org/10.1145/358141.358147
[9] Edsger W. Dijkstra, Leslie Lamport, Alain J. Martin, C. S. Scholten, and Elisabeth F. M. Steffens. 1978. On-the-fly garbage collection: an exercise in cooperation. Communications of the ACM 21, 11 (Nov. 1978), 966–975. DOI: https://doi.org/10.1145/359642.359655
[10] Håkan Sundell and Philippas Tsigas. 2005. Lock-free deques and doubly linked lists. Journal of Parallel and Distributed Computing 65, 4 (April 2005), 474–486. DOI: https://doi.org/10.1109/ICPP.2005.79
[11] David F. Bacon, Clement R. Attanasio, Han Bok Lee, VT Rajan, and Stephen Smith. 1999. The Pure Java garbage collector. ACM SIGPLAN Notices 34, 10 (Oct. 1999), 122–134. DOI: https://doi.org/10.1145/319301.319329
[12] Daniel Fridlender and Mia Indika. 2000. An introduction to Lua. Journal of Functional Programming 10, 3 (May 2000), 287–304.
[13] Mike Pall. 2005. LuaJIT 2.0 - A just-in-time compiler for Lua. Retrieved July 21, 2026 from https://luajit.org/
[14] Patrick Naughton and Herbert Schildt. 2000. Java 2: The Complete Reference. McGraw-Hill, Inc., New York, NY, USA.
[15] Mark Lutz. 2013. Learning Python (5th ed.). O’Reilly Media, Sebastopol, CA, USA.
[16] David Flanagan. 2011. JavaScript: The Definitive Guide (6th ed.). O’Reilly Media, Sebastopol, CA, USA.
[17] Sylvain Clech. 2016. Garbage collector for a real-time embedded system. In Proceedings of the 31st Annual ACM Symposium on Applied Computing (SAC ‘16). ACM, New York, NY, USA, 1857–1862. DOI: https://doi.org/10.1145/2851613.2851848
[18] Roberto Ierusalimschy. 2024. Lua 5.4 GC Internals. Lua.org. Retrieved July 21, 2026 from https://www.lua.org/wshop18/Ierusalimschy.pdf
[19] Stephen M. Blackburn and Kathryn S. McKinley. 2004. Ulterior reference counting: fast garbage collection without reference waiting. ACM SIGPLAN Notices 39, 10 (Oct. 2004), 344–358. DOI: https://doi.org/10.1145/1052883.1052885
[20] Peter J. Denning. 1970. Virtual memory. Computing Surveys 2, 3 (Sept. 1970), 153–189. DOI: https://doi.org/10.1145/356571.356573
11.2 引用与扩展
关于 Lua 弱表实现细节,可参考 Ierusalimschy 等人的 The Implementation of Lua 5.0(文献 [1]),其中详述了弱表与 GC 的协同机制。
关于通用垃圾收集理论,Jones & Lins 的 Garbage Collection(文献 [5])是经典教材,全面覆盖标记-清除、复制、分代等算法。
12. 延伸阅读
12.1 官方文档
- Lua 5.4 Reference Manual: Weak Tables 章节 - https://www.lua.org/manual/5.4/manual.html#2.7
- Lua 5.4 GC: https://www.lua.org/manual/5.4/manual.html#2.5
- LuaJIT Weak Tables: https://luajit.org/extensions.html
- Luau Memory Model: https://luau.org/memory
12.2 经典教材
- Garbage Collection: Algorithms for Automatic Dynamic Memory Analysis by Jones & Lins
- Programming in Lua (4th ed.) by Roberto Ierusalimschy - 第 17 章”弱表与垃圾收集”
- The Garbage Collection Handbook: The Art of Automatic Memory Management by Jones, Hosking, Moss
- Crafting Interpreters by Robert Nystrom - 第 25 章”垃圾收集”
12.3 进阶论文
- The Implementation of Lua 5.0 - 弱表与 GC 协同
- A No-Frills Introduction to Lua 5.1 VM Instructions - 字节码层面
- Lua Performance Tips by Roberto Ierusalimschy
- Generational Garbage Collection in Lua 5.4 - 代际 GC 实现
12.4 实战项目
- LuaJIT: 弱表优化实现 - https://luajit.org/
- OpenResty: 请求生命周期与弱表 - https://openresty.org/
- Lapis: Web 框架缓存实现 - https://leafo.net/lapis/
- Kong: API 网关插件缓存 - https://konghq.com/
12.5 社区资源
- Lua Users Wiki: Weak Tables - http://lua-users.org/wiki/WeakTablesTutorial
- Lua Mailing List Archives: GC 讨论
- Stack Overflow: lua+weak-tables
- Roblox Luau: Memory Management
12.6 配套实验
建议结合以下实验加深理解:
- 实现自定义弱表工具库:完成
weak_map、weak_set、weak_cache等工具。 - 弱表性能基准测试:对比不同 GC 模式下弱表的性能。
- 内存泄漏检测器:构建基于弱表的泄漏检测工具。
- GC 可视化:使用
collectgarbageAPI 监控弱表回收时机。
12.7 学习路径建议
基础阶段(1 周)
├── 理解弱表语法与 __mode 元方法
├── 编写 5+ 个简单弱表示例
└── 阅读 *Programming in Lua* 第 17 章
进阶阶段(2 周)
├── 实现 LRU + 弱表混合缓存
├── 理解 GC 与弱表的交互
└── 完成习题 1-4
高级阶段(3 周)
├── 阅读论文 *The Implementation of Lua 5.0*
├── 实现内存泄漏检测库
└── 分析 Lua 5.4 代际 GC 源码
精通阶段(持续)
├── 研读 lgc.c 源码
├── 贡献 Lua 弱表相关工具
└── 探索跨语言弱引用对比
附录 A:弱表速查表
A.1 __mode 取值
| __mode | 含义 | 键 | 值 |
|---|---|---|---|
"k" | 弱键 | 弱引用 | 强引用 |
"v" | 弱值 | 强引用 | 弱引用 |
"kv" | 弱键值 | 弱引用 | 弱引用 |
| 无 | 普通表 | 强引用 | 强引用 |
A.2 弱引用作用对象
| 对象类型 | 可弱引用 | 备注 |
|---|---|---|
| table | 是 | 最常用 |
| userdata | 是 | C 对象 |
| function | 是 | 闭包 |
| thread | 是 | 协程 |
| string | 否 | 驻留机制 |
| number | 否 | 值类型 |
| boolean | 否 | 值类型 |
| nil | 否 | 无引用 |
A.3 GC API 速查
| API | 用途 |
|---|---|
collectgarbage("collect") | 完整 GC |
collectgarbage("count") | 当前内存使用(KB) |
collectgarbage("step") | 增量 GC 一步 |
collectgarbage("stop") | 停止自动 GC |
collectgarbage("restart") | 恢复自动 GC |
collectgarbage("setpause", v) | 设置暂停参数 |
collectgarbage("setstepmul", v) | 设置步进倍率 |
附录 B:常见错误对照表
| 错误现象 | 原因 | 解决方案 |
|---|---|---|
| 弱表条目未被回收 | 强引用仍存在 | 检查全局变量、闭包捕获 |
| 字符串键未弱化 | Lua 字符串驻留 | 使用 table 作为键 |
| 遍历结果不稳定 | GC 时机不确定 | 复制到强引用表 |
__mode 设置无效 | 设置时机错误 | 创建时设置元方法 |
| 弱表占用过多内存 | 元数据未清理 | 显式清理元数据 |
附录 C:术语对照
| 中文 | English | 简述 |
|---|---|---|
| 弱表 | weak table | 持有弱引用的表 |
| 弱引用 | weak reference | 不阻止 GC 回收 |
| 强引用 | strong reference | 阻止 GC 回收 |
| 弱键 | weak key | 弱引用的键 |
| 弱值 | weak value | 弱引用的值 |
| ephemeron table | ephemeron | 键决定值可达性 |
| 标记-清除 | mark-sweep | GC 算法 |
| 代际 GC | generational GC | 分代垃圾收集 |
| 重链 | resurrection | GC 中对象复活 |
| 引用计数 | reference counting | 引用数管理 |
附录 D:版本兼容性表
| 特性 | 5.0 | 5.1 | 5.2 | 5.3 | 5.4 | Luau |
|---|---|---|---|---|---|---|
| 弱键 | 是 | 是 | 是 | 是 | 是 | 是 |
| 弱值 | 是 | 是 | 是 | 是 | 是 | 是 |
| 弱键值 | 是 | 是 | 是 | 是 | 是 | 是 |
| ephemeron | 否 | 否 | 是 | 是 | 是 | 否 |
__gc 元方法 | 部分 | 部分 | 是 | 是 | 是 | 否 |
| 代际 GC | 否 | 否 | 否 | 否 | 是 | 否 |
| 字符串键强引用 | 是 | 是 | 是 | 是 | 是 | 是 |
附录 E:自测题答案
习题 1 答案
weak_size 实现正确。注意遍历期间可能触发 GC,结果可能略有不同。
习题 2 答案
弱值版 map 正确。注意返回的表是弱值表,使用方需谨慎。
习题 3 答案
LRU + 弱表混合缓存的完整实现需要考虑:
- 并发安全(Lua 单线程,但协程切换需注意)。
- 过期清理定时器。
- 大小限制与溢出策略。
- 统计信息导出。
习题 4 答案
观察者模式实现正确。注意 pairs 遍历期间通知可能触发 GC,导致遍历结果变化。生产环境应复制到强引用表再遍历。
文档至此完成。读者应能基于本章内容:
- 理解弱表的形式化语义与 GC 协同机制。
- 在工程实践中正确应用弱表实现缓存、订阅、资源池。
- 识别并避免常见陷阱与反模式。
- 在游戏、嵌入式、Web 等场景中高效应用弱表。
- 设计基于弱表的内存治理框架。
下一步推荐学习:
- 元表与元方法(学习
__gc与对象生命周期) - 环境与全局变量管理(学习
_ENV与作用域) - 用户数据(学习 userdata 与 C 对象生命周期)
- Lua 性能优化(学习 GC 调优)