Lua 源码阅读完全指南 - 从虚拟机到垃圾回收

本文系统介绍 Lua 源码的核心概念,包括虚拟机原理、编译流程、垃圾回收机制、热更新原理等高级话题。


一、学习路线

1.1 推荐书籍

阶段 书籍 说明
初级 《Programming in Lua》(Lua 编程) 编写 Lua 代码的基本操作
中级 《Lua 程序设计第四版》 深入理解 Lua 特性
高级 《Lua 设计与实现》 Lua 实现的算法、虚拟机、编译器、GC

1.2 学习资源

资源类型 链接
官方文档 https://www.lua.org/
Bilibili 教程 https://www.bilibili.com/video/BV1Zz411i7QW
中文文档 https://cloudwu.github.io/lua53doc/
CSDN 系列 https://blog.csdn.net/yuanlin2008

二、源码文件结构

2.1 虚拟机核心文件

文件名 作用 接口前缀
lopcodes.c 字节码/操作码定义 luaP_
lcode.c 源码生成器 luaK_
llex.c 词法分析 luaX_
lparser.c 语法分析器 luaY_
lapi.c C 语言接口 lua_
ldebug.c 调试库、反射、钩子 luaG_
ldo.c 函数调用、栈管理 luaD_
ldump.c 预编译块序列化 luaU_
lfunc.c 原型和闭包操作 luaF_
lgc.c 垃圾回收 luaC_
lmem.c 内存管理 luaM_
lobject.c 对象管理 luaO_
lstate.c 全局状态机 luaE_
lstring.c 字符串操作 luaS_
ltable.c 表操作 luaH_
lundump.c 加载预编译字节码 luaU_
ltm.c Tag 方法 luaT_
lzio.c 缓存流接口 luaZ_
lvm.c 虚拟机执行引擎 luaV_

2.2 内嵌库文件

文件名 作用 接口前缀
lauxlib.c 辅助函数库 luaL_
lbaselib.c 基础库 luaB_
ldblib.c 调试库 db_
liolib.c IO 库 io_
lmathlib.c 数学库 math_
loslib.c 操作系统库 os_
ltablib.c 表操作库 -
lstrlib.c 字符串库 str_
loadlib.c 动态加载器 ll_

2.3 编译器与解释器

文件名 作用
lua.c Lua 解释器
luac.c 字节码编译器

三、虚拟机原理

3.1 虚拟机类型对比

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
┌─────────────────────────────────────────────────────────────────────────┐
│ 虚拟机实现方式对比 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ 基于栈的 VM │ │ 基于寄存器的 VM │ │
│ │ (Stack-Based) │ │ (Register-Based) │ │
│ ├─────────────────────┤ ├─────────────────────┤ │
│ │ JVM, Python │ │ Lua, LuaJIT │ │
│ │ 操作数在栈中 │ │ 操作数在寄存器中 │ │
│ │ 需要入栈/出栈操作 │ │ 无需入栈/出栈 │ │
│ │ 指令执行次数多 │ │ 指令执行次数少 │ │
│ │ 内存复制多 │ │ 内存复制少 │ │
│ │ 占用内存较少 │ │ 指令占用空间较大 │ │
│ └─────────────────────┘ └─────────────────────┘ │
│ │
│ 为什么 Lua 快? │
│ └── 基于寄存器的 VM 减少了指令执行次数和内存复制操作 │
│ │
└─────────────────────────────────────────────────────────────────────────┘

3.2 Lua 指令格式

Lua 指令分为 5 种格式:

格式 说明 用途
iABC 标准指令 大部分算术/逻辑运算
iABx 带 Bx 扩展 加载常量、跳转
iAsBx 带符号 Bx 条件跳转
iAx 单大操作数 EXTRA
ISx 系统指令 特殊操作

3.3 查看字节码

1
2
3
4
5
6
7
8
# 查看 Lua 代码生成的字节码
luac -l -l test.lua

# 生成预编译文件
luac -o test.luac test.lua

# 运行预编译文件
lua test.luac

四、类型系统

4.1 基本类型

类型 说明 特点
nil 空类型 只有一个值 nil
boolean 布尔 true / false
number 数字 Lua 5.3+ 默认 64 位浮点
string 字符串 不可变,自动去重
table 关联数组,Lua 的核心
function 函数 支持 C 函数和 Lua 函数
userdata 用户数据 封装 C 数据
thread 线程 实际是协程

4.2 Table 类型详解

Table 是 Lua 的核心数据结构,内部实现为数组 + 哈希表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
┌─────────────────────────────────────────────────────────────────────────┐
│ Table 内部结构 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Table │
│ ┌─────────────────────┬─────────────────────┐ │
│ │ 数组部分 │ 哈希表部分 │ │
│ │ Array Part │ Hash Part │ │
│ ├─────────────────────┼─────────────────────┤ │
│ │ [1] = value1 │ ["key1"] = value3 │ │
│ │ [2] = value2 │ ["key2"] = value4 │ │
│ │ [3] = value3 │ [key3] = value5 │ │
│ │ ... │ ... │ │
│ │ 适用于连续整数索引 │ 适用于任意类型索引 │ │
│ └─────────────────────┴─────────────────────┘ │
│ │
│ 最佳实践: │
│ └── 避免混用数组和哈希表部分,提升性能 │
│ │
└─────────────────────────────────────────────────────────────────────────┘

