lua GC實現入門


零、參考文檔

作者的說明

一、GC實現需要考慮的問題

1、着色可以處理循環引用


mark and sweep實現,通過着色的方法,一個優點就是可以避免循環引用,當A和B兩個對象可能互相指向對方時,着色可以避免無限遞歸。

2、全量集和可達集

sweep的時候是清除沒有被訪問過的節點,相當於從全量集合中刪除子集。所以就需要有一種方法,能夠找到系統中所有的變量;加上一個遍歷的起點(也就是根節點),從而mark所有可達節點。

3、增量收集中的引用關系變化

增量GC實現的問題在於增量過程中,這些變量的引用關系可能變化 

二、全量集和可達集

1、全量集

所有的可回收對象共享一個CommonHeader結構,該結構中的next可以組成一個鏈表。
/*
** Common Header for all collectable objects (in macro form, to be
** included in other objects)
*/
#define CommonHeader GCObject *next; lu_byte tt; lu_byte marked
通過global_State結構的
GCObject *allgc; /* list of all collectable objects */
字段作為一個鏈表的開始,可以將所有的可回收對象組成一個鏈表。當需要全量集合時,遍歷這個鏈表即可。

2、可達集

在三色標記中,可達集合為黑色和灰色,但是灰色只是中間狀態,它們是下一次增量mark的起始集合,在最終sweep的時候,它們應該為空。global_State的
GCObject *gray; /* list of gray objects */
字段作為gray集合的鏈表頭指針,每次從該集合開始增量mark,並且當這個集合為空時,一個mark周期結束。

3、初始gray集合

Only objects accessible from the root set are
preserved.
– root set: the registry and shared metatables.
– the registry contains the global table (_G), the main thread, and package.loaded. 

三、增量過程中引用變化問題

1、三色之間的限制

這里最為關鍵的一點是“黑色不能指向白色”,因為黑色要經過灰色這個過程。從邏輯上說,如果黑色表示它指向節點都已經被mark過,所以該黑色節點在之后的mark階段不會被考慮,而它指向白色,意味着白色節點沒有被mark過,該白色節點可能失去被mark的機會。三色之間總共有六個可能的指向:黑白、黑灰、灰白、灰灰、白白、白黑,除了黑白之外,其它都是合法指向。
● Objects in the root set are gray or black.
● A black object cannot point to a white object.
● Gray objects define the boundary between the black objects and the white objects.
● Collection advances by traversing gray objects,turning them black.
– which may create new gray objects
● Collection ends when there are no more gray objects.

2、增量過程中如果黑色引用白色

此時有兩種方法:一種是把黑色變成灰色,另一種是把白色變成灰色。無論如何,新變成灰色的節點會在下次mark時被重新檢測。
It can either move forward the white object to gray or move backward the black object to gray

3、賦值時對三色限制的保證

tsecer@harry: cat glob.assign.lua
a = b

tsecer@harry: ../src/luac -l -l glob.assign.lua

main <glob.assign.lua:0,0> (3 instructions at 0x7faa60)
0+ params, 2 slots, 1 upvalue, 0 locals, 2 constants, 0 functions
1 [1] GETTABUP 0 0 -2 ; _ENV "b"
2 [1] SETTABUP 0 -1 0 ; _ENV "a"
3 [1] RETURN 0 1
constants (2) for 0x7faa60:
1 "a"
2 "b"
locals (0) for 0x7faa60:
upvalues (1) for 0x7faa60:
0 _ENV 1 0
tsecer@harry:
虛擬機指令OP_SETTABUP對應的邏輯會執行到luaV_fastset,其中會執行luaC_barrierback,如果a為黑色而b為白色,則將a重新變為黑色。
#define luaV_fastset(L,t,k,slot,f,v) \
(!ttistable(t) \
? (slot = NULL, 0) \
: (slot = f(hvalue(t), k), \
ttisnil(slot) ? 0 \
: (luaC_barrierback(L, hvalue(t), v), \
setobj2t(L, cast(TValue *,slot), v), \
1))) 

