lua_gc 原始碼學習二
普及下常識:GC 是 garbage collector 資源回收器;
初期的 Lua GC 採取的是 stop the world 的實現。一旦產生 gc 就需要期待全部 gc 流程走完。lua 自己是個很精簡的體系,但不代表處理的資料量也必然很小。
從 Lua 5.1 入手下手,GC 的實現改成分步的。固然照舊是 stop the world ,可是,每個步驟均可以分階段執行。這樣,屢次擱淺的時間較小。隨之,這部門的程式碼也相對於紛亂了。分步執行最關鍵的問題是需要處理在 GC 的步驟之間,如果資料關聯的狀態發生了變化,如何保證 GC 的準確性。GC 的分步執行相對一次執行完,總的時候開銷的不同並非零代價的。只是在實現上,要儘可能讓額外增長的價錢較小。
先來看 GC 流程的分別。
lua 的 GC 分為五個大的階段。GC 處於哪一個階段(程式碼中被稱為狀態),根據的是global_State中的 gcstate 域。狀態以巨集式樣界說在 lgc.h 的 14 行。
/* ** Possible states of the Garbage Collector */ #define GCSpause 0 #define GCSpropagate 1 #define GCSsweepstring 2 #define GCSsweep 3 #define GCSfinalize 4
狀態的值的鉅細也表示著它們的履行順序。需要注重的是,GC 的執行歷程並不是每程式都堵塞在一個狀況上。
GCSpause 階段是每一個 GC 流程的啟始步調。僅僅標識表記標幟體系的根節點。見 lgc.c 的 561 行。
switch (g->gcstate)
{ case GCSpause: markroot(L); /* start a new collection */ return 0;
markroot 這個函式所做之事,就是標記主執行緒物件,標記主執行緒的區域性表、登錄檔,以及為區域性範例註冊的元表。標記的詳細過程我們背面再講。
GCSpause 階段執行完,立即就將狀態切換到了 GCSpropagate 。這是一個標記流程。這個流程會分步結束。當檢測到另有物件待標記時,迭代標記(重複呼叫 propagatemark);終究,會有一個標記功程不可被打斷,這些操縱放在一個叫作 atomic 的函式中執行
case GCSpropagate:
{ if (g->gray) return propagatemark(g); else /* no more `gray' objects */ atomic(L); /* finish mark phase */ return 0; }
這裡大概需要順帶提一下的是 gray 域。望文生義,它指 GCObject 中的灰色節點鏈。作為灰色,即處於白色和黑色之間的狀態。對於節點的顏色,頓時就會闡明。
接下來就是清除流程了。
前面我們提到過,string 在 Lua 中是單獨經管的,以是也需要單獨清除。GCSsweepstring 階段乾的就是這個事。string table 以 hash 表形式治理所有的 string 。GCSsweepstring 中,每個步驟(step) 清理 hash 表的一列。程式碼見 lgc.c 的 573 行
case GCSsweepstring: lu_mem old = g->totalbytes; sweepwholelist(L, &g->strt.hash[g->sweepstrgc++]); if (g->sweepstrgc >= g->strt.size) /* nothing more to sweep? */ g->gcstate = GCSsweep; /* end sweep-string phase */ lua_assert(old >= g->totalbytes); g->estimate -= old - g->totalbytes; return GCSWEEPCOST;
這裡可以看到 estimate 和 totalbytes 兩個域,從名字上可以知道,它們別離暗示了 lua vm 佔用的記憶體位元組數以及實際分派的位元組數。
ps. 如果你本身實現過記憶體管理器,當知道記憶體治理自己是有分外的記憶體開銷的。如果有需要確切控制記憶體數目,我偏向於連續記憶體管理器統計精確的記憶體使用環境。比如你向記憶體管理器索取 8 位元組記憶體,實際的記憶體開銷極可能是 12 位元組,乃至更多。如果想做這方面的點竄,讓 lua 的 gc 能更真正的反映記憶體實際使用情形,保舉修改 lmem.c 的 76 行,luaM_realloc_函式。所有的 lua 中的記憶體使用改變城市經由過程這個函式。
從下面這段程式碼中,我們還見到了 GCSWEEPCOST 這個祕密數字。這個數字用於節制 GC 的進度。這超越了本日的話題。留待今後闡發。
接下來就是對所有未標記的別的 GCObject 做清理勞動了。即 GCSsweep 階段。它和上面的 GCSsweepstring 近似。
最終是 GCSfinalize 階段。假如在前方的階段發現了需要呼叫 gc 元方法的 userdata 物件,將在這個階段逐一呼叫。做這件工作的函式是 GCTM 。
後面已談到過,全部具有 gc 元要領的 userdata 物件和其聯絡關係的資料,現實上都不會在以前的斷根階段被肅清。(因為零丁做過標誌)所有的元方法挪用都是平安的。而它們的現實排除,則需比及下一次 GC 流程了。或是在lua_close被調歷時消滅。
ps.lua_close其實不做完全的 gc 事情,只是簡單的處置懲罰所有 userdata 的 gc 元方法,以及開釋所有效到的記憶體。它是相對於便宜的。
接下來我們看看 GC 標記流程觸及的一些觀點。
簡略的說,lua 以為每一個 GCObject (需要被 GC 蒐集器辦理的工具)都有一個色彩。一起頭,一切節點都是白色的。新建立進去的節點也被默許配置為紅色。
在標記階段,可見的節點,逐個被設定為玄色。有些節點比較龐大,它會聯絡關係此外節點。再不處置完所相關聯節點前,lua 以為它是灰色的。
節點的顏色被貯存在 GCObject 的 CommonHeader 裡,放在 marked 域中。為了節儉記憶體,因此用位寄存。marked 是一個單位元組量。統共可以儲存 8 個標記。而 lua 5.1.4 用到了 7 個標記位。在 lgc.h 的 41 行,有其詮釋:
/* ** Layout for bit use in `marked' field: ** bit 0 - object is white (type 0) ** bit 1 - object is white (type 1) ** bit 2 - object is black ** bit 3 - for userdata: has been finalized ** bit 3 - for tables: has weak keys ** bit 4 - for tables: has weak values ** bit 5 - object is fixed (should not be collected) ** bit 6 - object is "super" fixed (only the main thread) */ #define WHITE0BIT 0 #define WHITE1BIT 1 #define BLACKBIT 2 #define FINALIZEDBIT 3 #define KEYWEAKBIT 3 #define VALUEWEAKBIT 4 #define FIXEDBIT 5 #define SFIXEDBIT 6 #define WHITEBITS bit2mask(WHITE0BIT, WHITE1BIT)
lua 定義了一組巨集來操縱這些標記位,程式碼就再也不列出。只要要翻開 lgc.h 就可以很輕快的明白這些巨集的函式。
白色和黑色是劃分標記的。當一個物件非白非黑時,就認為它是灰色的。
為何有兩個白色符號位?這是 lua 採取的一個小技能。在 GC 的標誌流程完成,但清算流程還沒有作完前。一旦物件間的干係產生轉變,好比新增添了一個物件。這些物件的生命期是不能預知的。最安定的方法是把它們標記為不可消除的。由於清理程序結束,需要把所有物件設定回白色,便利下次清算。lua 其實是單遍掃描,處置完一個節點就重置一個節點的顏色的。簡樸的把新創建出來的物件設定為黑,有可能致使它在 GC 流程竣事後,再也沒機遇變回白色了。
簡略的方法便是設定從第三種狀態。也便是第 2 種白色。
在 Lua 中,兩個白色狀態是一個乒乓開關,當前需要刪除 0 型白色節點時, 1 型白色節點就是被保護起來的;反之也同樣。
以後的白色是 0 型仍是 1 型,見global_State的 currentwhite 域。otherwhite() 用於乒乓切換。獲適當前白色狀態,利用定義在 lgcc.h 77 行的巨集:
#define luaC_white(g) cast(lu_byte, (g)->currentwhite & WHITEBITS)
FINALIZEDBIT 用於標記 userdata 。當 userdata 確定不被援用,則設定上這個標記。它分歧於顏色標記。因為 userdata 由於 gc 元方法的存在,釋放所佔記憶體是需要放到 gc 元方法呼叫以後的。這個標記可以包管元方法不會被頻頻呼叫。
KEYWEAKBIT 和 VALUEWEAKBIT 用於標記 table 的 weak 屬性,無需多言。
FIXEDBIT 可以保證一個 GCObject 不會在 GC 流程中被清除。為什麼要有這類狀態?關鍵在於 lua 本身會用到一個字串,它們有可能不被任何處所引用,但在屢次用到這個字串時。那麼,這些字串就會被珍愛起來,設定上 FIXEDBIT ,。
在 lstring.h 的 24 行界說有:
#define luaS_fix(s) l_setbit((s)->tsv.marked, FIXEDBIT)
能夠把一個字串配置為被庇護的。
典範的運用場所見 llex.c 的 64 行:
void luaX_init (lua_State *L) { int i; for (i=0; i<NUM_RESERVED; i++)
TString *ts="luaS_new(L," luaX_tokens[i]);
luaS_fix(ts);
reserved words are never collected * lua_assert(strlen(luaX_tokens[i])+1 <="TOKEN_LEN);" ts->tsv.reserved = cast_byte(i+1); /* reserved word */ }
以及 ltm.c 的 30 行:
void luaT_init (lua_State *L) { static const char *const luaT_eventname[] = /* ORDER TM */ "__index", "__newindex", "__gc", "__mode", "__eq", "__add", "__sub", "__mul", "__div", "__mod", "__pow", "__unm", "__len", "__lt", "__le", "__concat", "__call"; int i; for (i=0; itmname[i] = luaS_new(L, luaT_eventname[i]); luaS_fix(G(L)->tmname[i]); /* never collect these names */ }
以元方式為例,若是咱們操縱 lua 尺度 api 來摹擬 metatable 的行動,就不能寫的和原生的 meta 機制高效。由於,當我們取到一個 table 的 key ,想知道它是否是__index時,要末我們需要挪用 strcmp 做比力;要麼利用lua_pushlstring先將需要比較的 string 壓入lua_State,而後再比較。
咱們曉得 lua 中值分歧的 string 同享了一個 string 工具,即 TString 地點是一概的。對照兩個 lua string 的價格是很小(只需求比力一個指標),比 C 函式 strcmp 高效。但lua_pushlstring卻有額外開消。它必要去計較 hash 值,盤問 hash 表 (string table) 。
lua 的 GC 演算法並不做記憶體清算,它不會在記憶體中遷徙資料。實際上,如果你能確定一個 string 不會被清除,那末它的記憶體地點也是穩定的,這樣就帶來的優化空間。ltm.c 中就是如許做的。
見 lstate.c 的 93 行:
TString *tmname[TM_N]; /* array with tag-method names */
global_State中 tmname 域就乾脆以 TString 指標的體例記實了所有元方法的名字。換作準則的 lua api 來做的話,凡是我們需要把這些 string 放到登錄檔,或情況表中,才幹擔保其不被 gc 清除,且可以在比較時拿到。lua 本身的完成則哄騙 FIXEDBIT 做了一步優化。
最後,我們來看看 SFIXEDBIT 。實在它的用處只要一個,就是標記主 mainthread 。也就是一概的出發點。我們呼叫lua_newstate返回的阿誰佈局。
為甚麼需要把這個佈局特別看待?因為即便到lua_close的那一刻,這個構造也是不克隨便清除的。我們來看看天下末日時,法式都履行了什麼?見 lstate.c 的 105 行。
static void close_state (lua_State *L) global_State *g = G(L); luaF_close(L, L->stack); /* close all upvalues for this thread */ luaC_freeall(L); /* collect all objects */ lua_assert(g->rootgc == obj2gco(L)); lua_assert(g->strt.nuse == 0); luaM_freearray(L, G(L)->strt.hash, G(L)->strt.size, TString *); luaZ_freebuffer(L, &g->buff); freestack(L, L); lua_assert(g->totalbytes == sizeof(LG)); (*g->frealloc)(g->ud, fromstate(L), state_size(LG), 0);
這是lua_close的最後一個步驟。luaC_freeall將釋放所有的 GCObject ,但不包羅有 SFIXEDBIT 的 mainthread 物件 。見 lgc.c 484 行
void luaC_freeall (lua_State *L) bitmask(SFIXEDBIT); /* mask to collect all elements */ sweepwholelist(L, &g->rootgc); for (i = 0; i < g->strt.size; i++) /* free all string lists */ sweepwholelist(L, &g->strt.hash[i]);
這裡 FIXEDBIT 是被疏忽的,而在此以前,FIXEDBIT 被護衛著。見 lstate.c 的 153 行(lua_newstate函式):
g->currentwhite = bit2mask(WHITE0BIT, FIXEDBIT);
這麼做很輕易瞭解,lua 天下的發源,統統根資料都放在這個物件中,要是被提早清理,背面的程式碼就會出題目。真實釋放這個物件不是在 GC 中,而是末了那句:
lua_assert(g->totalbytes == sizeof(LG)); (*g->frealloc)(g->ud, fromstate(L), state_size(LG), 0);
順帶還 assert 了一下,最後,世界上是不是隻剩下這個結構。