Lua原始碼剖析(五)
這次主要來分析lua的gc。
首先lua中的資料型別包括下面9種,ni, Boolean, number, string, table,user data, thread , functions 以及 lightusedata.其中 string, table,thread , function 是會被垃圾回收管理的,其他的都是值存在。
因此我們來看對應的GC資料結構.
1 |
|
我們可以看到在lua中字串,userdata, thread, table ,string, thread(以及Upval, proto) 都會被垃圾回收管理。這裡比較關鍵的就是GCheader這個結構體,我們可以看到這個結構體其實就是一個連結串列,也就是說所有的gc物件都會被鏈到一個連結串列中,其中tt表示當前物件的型別,在lua中包括下面這些型別:
1 |
|
而marked表示當前物件的狀態(涉及到gc演算法,後續會詳細分析),狀態位包括下面這些:
1 |
|
然後我們來看lua_state這個資料結構,這個結構也就是一個lua意義上的thread。
1 |
|
每一個lua虛擬機器可能會包含很多個lua_state結構。而所有的lua_State所共享的資料(比如string,比如gc資料等), 都將會放到global_State中。
這裡要注意所有的GC資料中,string和其他的是不同的,他和其他的GC物件分開管理,我們先來看非string類的物件如何管理。先來看global_State這個結構:
1 |
|
著重來看GC相關的幾個資料結構。當前虛擬機器的所有的GC物件都會儲存在一個連結串列中,這個連結串列的根就是global_State的rootgc中。我們來看roottgc的初始化,程式碼在lua_newstate中:
1 |
|
然後每一個被建立的gc物件都會被掛載到這個連結串列中。掛載函式就是luaC_link。這個函式主要用來將需要gc的物件link到全域性的global_state中。
1 |
|
通過上面的函式,我們可以看到每次新的gc物件插入的時候,總是放到連結串列的最前端(rootgc 為當前物件).
不過這裡的upvalue和userdata都是使用另外的方法掛載到全域性的漣中的,先來看upvalue(upvalue是什麼,我這裡就不介紹了,前面的lua原始碼分析有介紹過的).
1 |
|
可以看到和luaC_link不同的是,進行了gc演算法的一些操作,這裡我們先擱置,後續介紹gc演算法的時候,會再來看這裡。
然後就是userdata的特殊處理:
1 |
|
可以看到它是和上面的兩種方式完全不同,這是因為userdata一般來說都有自己的gc方法,因此最好能夠放在一起處理,因此這裡會將udata放到最末尾.
在lua中的gc演算法,是mark-sweep演算法,這個演算法簡單來說就分為兩步,第一步是遍歷所有的GCObject的物件,然後做標記 。 第二步是遍歷所有可回收物件,然後清除沒有做過標記的物件。 在lua中通過兩個引數來控制gc的頻率和週期,分別是garbage-collector pause 和garbage-collector step multiplier, 這兩個值都是使用百分比(100表示 100%). 其中garbage-collector pause控制回收器等待多久開始一次新的垃圾回收,比如預設值是200,那麼就說明只有當等待記憶體使用為上一次gc時的2倍才會進行下一次gc。而garbage-collector step multiplier控制垃圾回收的相對速度(相對於分配的速度), 預設也是200,說明垃圾回收的速度為記憶體分配的兩倍,這兩個值都可以通過lua_gc來修改(LUA_GCSETPAUSE與LUA_GCSETSTEPMUL).
而在lua 5.1中實現的是 Tri-color marking 演算法,演算法描述見wiki(http://en.wikipedia.org/wiki/Garbage_collection_%28computer_science%29#Tri-color_marking) , 這個演算法將每一個物件分為三種顏色,分別是白色(初始狀態), 灰色(和root有連線,可是它所連線的物件還沒有被掃描,因此這個狀態不能被gc),以及黑色(可以被釋放的物件集合), 所有的物件都會經歷從白色到灰色再到黑色的過程.
演算法的具體步驟如下:
- Create initial white, grey, and black sets; these sets will be used to maintain progress during the cycle.
2.
- Initially the white set or condemned set is the set of objects that are candidates for having their memory recycled.
- The black set is the set of objects that can cheaply be proven to have no references to objects in the white set, but are also not chosen to be candidates for recycling; in many implementations, the black set starts off empty.
- The grey set is all the objects that are reachable from root references but the objects referenced by grey objects haven’t been scanned yet. Grey objects are known to be reachable from the root, so cannot be garbage collected: grey objects will eventually end up in the black set. The grey state means we still need to check any objects that the object references.
- The grey set is initialised to objects which are referenced directly at root level; typically all other objects are initially placed in the white set.
- Objects can move from white to grey to black, never in the other direction.
- Pick an object from the grey set. Blacken this object (move it to the black set), by greying all the white objects it references directly. This confirms that this object cannot be garbage collected, and also that any objects it references cannot be garbage collected.
- Repeat the previous step until the grey set is empty.
- When there are no more objects in the grey set, then all the objects remaining in the white set have been demonstrated not to be reachable, and the storage occupied by them can be reclaimed.
然後我們來看具體實現,我們就從最常見的table來分析。首先來看lua gc的啟動。這裡核心方法就是luaC_checkGC這個巨集, 因為gc的啟動一般來說就是通過這個巨集開始的。
1 |
|
這裡我們可以看到它會比較當前分配的位元組數與GCthreshold進行比較,如果大於這個值才會進行step。而GCthreshold就是一個閥值..
然後來看luaC_step這個函式:
1 |
|
這裡主要就是singlestep函式:
1 |
|
可以看到這裡是一個狀態機. 這裡lua的gc執行順序也是按照上面的狀態的從大到小開始。
其中GCSpause是初始化狀態。在這個狀態主要就是標記主執行緒物件(也就是從白色染成灰色)。我們就從這個狀態開始,我們可以看到這個狀態的處理很簡單,就是呼叫markroot函式來標記物件:
1 |
|
上面的markXXX的幾個函式最終都會呼叫reallymarkobject函式,因此我們就從這個函式開始:
1 |
|
這個函式我們可以看到首先它會將白色染成灰色,然後會根據物件的型別來做不同的操作,這裡特殊操作就3種類型,分別是string(不通過gc管理),userdata, 以及upval,其他的型別都是將灰色的物件連線到global state的連結串列中.
當GCSpause狀態之後,會進入GCSpropagate狀態(上面的markroot函式最後一個語句).這個狀態也是一個標記過程,並且這個狀態會被進入多次,也就是分佈迭代。如果gray物件一直存在的話,會反覆呼叫propagatemark函式,等所有的gray物件都被標記了,那麼就將會進入atomic函式處理。這個函式,顧名思義,也就是原子操作,最終在這個狀態之後,gc進入清理字串的階段.
1 |
|
可以看到在propagate狀態,也會根據物件型別來進行標記,這裡我們可以看到它首先會把當前的物件節點標記為黑色,然後再進行後續處理,主要來看table型別。它會將物件掛載到gray連結串列,然後開始遍歷標記table,這裡注意如果table是weak的,那麼則會將black節點重新染成gray的, 最後返回這次標記的記憶體大小,而核心方法就在traversetable.
1 |
|
traversetable方法首先會標記元表,然後主要是對weak table進行特殊處理,由於weak table是弱引用,因此這裡將會在gc之後單獨處理弱表(g->weak).如果不是weak表,那麼將會對這個物件進行mark。最後返回值是表示當前的表是否處於weak模式.
如果traversetable返回1,則表示表是weak模式,此時重新將物件的顏色染回灰色,因為weak table,後續會統一處理,也就是脫離lua的gc.
最後如果已經將所有的gray物件染色完畢(weak 表的話,gray物件會被移到g->weak),那麼GCSpropagate狀態最後將會進入atomic這個函式。這個函式之所以叫atomic,是因為在這個狀態下lua的標記是不會被打斷的,它最終會做一次清理,也就是對於在標記期間有改變的物件再次進行mark。這裡就涉及到一個barrier的概念,之所以要有barrier,是因為由於lua的gc是分步的,因此在進入最終的清理狀態之前,有可能被標記的物件的顏色已經改變(比如本來是白色,可是我們第一次掃描之後,它又被使用了,此時自然就變成灰色了,或者是已經被染色為黑色了,可是物件後續又沒有對應的引用了),在這些情況下,都會將顏色染回灰色,要麼是barrier fwd(white->gray),要麼是 barrier back(black->gray).後續我們會詳細介紹barrier,這裡先跳過.
1 |
|
這裡還有一個要注意的,那就是處理useadata,由於userdata是會有自己的gc方法,因此userdata最終會單獨處理(前面我們看到連結到gcroot的時候,也是放在最末尾).來看luaC_separateudata:
1 |
|
通過上面我們可以看到這裡並沒有真正的釋放userdata,只是將有gc方法的userdata連結到g->tmudata上。我們要謹記,在lua gc中,只有清理階段才會真正釋放記憶體。
然後我們來看GCSsweepstring狀態,也就是清理string。
1 |