深入探究Lua的GC算法

對於內存的管理,是程序在應用的時候的必需知識點,《Lua設計與實現》中對Lua語言的GC原理做了一個詳細的講解,雲風的blog也對其進行了詳盡的講解Lua GC 的源碼剖析 系列

給出作者 

 在github上的lua源碼鏈接:github.com/lichuang/Lua

這兒就繼續做《Lua設計與實現》的閱讀筆記,對Lua GC原理及其過程做一個詳盡的講解,由於篇幅較大,就一分爲二,寫上下篇來講解整個過程。


一、GC的原理及其算法設計

不同的語言,對GC算法的設計不同,常見的GC算法是引用計數和Mark-Sweep算法, c#採用的是Mark-sweep && compact算法, Lua採用的是Mark-sweep算法,分開說一下:

引用計數算法:在一個對象被引用的情況下,將其引用計數加1,反之則減1,如果計數值爲0,則在GC的時候回收,這個算法有個問題就是循環引用。

Mark-sweep算法:每次GC的時候,對所有對象進行一次掃描,如果該對象不存在引用,則被回收,反之則保存。

在Lua5.0及其更早的版本中,Lua的GC是一次性不可被打斷的過程,使用的Mark算法是雙色標記算法(Two color mark),這樣系統中對象的非黑即白,要麼被引用,要麼不被引用,這會帶來一個問題:在GC的過程中如果新加入對象,這時候新加入的對象無論怎麼設置都會帶來問題,如果設置爲白色,則如果處於回收階段,則該對象會在沒有遍歷其關聯對象的情況下被回收;如果標記爲黑色,那麼沒有被掃描就被標記爲不可回收,是不正確的。


爲了降低一次性回收帶來的性能問題以及雙色算法的問題,在Lua5.1後,Lua都採用分佈回收以及三色增量標記清除算法(Tri-color incremental mark and sweep)


其基本的原理僞代碼,參考書中原文爲:

每個新創建的對象顏色設置爲白色

//初始化階段

遍歷root節點中引用的對象,從白色置爲灰色,並且放入到灰色節點列表中

//標記階段

while(灰色鏈表中還有未掃描的元素):

從中取出一個對象,將其置爲黑色

遍歷這個對象關聯的其他所有對象:

if 爲白色

標記爲灰色,加入到灰色鏈表中(insert to the head)

//回收階段

遍歷所有對象:

if 爲白色,

沒有被引用的對象,執行回收

else

重新塞入到對象鏈表中,等待下一輪GC


二、GC的數據結構

分析Lua中對於需要GC的類型數據

#define iscollectable(o) (ttype(o) >= LUA_TSTRING)

都會有一個基本的定義CommonHeader,其定義爲:

next: GCObject鏈表指針,該指針用來將所有的GC對象都鏈接在一個表中;

tt: 數據類型:nil, boolean, number, string...

marked: 標記字段,byte表示的字段顏色定義爲

這兒特定解釋一下爲什麼會有兩種白色,前面提到,5.1後的Lua採用的是三色標記算法,其實質是四色標記算法,分爲0型白色和1型白色,在GC回收的時候,會設置當前的白色爲其中一種,詳見globalstate中的currentwhite,這樣在代碼回收的時候,如果當前對象的白色不爲currentwhite,則認爲其不可回收,這樣的對象需要等到下一次的GC才能決定是否回收,具體參看後面的,會有對應的應用。對於global_state的設計爲:

具體的參數的作用,詳見註釋,就不在一一解釋了


三、GC的流程

1、數據的創建

想要了解GC的過程,首先看看數據是怎麼在創建的時候被鏈接到GC鏈表中的,主要分爲三種數據的創建

1) 一般數據的創建 luaC_link

簡單直接,直接insert to the head

2) upval的創建 luaC_linkupval

3) userdata(udata)的創建 luaS_newdata


2、開始GC

整個GC過程分爲五個階段,其定義爲

其執行GC的函數爲singlestep,來看第一步的操作:


進一步看看markroot的操作:

其實就是reset一遍相關的變量,然後標記mainthread, G表,registry表,然後切換到下一個標記階段。

參看定義:

最後都要執行reallymarkobject函數(此處需要展示一下我的豎屏截圖便利了:D):

基本的註釋都解釋了各個對象是如何的處理的,udata是不會引用其他類型的數據,所以一步到黑色,upvalue則根據是否爲close來決定是否標記到黑色,open狀態的upvalue變化較爲頻繁,需要在後面的remarkupvals中解決。


3、GC的掃描階段 GCSpropagate

只要處於這個階段,就會分2種情況執行,一個是propagatemark,一個是atomic,讓我們分別看其實現過程。

首先看處於灰色鏈表中一直都有對象的情況,在這步操作當中,是可以分步操作的,整個GC的分步操作,就是在這一步操作中,在每次掃描後,都會返回本次掃描標記的對象的大小之和,再下一個分步執行的時候再繼續執行,而一旦進入atomic函數中,就需要一次性的執行,不能再分步執行了。

