標籤:
普及下常識: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 的函數中執行。見 lgc.c 的 565 行:
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 ,Mac下主動殺死ssh曆程並重連。
在 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 了一下,最後,世界上是不是只剩下這個結構。
lua_gc 源碼學習二