五、元表与元方法

5.1 元表操作

1
2
3
4
5
-- 获取元表
getmetatable(obj)

-- 设置元表
setmetatable(obj, metatable)

5.2 元方法完整列表

元方法 触发条件 说明
算术运算
__add a + b 加法
__sub a - b 减法
__mul a * b 乘法
__div a / b 除法
__mod a % b 取模
__pow a ^ b 次方
__unm -a 取负
__idiv a // b 向下取整除法
位运算
__band a & b 按位与
__bor a | b 按位或
__bxor a ~ b 按位异或
__bnot ~a 按位非
__shl a << b 左移
__shr a >> b 右移
其他运算
__concat a .. b 字符串连接
__len #a 取长度
比较运算
__eq a == b 等于
__lt a < b 小于
__le a <= b 小于等于
表访问
__index table[key] 读取不存在的键
__newindex table[key] = value 写入不存在的键
其他
__call obj(args) 函数调用
__tostring tostring(obj) 转字符串
__gc GC 回收时 析构函数
__mode - 弱引用标记

5.3 __index 查找流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
┌─────────────────────────────────────────────────────────────────────────┐
│ __index 查找流程 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ table[key] 查找: │
│ │
│ ┌─────────────────┐ │
│ │ Step 1: 查自身 │ │
│ │ table 有 key? │ ──Yes──> 返回 value │
│ └────────┬────────┘ │
│ │ No │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Step 2: 查元表 │ │
│ │ 有 metatable? │ ──No───> 返回 nil │
│ └────────┬────────┘ │
│ │ Yes │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Step 3: 查 __index│ │
│ │ __index 是表? │ ──Yes──> 在 __index 表中查找 │
│ │ │ │
│ │ __index 是函数? │ ──Yes──> 调用 __index(table, key) │
│ └─────────────────┘ │
│ │
│ ⚠️ 注意: 不是查找 metatable 自身的 key,而是 metatable.__index │
│ │
└─────────────────────────────────────────────────────────────────────────┘

六、垃圾回收机制

6.1 GC 算法演进

版本 算法 特点
Lua 5.0 双色标记清除 全局暂停,会卡顿
Lua 5.1 三色增量标记 可增量执行,减少卡顿
Lua 5.4+ 分代 GC 进一步优化性能

6.2 三色标记算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
┌─────────────────────────────────────────────────────────────────────────┐
│ 三色标记算法 (Tri-Color Marking) │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 颜色含义: │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ ● 白色 (White): 待访问状态,对象创建后的初始状态 │ │
│ │ ● 灰色 (Gray): 待扫描状态,已访问但引用未遍历 │ │
│ │ ● 黑色 (Black): 已扫描状态,已访问且引用已遍历 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ 回收流程: │
│ │
│ 1. 初始阶段 │
│ └── 遍历 root 节点引用的对象,从白色置为灰色 │
│ │
│ 2. 标记阶段 │
│ └── 从灰色链表取对象 → 标记为黑色 → 遍历其引用 │
│ └── 白色引用 → 标记为灰色,加入灰色链表 │
│ │
│ 3. 回收阶段 │
│ └── 遍历所有对象 │
│ └── 白色对象 → 回收(未被引用) │
│ └── 其他颜色 → 重置为白色,等待下一轮 GC │
│ │
│ 双白色优化: │
│ └── 白色分为"当前白色"和"非当前白色",交替使用 │
│ └── 避免标记阶段后新创建的对象被误回收 │
│ │
└─────────────────────────────────────────────────────────────────────────┘

6.3 GC 控制函数

1
2
3
4
5
6
7
8
9
10
collectgarbage("opt" [, arg])

-- 选项:
-- "stop": 停止垃圾收集器
-- "restart": 重启垃圾收集器
-- "collect": 执行一次完整的垃圾收集
-- "count": 返回当前已用内存 (KB)
-- "step": 执行一步垃圾收集
-- "setpause": 设置 pause 参数 (百分比)
-- "setstepmul": 设置 step multiplier (百分比)

6.4 弱引用表

1
2
3
4
5
6
7
8
9
10
11
-- 弱引用键
weakKeyTable = {}
setmetatable(weakKeyTable, {__mode = "k"})

-- 弱引用值
weakValueTable = {}
setmetatable(weakValueTable, {__mode = "v"})

-- 弱引用键和值
weakTable = {}
setmetatable(weakTable, {__mode = "kv"})

七、模块加载流程