來看propagatemark函數是如何實現的:

對於table,如果該表是weak表,則退回到灰色狀態,否則遍歷表的數組和散列表部分進行標記,詳見traversetable函數;

對於func,traverseclosure主要對func中的upval進行標記;

對於thread, 則將其移植到grayagain中,放在atomic中進行處理;

對於proto,對其中的字符串、upvalue、局部變量等進行遍歷標記;

注意,這兒沒有處理string\udata類型數據,這是放在其他部分進行的,不需要進行相關的標記;


4、GC 掃描階段的barrier操作

由於採用分步式增量掃描標記算法,所以會出現在分步操作過程中,新增加的對象與被掃描過的對象之間有引用關係的變化,未來確保黑色對象引用的對象中有白色對象,lua提供了兩種操作設計:

1)標記過程向前走一步 luaC_barrierf

如果新建對象是白色,而它被一個黑色對象引用了,那麼將這個新建對象顏色從白色變爲灰色;

2)標記過程向後走一步 luaC_barrierback

類似於上,此時將引用的它的黑色對象的顏色從黑色變爲灰色,使得其重新被掃描一次

(或許你看出截圖顏色變了,是的,回家了,又是新的編輯器了~)

從define可以看出,只有table需要進行luaC_barrierback,這是由於table本身設計,就是一個table可能會對應N個key或者value,這樣如果新增一個key/value,如果將其置爲灰色,然後將其加入gray鏈表中,這樣多個添加會帶來較大的性能。

採用向後,就是將該table對象退回到gray狀態,這樣添加多個,其實質都是隻改變該table一次,注意這個gray不是改爲gray鏈中,而是將該table加入到grayagain鏈中,在掃描完gray鏈後再掃描grayagain鏈即可。參考源碼即可:

對比向前比較簡單了:直接調用reallymarkoject


5、GC的atomic操作

當gray鏈表中對象都標記完成後,會執行一次atomic操作,注意這個操作是不能被打斷的,所以叫原子操作,參考源碼:

首先處理上一篇文章中提到的對open狀態的upvalues,然後處理一次gray鏈表;

然後處理整個弱表,將lua_State指針指向meta表,然後處理一次gray鏈表

然後處理grayagain鏈表,類似於上

然後處理udata,其處理函數爲luaC_separateudata:

註釋很詳細,注意放到tmudata鏈表中後,是在後續操作再集中處理一次;

處理完基本的幾個數據後,atomic會把白色類型切換到下一個GC操作的白色類型,然後修改狀態到回收階段CGSsweepstring, 這兒對sweepstrgc進行了賦初值,是爲了下面的字符串定位。


6、GC的回收階段 GCSsweepstring/GCSsweep

首先進入的回收階段是對字符串的處理

雖然是case,但是其實質是一個循環,每次取出散列表中的一個字符串鏈表,進行一次遍歷回收,sweepwholelist最終會調用到sweeplist,等一下給出源碼。

當處理完所有的字符串後,切換到GCSsweep狀態:

關鍵操作是sweeplist,參看其源碼:

代碼中也對前面說的多色標記中的兩種白色的作用做了講解,otherwhite就是本次不可回收的白色,如果處理的對象的白色就是otherwhite,是不會被回收的


7、結束階段 GCSfinalize

這是整個GC的最後階段了,來看看其操作的源碼:

首先處理,是否有前面提到的tmudata鏈表, 其操作函數爲GCTM:

注意,udata本身有GC方法,未來確保其GC方法的調用,實在這次GC中調用G方法,但是這個udata本身,是在下一次的GC中才會被回收的。udata的GC調用則是在fasttm中調用TM_GC來實現。

初看也會迷糊怎麼循環的,其實結合上面的case中的 if(g->tmudata)可以理解,爲什麼每次GCTM都會執行 g->tmudata的移動賦值操作。


最終萬事大吉,本次GC流程走完,設置到GCSpause狀態,等待下一次GC調用。


8、GC的進度控制

其實GC的調用,可以分爲兩種,一種是自動調用,一個是手動調用

自動調用函數: luaC_checkGC

一般不希望自動GC,可以採用setthreshold,將GCthreshold的值設置爲非常大,這樣不回自動觸發GC

手動調用,則設置GC的相關參數 setthreshold:

estimate是對當前內存使用量的一個預估值,gcpause是一個百分比,通過lua_gc可以設置,另一個gc進度的參數是gcstepmul,其主要影響singlestep函數的調用次數,具體原因參看源碼:

整個流程都在註釋中講解了,其中關鍵是lim的設置,然後不斷的調用singlestep, 然後處理GC狀態即可,注意setthreshold是設置的兩次GC之間的時間間隔。由於修改了threshold,對於關閉自動GC的情況,需要再次重新設置關閉自動GC一次。


9、總結

對於lua的GC的原理的探究就到這兒,熟悉一門語言的GC流程後,同理去推導理解其他語言的GC會有很大幫助,同時也可以在平時使用lua的時候,對於GC的一些操作更加知其所以然。大家共勉!


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章