前提:
本文參考和借鑒相關博客,相關版權歸其所有,我只是做一個歸納整理,所以本文沒有任何版權
參考文獻和書籍:
CLR和.Net對象生存周期: https://www.cnblogs.com/Wddpct/p/5547765.html
c#Finalize 和Dispose的區別: https://www.cnblogs.com/Jessy/articles/2552839.html
《Lua設計與實現》——codedump 著
一、概要
本次對常見使用的c#和lua語言的gc操作原理和過程進行一次歸類整理,加深對語言的理解,也為后續寫出更優性能更好的代碼做相關知識儲備。
二、c#的垃圾回收
2.1 基本概念
1. CLR
CLR: Common Language Runtime, 公共語言運行時,是一種可以支持多種語言的運行時,其基本的核心功能包含:
- 內存管理
- 程序集加載和卸載
- 類型安全
- 異常處理
- 線程同步
2. 托管模塊
CLR並不關心是使用何種語言進行編程開發,只要編譯器是面向CLR而進行編譯的即可,這個中間的結果,就是IL(Intermediate Language), 最終面向CLR編譯得到的結果是:IL語句以及托管數據(元數據)組成的托管模塊
PS:
- 元數據: 元數據的本質就是一種描述數據的數據
借鑒相關文章的圖,其基本的過程為:
托管模塊的基本組成:
- PE32/PE32+(64位)
- CLR頭
- 元數據
- IL代碼(托管代碼)
3. 引用類型和值類型
這部分略過,基本都有相關的認識,本質是看其分配的內存位於內存堆上還是棧上。
- 每個進程會分配一個對應的進程堆,這就是我們常說的程序內存申請區域,不同進程是不會有交叉的。在堆上還是在棧上進行內存分配,是沒有速度差異的,都很快。
4. 垃圾回收器(Garbage Collector)
在CLR中的自動內存管理,就會使用垃圾回收器來執行內存管理,其會定時執行,或者在申請內存分配是發現內存不足時觸發執行,也可以手動觸發執行(System.GC.Collect)
垃圾回收的幾種基本算法
-
標記清除算法(Mark-Sweep)
關鍵點是,清除后,並不會執行內存的壓縮 -
復制算法(Copying) 內存等額划分,每次執行垃圾回收后,拷貝不被回收的內存到沒有被使用的內存塊,自帶內存壓縮,弊端是內存浪費大(每次只能使用部分,預留部分給拷貝使用)
-
標記整理算法(Mark-Compact)
關鍵點,清除后,會執行內存壓縮,不會有內存碎片 -
分代收集算法(Generational Collection)
對內存對象進行分代標記,避免全量垃圾回收帶來的性能消耗。下文會詳細講解。
2.2 垃圾回收模型
1. 垃圾回收的目的
緣由: 內存是有限的,為了避免內存溢出,需要清理無效內存
2. 觸發時機
- 申請分配內存時內存不足(本身不足或者內存碎片過多沒有足夠大小的內存片)
- 強制調用System.GC.Collect
- CLR卸載應用程序域(AppDomain)
- CLR正在關閉(后面2種在進程運行時不會觸發)
3. 垃圾回收的流程
-
GC准備階段
暫停進程中的所有線程,避免線程在CLR檢測根期間訪問堆內存 -
GC的標記階段
首先,會默認托管堆上所有的對象都是垃圾(可回收對象),然后開始遍歷根對象並構建一個由所有和根對象之間有引用關系的對象構成的對象圖,然后GC會挨個遍歷根對象和其引用對象,如果根對象沒有任何引用對象(null)GC會忽略該根對象。
對於含有引用對象的根對象以及其引用對象,GC將其納入對象圖中,如果發現已經處於對象圖中,則換一個路徑遍歷,避免無限循環。
PS: 所有的全局和靜態對象指針是應用程序的根對象。
-
垃圾回收階段 完成遍歷操作后,對於沒有被納入對象圖中的對象,執行清理操作
-
碎片整理階段
如果垃圾回收算法包含這個階段,則會對剩下的保留的對象進行一次內存整理,重新歸類到堆內存中,相應的引用地址也會對應的整理,避免內存碎片的產生。
4. 分代垃圾回收的過程
分代的基本設計思路:
- 對象越新,生命周期越短,反之也成立
- 回收托管堆的一部分,性能和速度由於回收整個托管堆
基本的分代: 0/1/2:
- 0代: 從未被標記為回收的新分配對象
- 1代: 上一次垃圾回收中沒有被回收的對象
- 2代: 在一次以上的垃圾回收后任然未被回收的對象
操作圖解釋分代的過程:
- 低一代的GC觸發,移動到高一代后,未必會觸發高一代的GC,只有高一代的內存不足時才會觸發高一代的GC
- 不同代的自動GC頻率是可以設置的,一般0:1:2的頻率為100:10:1
2.3 非托管對象的回收
對於非托管對象的管理,不受CLR的自動內存管理操作,這部分需要借鑒CLR的自動管理或者手動執行內存回收,這就是兩種非托管對象的管理方式: Finalize和Dispose
非托管資源: 原始的操作系統文件句柄,原始的非托管數據庫連接,非托管內存或資源
1.Finalize
System.Object定義了Finalize()虛方法,不能用override重寫,其寫法類似c++的析構函數:
class Finalization{
~Finalization()
{
//這里的代碼會進入Finalize方法 Console.WriteLine("Enter Finalize()"); } }
轉換的IL:
基類方法放入到Finally中,其本質還是交給GC進行處理,只是其執行的時間不確定,是在GC完后在某個時間點觸發執行Finalize方法,使用這個方法的唯一好處就是: 非托管資源是必然會被釋放的。
2. IDisposable
繼承了該接口,則需要實現Disposable接口,需要手動調用,這就確保了回收的及時性,對應的問題是如果不顯示調用Dispose方法,則這部分非托管資源是不會被回收的。
c#中的using關鍵字,轉換成IL語句,就是內部實現了IDispoable方法,最終的try/finally中,會在finally中調用dispose方法。
2.4 Unity中的C# GC
目前unity2018.4還是 Boehm–Demers–Weiser garbage collector, unity2019.1 中已經開始引入: Incremental Garbage Collection增量式垃圾回收功能,
相關鏈接: https://www.gamefromscratch.com/post/2018/11/27/unity-add-incremental-garbage-collection-in-20191.aspx
三、lua語言的垃圾回收
3.1 基本數據結構
lua的基本數據結構: union + type
typedef union Value{
GCObject* gc; //gc object
void* p; // light userdata
int b; // booleans
lua_CFunction f; // light c functions
lua_Integer i; //integer number 5.1為double,5.3為long long 8個字節 lua_Number n; // double number 5.3 為double 8個字節 } Value; struct lua_Value{ Value value_; int tt_; } TValue;
對於所有的需要被GC的對象,都會放在GCObject組成的鏈表中
3.2 GC算法和流程
1. 雙色標記清除算法
在Lua5.0中的GC,是一次性不可被打斷的操作,執行的算法是Mark-and-sweep算法,在執行GC操作的時候,會設置2種顏色,黑色和白色,然后執行gc的流程,大體的偽代碼流程如下:
每個新創建的對象為白色 //初始化階段 遍歷root鏈表中的對象,並將其加入到對象鏈表中 //標記階段 當前對象鏈表中還有未被掃描的元素: 從中取出對象並將其標記為黑色 遍歷這個對象關聯的其他所有對象: 標記為黑色 //回收階段 遍歷所有對象: 如果為白色: 這些對象沒有被引用,則執行回收 否則: 這些對象仍然被引用,需要保留
整個過程是不能被打斷的,這是為了避免一種情況:
如果可以被打斷,在GC的過程中新創建一個對象
那么如果標記為白色,此時處於回收階段,那么這個對象沒有被掃描就會被回收;
如果標記為黑色,此時處於回收階段,那么這個對象沒有被掃描就會被保留
兩種情況都不適合,所以只有讓整個過程不可被打斷,帶來的問題就是造成gc的時候卡頓
2. 三色標記清除算法
雖然是三色,本質是四色,顏色分為三種:
白色: 當前對象為待訪問狀態,表示對象還未被gc標記過,也就是對象創建的初始狀態; 同理,如果在gc完成后,仍然為白色,則說明當前對象沒有被引用,則可以被清除回收
灰色: 當前對象為待掃描狀態,當前對象已經被掃描過,但是其引用的其他對象沒有被掃描
黑色: 當前對象已經掃描過,並且其引用的其他對象也被掃描過
其流程偽代碼:
每個新創建的對象為白色 //初始化階段 遍歷root階段中引用的對象,從白色設置為灰色,並放入到灰色節點列表中 //標記階段 當灰色鏈表中還有未被掃描的元素: 從中去除一個對象並將其標記為黑色 遍歷這個對象關聯的其他所有對象: 如果是白色: 標記為灰色,並加入灰色鏈表中 //回收階段 遍歷所有對象: 如果為白色: 這些對象沒有被引用,需要被回收 否則: 重新加入對象鏈表中等待下次gc
整個標記過程是可以被打斷的,被打斷后回來只需要接着執行標記過程即可,回收階段是不可被打斷的。
如何解決在標記階段之后創建的對象為白色的問題?
分裂白色為兩種白色,一種為當前白色 currentwhite, 一種為非當前白色 otherwhite,新創建的對象都為otherwhite,則在執行回收的時候,如果為otherwhite則不執行回收操作,等待下次gc的時候,會執行白色的輪換,則新創建的對象會進入下一輪gc。
3.3 lua gc的一些關鍵點
1. 初始化階段的操作原理
以前我一直理解這個root就是將gcobject的鏈表進行轉換到灰色鏈表中,其實並不是,而是去對當前虛擬機中的mainthread表, G表, registry表進行操作,其函數為:
static void markroot(lua_State * L) { global_State *g = G(L); g->gray = NULL; g->grayagain = NULL; g->weak = NULL; //標記幾個入口 markobject(g, g->mainthread); markvalue(g, gt(g->mainthread)); markvalue(g, registry(L)); markmt(g); g->gcstate = GCSpropagte; }
markobject/markvalue都是將對象從白色標記為灰色,所以這里面還有效的數據,就會最終進行掃描標記,如果最終不是白色,則會被保留,而執行回收操作的時候,是對gclist進行操作的,只要是currentwhite,那么就是可以被回收的。
2. 對於中途創建的對象的顏色處理
這兒會分為兩種,前向操作和后退操作:
前向操作: 新創建對象為白色,被一個黑色對象引用,則將當前新創建對象標記為灰色
后退操作: 新創建對象為白色,被黑色對象引用,該黑色對象退回到灰色,塞入到grayagain表中,后續一次性掃描處理
對大部分數據,都是前向操作,對於table類型數據,則如果其新創建對象,該table會回退到灰色塞入到grayagain表中。
本質沒區別,主要是table屬於頻繁操作的對象,如果反復將table中新創建的對象都設置成灰色,則灰色鏈表會容易變得很大,所以為了提高性能,就將table塞入到grayagain表中,后續一次性處理即可。