7.1 加载流程图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
┌─────────────────────────────────────────────────────────────────────────┐
│ Lua 模块加载流程 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ require("module.name") │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ 1. 检查缓存 │ │
│ │ package.loaded │ ───Yes──> 返回缓存的模块 │
│ └────────┬────────┘ │
│ │ No (未加载) │
│ ▼ │
│ ┌─────────────────┐ │
│ │ 2. 搜索文件 │ │
│ │ package.path │ │
│ └────────┬────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ 3. 依次尝试 Loader│ │
│ │ │ │
│ │ ┌─────────────┐ │ │
│ │ │ preload │ │ → package.preload │
│ │ ├─────────────┤ │ │
│ │ │ Lua loader │ │ → loadfile(".lua") │
│ │ ├─────────────┤ │ │
│ │ │ C loader │ │ → package.cpath + loadlib │
│ │ ├─────────────┤ │ │
│ │ │ Croot │ │ → 全局符号查找 │
│ │ └─────────────┘ │ │
│ └────────┬────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ 4. 缓存结果 │ │
│ │ package.loaded │ <── 存入缓存,下次直接返回 │
│ └────────┬────────┘ │
│ │ │
│ ▼ │
│ 返回模块 │
│ │
└─────────────────────────────────────────────────────────────────────────┘

7.2 热更新实现

1
2
3
4
5
6
-- 方式一:将模块置空
package.loaded["module_name"] = nil
require("module_name") -- 重新加载

-- 方式二:直接删除
package.loaded["module_name"] = nil

⚠️ 注意:正在使用的代码无法被回收,只有未使用的模块才能安全重载。


八、Lua 与 C 互操作

8.1 虚拟栈

Lua 使用虚拟栈与 C 互传值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
┌─────────────────────────────────────────────────────────────────────────┐
│ Lua 虚拟栈结构 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Lua 调用 C 函数: │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 栈顶 (Top) │ │
│ │ ┌─────┐ │ │
│ │ │ argN│ ← 第 N 个参数 │ │
│ │ ├─────┤ │ │
│ │ │ ... │ │ │
│ │ ├─────┤ │ │
│ │ │ arg2│ ← 第 2 个参数 │ │
│ │ ├─────┤ │ │
│ │ │ arg1│ ← 第 1 个参数 (索引 1) │ │
│ │ ├─────┤ │ │
│ │ │ ... │ ← 其他 Lua 值 │ │
│ │ └─────┘ │ │
│ │ 栈底 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ C 函数签名: │
│ typedef int (*lua_CFunction) (lua_State *L); │
│ │
└─────────────────────────────────────────────────────────────────────────┘

8.2 C 函数示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 返回多个值的 C 函数
static int foo(lua_State *L) {
int n = lua_gettop(L); // 获取参数个数
lua_Number sum = 0.0;

// 遍历参数求和
for (int i = 1; i <= n; i++) {
if (!lua_isnumber(L, i)) {
lua_pushliteral(L, "incorrect argument");
lua_error(L);
}
sum += lua_tonumber(L, i);
}

// 返回平均值和总和
lua_pushnumber(L, sum / n); // 第一个返回值
lua_pushnumber(L, sum); // 第二个返回值
return 2; // 返回值个数
}

九、LuaJIT 扩展

9.1 FFI (Foreign Function Interface)

FFI 允许 Lua 直接调用 C 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
local ffi = require("ffi")

-- 定义 C 结构和函数
ffi.cdef[[
typedef struct { int x, y; } point_t;
int printf(const char *fmt, ...);
]]

-- 使用 C 函数
ffi.C.printf("Hello %s!\n", "world")

-- 创建 C 数据结构
local p = ffi.new("point_t", 10, 20)

9.2 LuaJIT 优化建议

建议 说明
使用局部变量 缓存全局变量/函数,性能提升约 30%
避免频繁创建临时表 复用表对象
使用 FFI 高频计算用 C 实现
使用 table.concat 代替 .. 拼接字符串

十、最佳实践

10.1 性能优化

1
2
3
4
5
6
7
8
9
10
-- ❌ 不推荐:频繁使用全局变量
for i = 1, 10000 do
math.sqrt(i)
end

-- ✅ 推荐:缓存到局部变量
local sqrt = math.sqrt
for i = 1, 10000 do
sqrt(i)
end

10.2 字符串拼接

1
2
3
4
5
6
7
8
9
10
11
12
-- ❌ 不推荐:使用 .. 拼接大量字符串
local result = ""
for i = 1, 1000 do
result = result .. tostring(i) -- 每次都创建新字符串
end

-- ✅ 推荐:使用 table.concat
local parts = {}
for i = 1, 1000 do
parts[i] = tostring(i)
end
local result = table.concat(parts)

💡 学习建议

  • 使用 luac -l -l 分析字节码,理解 Lua 执行流程
  • 重点理解 lvm.c 中的虚拟机执行函数 luaV_execute
  • 深入学习 GC 算法,理解三色标记的实现
  • 实践热更新机制,理解模块加载流程

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 1487842110@qq.com