Lua 源码阅读完全指南 - 从虚拟机到垃圾回收
本文系统介绍 Lua 源码的核心概念,包括虚拟机原理、编译流程、垃圾回收机制、热更新原理等高级话题。
一、学习路线
1.1 推荐书籍
| 阶段 |
书籍 |
说明 |
| 初级 |
《Programming in Lua》(Lua 编程) |
编写 Lua 代码的基本操作 |
| 中级 |
《Lua 程序设计第四版》 |
深入理解 Lua 特性 |
| 高级 |
《Lua 设计与实现》 |
Lua 实现的算法、虚拟机、编译器、GC |
1.2 学习资源
二、源码文件结构
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
| 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])
|
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
| 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")
ffi.cdef[[ typedef struct { int x, y; } point_t; int printf(const char *fmt, ...); ]]
ffi.C.printf("Hello %s!\n", "world")
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
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