lua5.3 gc table dead key分析(8)
这里搜到一篇比较好的介绍和 table dead key 相关的文章,我就做个记录把
https://luyuhuang.tech/2020/10/23/lua-next.html
这篇文章提到的例子:
local k = 'a'..'b'
local t = {
a = 1,
[k] = 2,
}
t[k] = nil
k = nil
collectgarbage("collect")
next(t, 'a'..'b')
把 next(t, 'a'..'b') 改成 next(t, 'ab')后就不会报错了。文章中也有解释,我想自己再做个补充。
如果我们改成的是next(t, 'ab'),不会报错,是因为字符串 ab 在常量表中,而常量表其实是放在函数原型 Proto 对象上的。我们知道一个函数对应一个Proto,里面存放了函数相关的指令集,字面常量(包括数字,true,false,一些非关键字的字符串),上值描述信息等。
/*
** Function Prototypes
*/
typedef struct Proto {
CommonHeader;
lu_byte numparams; /* number of fixed parameters */
lu_byte is_vararg;
lu_byte maxstacksize; /* number of registers needed by this function */
int sizeupvalues; /* size of 'upvalues' */
int sizek; /* size of 'k' */
int sizecode;
int sizelineinfo;
int sizep; /* size of 'p' */
int sizelocvars;
int linedefined; /* debug information */
int lastlinedefined; /* debug information */
TValue *k; /* constants used by the function */
Instruction *code; /* opcodes */
struct Proto **p; /* functions defined inside the function */
int *lineinfo; /* map from opcodes to source lines (debug information) */
LocVar *locvars; /* information about local variables (debug information) */
Upvaldesc *upvalues; /* upvalue information */
struct LClosure *cache; /* last-created closure with this prototype */
TString *source; /* used for debug information */
GCObject *gclist;
} Proto;
这里的字段 k 就是对应文章说的常量表。
在例子中,Proto.k 常量表引用到的常量一共有 8 个,我们可以同过 luac -l -l lua文件 来查看对应的常量表信息。
main <tc.lua:0,0> (18 instructions at 0000000000bf8490)
0+ params, 6 slots, 1 upvalue, 2 locals, 8 constants, 0 functions
1 [1] LOADK 0 -1 ; "a"
2 [1] LOADK 1 -2 ; "b"
3 [1] CONCAT 0 0 1
4 [2] NEWTABLE 1 0 2
5 [3] SETTABLE 1 -1 -3 ; "a" 1
6 [4] SETTABLE 1 0 -4 ; - 2
7 [6] SETTABLE 1 0 -5 ; - nil
8 [7] LOADNIL 0 0
9 [8] GETTABUP 2 0 -6 ; _ENV "collectgarbage"
10 [8] LOADK 3 -7 ; "collect"
11 [8] CALL 2 2 1
12 [9] GETTABUP 2 0 -8 ; _ENV "next"
13 [9] MOVE 3 1
14 [9] LOADK 4 -1 ; "a"
15 [9] LOADK 5 -2 ; "b"
16 [9] CONCAT 4 4 5
17 [9] CALL 2 3 1
18 [9] RETURN 0 1
constants (8) for 0000000000bf8490:
1 "a"
2 "b"
3 1
4 2
5 nil
6 "collectgarbage"
7 "collect"
8 "next"
locals (2) for 0000000000bf8490:
0 k 4 19
1 t 7 19
upvalues (1) for 0000000000bf8490:
0 _ENV 1 0
8 constants 就是指有 8 个字面常量,从 每一行指令的 ; 后面看,当然,也可以从 21 行中查看具体有哪些。
字符串 ab,是通过虚拟机执行'a' .. 'b'指令时才生成的,且动态生成的 ab,只有表 t 中被引用着,当 key 来使用,当我们把 t[k] = nil; k = nil,全量 gc 的时候,字符串 ab 没有再有其他地方引用着了,会被释放掉,而在最后一个 next() 又使用新创建的字符串 ab,所以, 在if (luaV_rawequalobj(gkey(n), key) || (ttisdeadkey(gkey(n)) && iscollectable(key) && deadvalue(gkey(n)) == gcvalue(key))) 中因新创建的字符串 ab 地址,和 gkey(n) 引用的已释放的地址不相同,而没有进 if 里头。也就会导致报错了。
如果是改成 next(t, 'ab'),我们luac -l -l 输出代码对应的指令形式,看到字面常量 ; "ab",这个时候,字面常量 ab 会放到 Proto 的常量表 k 中。
main <tc.lua:0,0> (16 instructions at 00000000006c8490)
0+ params, 5 slots, 1 upvalue, 2 locals, 9 constants, 0 functions
1 [1] LOADK 0 -1 ; "a"
2 [1] LOADK 1 -2 ; "b"
3 [1] CONCAT 0 0 1
4 [2] NEWTABLE 1 0 2
5 [3] SETTABLE 1 -1 -3 ; "a" 1
6 [4] SETTABLE 1 0 -4 ; - 2
7 [6] SETTABLE 1 0 -5 ; - nil
8 [7] LOADNIL 0 0
9 [8] GETTABUP 2 0 -6 ; _ENV "collectgarbage"
10 [8] LOADK 3 -7 ; "collect"
11 [8] CALL 2 2 1
12 [9] GETTABUP 2 0 -8 ; _ENV "next"
13 [9] MOVE 3 1
14 [9] LOADK 4 -9 ; "ab"
15 [9] CALL 2 3 1
16 [9] RETURN 0 1
constants (9) for 00000000006c8490:
1 "a"
2 "b"
3 1
4 2
5 nil
6 "collectgarbage"
7 "collect"
8 "next"
9 "ab"
locals (2) for 00000000006c8490:
0 k 4 17
1 t 7 17
upvalues (1) for 00000000006c8490:
0 _ENV 1 0
这个时候,字符串 ab 会在表 t,和常量表 k 中同时引用着,如果先 mark 表 t,那么会把 key("ab")标记为死亡。之后再 mark 到 Proto 时,字符串 ab 因在常量表 k 中,而会被标记为黑色,不会被 gc 释放。所以,最后调用的 next()=> deadvalue(gkey(n)) == gcvalue(key)) ,表 t 键 key 引用着的字符串 ab 和 传参字符串 ab 使用的地址相同,成功走进 if 里头,就不会报错。
不过,我还发现一个有意思的现象。如果你执行上面的 lua 程序,最后一行是 next(t, 'a'..'b') 不变,运行结果没有报错的话,你可以试着在多执行几次看看。你会发现,有时候执行会报错,有时候不会报错。

同样的代码,执行结果会不一样,第一次报错了,第二次没有报错,这个是为什么呢,很神奇?
我在源码中加了些日志打印,发现如果在 next(t, 'a' .. 'b') 中,'a' .. 'b' 创建的新字符串 'ab', 如果申请使用的地址是和 gc 释放之前地址一样的话,deadvalue(gkey(n)) == gcvalue(key))判断地址会相等,所以能通过新的字符串 'ab' 执行 next,而不报错。

浙公网安备 33010602011771号