4、棧變量修改的保持

從lua虛擬機代碼來看,對於局部變量的修改並沒有嘗試進行三色保持。那么考慮下面代碼
local a={}
假設在這條指令之后進行GC的mark和sweep,那么新創建的table會被認為沒有被引用,此時就可能會被回,這名顯示錯誤的。
所以在最后回收的時候,gc會對棧中的所有變量全部進行一次mark,在棧變量不多並且操作頻發的情況下,最后統一mark比每次賦值都mark的代碼更低。
static l_mem atomic (lua_State *L) {
……
markobject(g, L); /* mark running thread */
……
}
static lu_mem traversethread (global_State *g, lua_State *th) {
……
for (; o < th->top; o++) /* mark live elements in the stack */
markvalue(g, o);
……
}

四、代碼說明

1、數據結構

代碼中使用的都是TValue,
/*
** Union of all Lua values
*/
typedef union Value {
GCObject *gc; /* collectable objects */
void *p; /* light userdata */
int b; /* booleans */
lua_CFunction f; /* light C functions */
lua_Integer i; /* integer numbers */
lua_Number n; /* float numbers */
} Value;


#define TValuefields Value value_; int tt_


typedef struct lua_TValue {
TValuefields;
} TValue;

而lua管理的是GCUnion

/*
** Union of all collectable objects (only for conversions)
*/
union GCUnion {
GCObject gc; /* common header */
struct TString ts;
struct Udata u;
union Closure cl;
struct Table h;
struct Proto p;
struct lua_State th; /* thread */
}; 

2、假設局部變量創建table

對於一個函數的堆棧,堆棧中的基本單元並不是我們在C語言中使用的CPU word,而是虛擬機自定義的TValue結構,每次堆棧的訪問、增減都是以該單位為最小粒度操作。
tsecer@harry: cat localtable.lua
local a = {}
tsecer@harry: ../src/luac -l -l localtable.lua

main <localtable.lua:0,0> (2 instructions at 0x7eda50)
0+ params, 2 slots, 1 upvalue, 1 local, 0 constants, 0 functions
1 [1] NEWTABLE 0 0 0
2 [1] RETURN 0 1
constants (0) for 0x7eda50:
locals (1) for 0x7eda50:
0 a 2 3
upvalues (1) for 0x7eda50:
0 _ENV 1 0
tsecer@harry:
可以看到,虛擬機指令OP_NEWTABLE對應的是創建一個table,然后讓棧中變量ra(Value類型)的gc指針指向新創建的table,所以相當於棧中有一個指針指向新創建的Table對象
vmcase(OP_NEWTABLE) {
int b = GETARG_B(i);
int c = GETARG_C(i);
Table *t = luaH_new(L);
sethvalue(L, ra, t);
if (b != 0 || c != 0)
luaH_resize(L, t, luaO_fb2int(b), luaO_fb2int(c));
checkGC(L, ra + 1);
vmbreak;

3、遍歷函數中棧常量

由於棧中變量的TValue類型的gc指向Table,所以可以訪問到Table對象,從而可以避免該對象被回收。
static int traverseproto (global_State *g, Proto *f) {
……
for (i = 0; i < f->sizek; i++) /* mark literals */
markvalue(g, &f->k[i]);
……
}

4、新創建節點添加到gc鏈表上

所有lua創建的對象在創建時都通過luaC_newobj完成,可以看到,新創建的對象掛在了global_State中allgc鏈表的開始。
GCObject *luaC_newobj (lua_State *L, int tt, size_t sz) {
global_State *g = G(L);
GCObject *o = cast(GCObject *, luaM_newobject(L, novariant(tt), sz));
o->marked = luaC_white(g);
o->tt = tt;
o->next = g->allgc;
g->allgc = o;
return o;
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM