雲風的 BLOG: Lua GC 的原始碼剖析 (3)
從程式碼可見,對內部狀態的訪問,都是直接訪問 global state 表的。GC 控制則是呼叫內部 api 。lua 中對外的 api 和內部模組互動的 api 都是分開的。這樣層次分明。內部子模組一般名為 luaX_xxx
X 為子模組代號。對於收集器相關的 api 一律以 luaC_xxx
命名。這些 api 定義在 lgc.h 中。
此間提到的 api 有兩個:
用於分步 GC 已經完整 GC 。
另一個重要的 api 是:
它以巨集形式定義出來,用於自動的 GC 。如果我們審查 lapi.c ldo.c lvm.c ,會發現大部分會導致記憶體增長的 api 中,都呼叫了它。保證 gc 可以隨記憶體使用增加而自動進行。
這裡插幾句。
使用自動 gc 會有一個問題。它很可能使系統的峰值記憶體佔用遠超過實際需求量。原因就在於,收集行為往往發生在呼叫棧很深的地方。當你的應用程式呈現出某種週期性(大多數包驅動的服務都是這樣)。在一個服務週期內,往往會引用眾多臨時物件,這個時候做 mark 工作,會導致許多臨時物件也被 mark 住。
一個經驗方法是,呼叫 LUA_GCSTOP
停止自動 GC。在週期間定期呼叫 gcstep 且使用較大的 data 值,在有限個週期做完一整趟 gc 。
另,condhardstacktests 是一個巨集,通常是不開啟的。
先來看 luaC_fullgc
。它用來執行完整的一次 gc 動作。fullgc 並不是僅僅把當前的流程走完。因為之前的 gc 行為可能執行了一半,可能有一些半路加進來的需要回收的物件。所以在走完一趟流程後,fullgc 將阻塞著再完整跑一遍 gc 。整個流程有一些優化的餘地。即,前半程的 gc 流程其實不必嚴格執行,它並不需要真的去清除什麼。只需要把狀態恢復。這個工作是如何做到的呢?見 lgc.c 的 637 行:
比較耗時的 mark 步驟被簡單跳過了(如果它還沒進行完的話)。和正常的 mark 流程不同,正常的 mark 流程最後,會將白色標記反轉。見 lgc.c 548 行,atomic 函式:
在 fullgc 的前半程中,直接跳過了 GCSpropagate ,重置了內部狀態,但沒有翻轉白色標記。這會導致後面的 sweep 流程不會真的釋放那些白色物件。sweep 工作實際做的只是把所有物件又重新設定回白色而已。
接下來就是一個完整不被打斷的 gc 過程了。
從根開始 mark ,直到整個 gc 流程執行完畢。最後,重新設定了 GCthreshold 。注:呼叫 fullgc 會重置 GCthreshold ,所以如果你曾經呼叫 LUA_GCSTOP
暫停自動 GC 的話(也是通過修改 GCthreshold 實現) ,記得再呼叫一次。
stepgc 要相對複雜一些。在 lua 手冊的 2.10 解釋了 garbage-collector pause 和 step multiplier 的意義,卻沒有給出精確定義。lua_gc
的說明裡,也只說“LUA_GCSTEP
: 發起一步增量垃圾收集。 步數由 data 控制(越大的值意味著越多步), 而其具體含義(具體數字表示了多少)並未標準化。 如果你想控制這個步數,必須實驗性的測試 data 的值。 如果這一步結束了一個垃圾收集週期,返回返回 1 。 並沒有給出準確的含義。實踐中,我們也都是以經驗取值。
回到原始碼,我們就能搞清楚它們到底是什麼了。
這裡只是設定 gcpause gcstepmul 。gcpause 實際只在 lgc.c 59 行的 setthreshold 巨集中用到
看見,GCSETPAUSE 其實是通過調整 GCthreshold 來實現的。當 GCthreshold 足夠大時,luaC_step
不會被 luaC_checkGC
自動觸發。事實上,GCSTOP 正是通過設定一個很大的 GCthreshold 值來實現的。
gcpause 值的含義很文件一致,用來表示和實際記憶體使用量 estimate 的比值(放大 100 倍)。一旦記憶體使用量超過這個閥值,就會出發 GC 的工作。
要理解 gcstepmul ,就要從 lua_gc
的 LUA_GCSTEP
的實現看起。
step 的長度 data 被放大了 1024 倍。在 lgc.c 的 26 行,也可以看到
我們姑且可以認為 data 的單位是 KBytes ,和 lua 總共佔用的記憶體 totalbytes 有些關係。
ps. 這裡 totalbytes 是嚴格通過 Alloc 管理的記憶體量。而前面提到的 estimate 則不同,它是一個估算量,比 totalbytes 要小。這是因為,前面也提到過,userdata 的回收比較特殊。被檢測出已經訪問不到的 userdata 佔用的記憶體並不會馬上釋放(保證 gc 元方法的安全呼叫),但 estimate 會拋去這部分,不算在實際記憶體使用量內。
見 lgc.c 544 行
以及 lgc.c 553 行
從程式碼邏輯,我們暫時可以把 data 理解為,需要處理的位元組數量(以 K bytes 為單位)。如果需要處理的資料量超過了 totalbytes ,自然就可以把 GCthreshold 設定為 0 了。
實際上不能完全這麼理解。因為 GC 過程並不是一點點回收記憶體,同時可用記憶體越來越多。GC 分標記(mark) 清除(sweep) 呼叫 userdata 元方法等幾個階段。只有中間的清除階段是真正釋放記憶體的。所以可用記憶體的增加( totalbytes 減少)過程,時間上並不是線性的。通常標記的開銷更大。為了讓 gcstep 的每個步驟消耗的時間更平滑,就得有手段動態調整 GCthreshold 值。它和 totalbytes 最終影響了每個 step 的時間。
下面的關注焦點轉向 luaC_step
,見 lgc.c 的 611 行:
從程式碼我們可以看到,GC 的核心其實在於 singlestep 函式。luaC_step
每次呼叫多少次 singlestep 跟 gcstepmul 的值有關。
如果是自動進行的 GC ,當 totalbytes 大於等於 GCthreshold 時,就會觸發 luaC_step
。每次 luaC_step
,GCthreshold 都會被調高 1K (GCSTEPSIZE) 直到 GCthreshold 追上 totalbytes 。這個追趕過程通常發生在 mark 流程。因為這個流程中,totalbytes 是隻增不減的。
如果是手控 GC ,每次 gcstep 呼叫執行多少次 luaC_step
則跟 data 值有關。大體上是 1 就表示一次(在 mark 過程中就是這樣)到了 sweep 流程就不一定了。這和 singlestep 呼叫次數,即 gcstepmul 的值有關。它影響了 totalbytes 的減小速度。
所以,一兩句話很難嚴格定義出這些控制 GC 步進量的引數的含義,只能慢慢閱讀程式碼,看看實現了。
在 lua 手冊的 2.10 這樣描述“step multiplier 控制了收集器相對記憶體分配的速度。 更大的數字將導致收集器工作的更主動的同時,也使每步收集的尺寸增加。 小於 1 的值會使收集器工作的非常慢,可能導致收集器永遠都結束不了當前週期。 預設值為 2 ,這意味著收集器將以記憶體分配器的兩倍速執行。”
從程式碼看,這絕非嚴格定義。至少從今天已經分析的程式碼中還看不出這一點。
gcstepmul 的值和記憶體增漲速度如何產生聯絡?明天再寫 :)