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,而不报错。

posted @ 2023-08-11 19:02  墨色山水  阅读(28)  评论(0)    收藏  举报