雲風的 BLOG: Lua GC 的原始碼剖析 (1)
我們可以看到,Value 以 union 方式定義。如果是需要被 GC 管理的物件,就以 GCObject 指標形式儲存,否則直接存值。在程式碼的其它部分,並不直接使用 Value 型別,而是 TValue 型別。它比 Value 多了一個型別標識。用 int tt 記錄。通常的系統中,每個 TValue 長為 12 位元組。btw, 在
所有的 GCObject 都有一個相同的資料頭,叫作 CommonHeader ,在 lobject.h 裡 43 行 以巨集形式定義出來的。使用巨集是源於使用上的某種便利。C 語言不支援結構的繼承。
#define CommonHeader GCObject *next; lu_byte tt; lu_byte marked
從這裡我們可以看到:所有的 GCObject 都用一個單向連結串列串了起來。每個物件都以 tt 來識別其型別。marked 域用於標記清除的工作。
標記清除演算法是一種簡單的 GC 演算法。每次 GC 過程,先以若干根節點開始,逐個把直接以及間接和它們相關的節點都做上標記。對於 Lua ,這個過程很容易實現。因為所有 GObject 都在同一個連結串列上,當標記完成後,遍歷這個連結串列,把未被標記的節點一一刪除即可。
Lua 在實際實現時,其實不只用一條連結串列維繫所有 GCObject 。這是因為 string 型別有其特殊性。所有的 string 放在一張大的 hash 表中。它需要保證系統中不會有值相同的 string 被建立兩份。顧 string 是被單獨管理的,而不串在 GCObject 的連結串列中。
回頭來看看 lua_State
lua_State
也是一個型別為 thread 的 GCObject 。見其定義於 lstate.h 97 行。
一個完整的 lua 虛擬機器在執行時,可有多個 lua_State
,即多個 thread 。它們會共享一些資料。這些資料放在 global_State *l_G
域中。其中自然也包括所有 GCobject 的連結串列。
所有的 string 則以 stringtable 結構儲存在 stringtable strt 域。string 的值型別為 TString ,它和其它 GCObject 一樣,擁有 CommonHeader 。但需要注意,CommonHeader 中的 next 域卻和其它型別的單向連結串列意義不同。它被掛接在 stringtable 這個 hash 表中。
除 string 外的 GCObject 連結串列頭在 rootgc ( lstate.h 75 行)域中。初始化時,這個域被初始化為主執行緒。見 lstate.c 170 行,lua_newstate
函式中:
每當一個新的 GCobject 被創建出來,都會被掛接到這個連結串列上,掛接函式有兩個,在 lgc.c 687 行的
upvalue 在 C 中型別為 UpVal ,也是一個 GCObject 。但這裡被特殊處理。為什麼會這樣?因為 Lua 的 GC 可以分步掃描。別的型別被新建立時,都可以直接作為一個白色節點(新節點)掛接在整個系統中。但 upvalue 卻是對已有的物件的間接引用,不是新資料。一旦 GC 在 mark 的過程中( gc 狀態為 GCSpropagate ),則需增加屏障 luaC_barrier
。對於這個問題,會在以後詳細展開。
lua 還有另一種資料型別建立時的掛接過程也被特殊處理。那就是 userdata 。見 lstring.c 的 95 行:
這裡並沒有呼叫 luaC_link
來掛接新的 Udata 物件,而是直接使用的
把 u 掛接在 mainthread 之後。
從前面的 mainstate 建立過程可知。mainthread 一定是 GCObject 連結串列上的最後一個節點(除 Udata 外)。這是因為掛接過程都是向連結串列頭新增的。
這裡,就可以把所有 userdata 全部掛接在其它型別之後。這麼做的理由是,所有 userdata 都可能有 gc 方法(其它型別則沒有)。需要統一去呼叫這些 gc 方面,則應該有一個途徑來單獨遍歷所有的 userdata 。除此之外,userdata 和其它 GCObject 的處理方式則沒有區別,顧依舊掛接在整個 GCObject 連結串列上而不需要單獨再分出一個連結串列。
處理 userdata 的流程見 lgc.c 的 127 行
這個函式會把所有帶有 gc 方法的 userdata 挑出來,放到一個迴圈連結串列中。這個迴圈連結串列在 global_State
的 tmudata 域。需要呼叫 gc 方法的這些 userdata 在當個 gc 迴圈是不能被直接清除的。所以在 mark 環節的最後,會被重新 mark 為不可清除節點。見 lgc.c 的 545 行:
這樣,可以保證在呼叫 gc 方法環節,這些物件的記憶體都沒有被釋放。但因為這些物件被設定了 finalized 標記。(通過 markfinalized ),下一次 gc 過程不會進入 tmudata 連結串列,將會被正確清理。
具體 userdata 的清理流程,會在後面展開解釋。
今天暫時先到這裡。