C#垃圾回收機制詳解
GC的前世與今生
雖然本文是以.net
作為目標來講述GC
,但是GC
的概念並非才誕生不久。早在1958年,由鼎鼎大名的圖林獎得主John McCarthy
所實現的Lisp
語言就已經提供了GC
的功能,這是GC
的第一次出現。Lisp
的程式設計師認為記憶體管理太重要了,所以不能由程式設計師自己來管理。但後來的日子裡Lisp
卻沒有成氣候,採用記憶體手動管理的語言佔據了上風,以C為代表。出於同樣的理由,不同的人卻又不同的看法,C程式設計師認為記憶體管理太重要了,所以不能由系統來管理,並且譏笑Lisp
程式慢如烏龜的執行速度。的確,在那個對每一個Byte
都要精心計算的年代GC
的速度和對系統資源的大量佔用使很多人的無法接受。而後,1984年由Dave Ungar
開發的Small talk
語言第一次採用了Generational garbage collection
的技術(這個技術在下文中會談到),但是Small talk
也沒有得到十分廣泛的應用。
直到20世紀90年代中期GC
才以主角的身份登上了歷史的舞臺,這不得不歸功於Java
的進步,今日的GC
已非吳下阿蒙。Java採用VM(Virtual Machine)機制,由VM來管理程式的執行當然也包括對GC管理。90年代末期.net
出現了,.net
採用了和Java
類似的方法由CLR(Common Language Runtime)
來管理。這兩大陣營的出現將人們引入了以虛擬平臺為基礎的開發時代,GC
也在這個時候越來越得到大眾的關注。
為什麼要使用GC
呢?也可以說是為什麼要使用記憶體自動管理?有下面的幾個原因:
1、提高了軟體開發的抽象度;
2、程式設計師可以將精力集中在實際的問題上而不用分心來管理記憶體的問題;
3、可以使模組的介面更加的清晰,減小模組間的偶合;
4、大大減少了記憶體人為管理不當所帶來的Bug
;
5、使記憶體管理更加高效。
總的說來就是GC
可以使程式設計師可以從複雜的記憶體問題中擺脫出來,從而提高了軟體開發的速度、質量和安全性。
什麼是GC
GC
如其名,就是垃圾收集,當然這裡僅就記憶體而言。Garbage Collector
(垃圾收集器,在不至於混淆的情況下也成為GC)以應用程式的root
為基礎,遍歷應用程式在Heap
上動態分配的所有物件[2],通過識別它們是否被引用來確定哪些物件是已經死亡的哪些仍需要被使用。已經不再被應用程式的root
或者別的物件所引用的物件就是已經死亡的物件,即所謂的垃圾,需要被回收。這就是GC
工作的原理。為了實現這個原理,GC
有多種演算法。比較常見的演算法有Reference Counting,Mark Sweep,Copy Collection等等。目前主流的虛擬系統.net CLR
,Java VM
和Rotor
都是採用的Mark Sweep
演算法。
一、Mark-Compact 標記壓縮演算法
簡單把.NET
的GC
演算法看作Mark-Compact
演算法
階段1: Mark-Sweep
標記清除階段
先假設heap
中所有物件都可以回收,然後找出不能回收的物件,給這些物件打上標記,最後heap
中沒有打標記的物件都是可以被回收的
階段2: Compact
壓縮階段
物件回收之後heap
記憶體空間變得不連續,在heap
中移動這些物件,使他們重新從heap
基地址開始連續排列,類似於磁碟空間的碎片整理
Heap
記憶體經過回收、壓縮之後,可以繼續採用前面的heap
記憶體分配方法,即僅用一個指標記錄heap
分配的起始地址就可以
主要處理步驟:將執行緒掛起=>確定roots
=>建立reachable objectsgraph
=>物件回收=>heap
壓縮=>指標修復
可以這樣理解roots
:heap
中物件的引用關係錯綜複雜(交叉引用、迴圈引用),形成複雜的graph
,roots
是CLR
在heap
之外可以找到的各種入口點。GC
搜尋roots
的地方包括全域性物件、靜態變數、區域性物件、函式呼叫引數、當前CPU暫存器中的物件指標(還有finalizationqueue
)等。主要可以歸為2種型別:已經初始化了的靜態變數、執行緒仍在使用的物件(stack+CPU register)
Reachable objects
:指根據物件引用關係,從roots
出發可以到達的物件。例如當前執行函式的區域性變數物件A是一個rootobject
,他的成員變數引用了物件B,則B是一個reachable object
。從roots
出發可以建立reachable objectsgraph
,剩餘物件即為unreachable
,可以被回收
指標修復是因為compact
過程移動了heap
物件,物件地址發生變化,需要修復所有引用指標,包括stack
、CPUregister
中的指標以及heap
中其他物件的引用指標
Debug
和release
執行模式之間稍有區別,release
模式下後續程式碼沒有引用的物件是unreachable
的,而debug
模式下需要等到當前函式執行完畢,這些物件才會成為unreachable
,目的是為了除錯時跟蹤區域性物件的內容
傳給了COM+
的託管物件也會成為root
,並且具有一個引用計數器以相容COM+
的記憶體管理機制,引用計數器為0時這些物件才可能成為被回收物件
Pinnedobjects
指分配之後不能移動位置的物件,例如傳遞給非託管程式碼的物件(或者使用了fixed
關鍵字),GC
在指標修復時無法修改非託管程式碼中的引用指標,因此將這些物件移動將發生異常。pinnedobjects
會導致heap
出現碎片,但大部分情況來說傳給非託管程式碼的物件應當在GC
時能夠被回收掉
二、Generational 分代演算法
程式可能使用幾百M、幾G的記憶體,對這樣的記憶體區域進行GC
操作成本很高,分代演算法具備一定統計學基礎,對GC
的效能改善效果比較明顯
將物件按照生命週期分成新的、老的,根據統計分佈規律所反映的結果,可以對新、老區域採用不同的回收策略和演算法,加強對新區域的回收處理力度,爭取在較短時間間隔、較小的記憶體區域內,以較低成本將執行路徑上大量新近拋棄不再使用的區域性物件及時回收掉
分代演算法的假設前提條件:
1、大量新建立的物件生命週期都比較短,而較老的物件生命週期會更長
2、對部分記憶體進行回收比基於全部記憶體的回收操作要快
3、新建立的物件之間關聯程度通常較強。heap
分配的物件是連續的,關聯度較強有利於提高CPU cache
的命中率
.NET將heap分成3個代齡區域: Gen 0、Gen 1、Gen 2
Heap
分為3個代齡區域,相應的GC
有3種方式: # Gen 0 collections
, # Gen 1 collections
, #Gen 2 collections
。如果Gen 0 heap記憶體達到閥值,則觸發0代GC
,0代GC
後Gen 0中倖存的物件進入Gen 1。如果Gen 1
的記憶體達到閥值,則進行1代GC
,1代GC
將Gen 0 heap和Gen 1 heap一起進行回收,倖存的物件進入Gen2。2代GC
將Gen 0 heap、Gen 1 heap和Gen 2 heap一起回收
Gen 0和Gen 1比較小,這兩個代齡加起來總是保持在16M左右;Gen2的大小由應用程式確定,可能達到幾G,因此0代和1代GC
的成本非常低,2代GC
稱為fullGC
,通常成本很高。粗略的計算0代和1代GC
應當能在幾毫秒到幾十毫秒之間完成,Gen 2 heap比較大時fullGC
可能需要花費幾秒時間。大致上來講.NET
應用執行期間2代、1代和0代GC
的頻率應當大致為1:10:100
。
三、Finalization Queue
和Freachable Queue
這兩個佇列和.net
物件所提供的Finalize
方法有關。這兩個佇列並不用於儲存真正的物件,而是儲存一組指向物件的指標。當程式中使用了new
操作符在Managed Heap
上分配空間時,GC
會對其進行分析,如果該物件含有Finalize
方法則在Finalization Queue
中新增一個指向該物件的指標。在GC
被啟動以後,經過Mark
階段分辨出哪些是垃圾。再在垃圾中搜尋,如果發現垃圾中有被Finalization Queue
中的指標所指向的物件,則將這個物件從垃圾中分離出來,並將指向它的指標移動到Freachable Queue
中。這個過程被稱為是物件的復生(Resurrection
),本來死去的物件就這樣被救活了。為什麼要救活它呢?因為這個物件的Finalize
方法還沒有被執行,所以不能讓它死去。Freachable Queue
平時不做什麼事,但是一旦裡面被新增了指標之後,它就會去觸發所指物件的Finalize
方法執行,之後將這個指標從佇列中剔除,這是物件就可以安靜的死去了。.net framework
的System.GC
類提供了控制Finalize
的兩個方法,ReRegisterForFinalize
和SuppressFinalize
。前者是請求系統完成物件的Finalize
方法,後者是請求系統不要完成物件的Finalize
方法。ReRegisterForFinalize
方法其實就是將指向物件的指標重新新增到Finalization Queue
中。這就出現了一個很有趣的現象,因為在Finalization Queue
中的物件可以復生,如果在物件的Finalize
方法中呼叫ReRegisterForFinalize
方法,這樣就形成了一個在堆上永遠不會死去的物件,像鳳凰涅槃一樣每次死的時候都可以復生。
託管資源:
Net
中的所有型別都是(直接或間接)從System.Object
型別派生的。
CTS
中的型別被分成兩大類——引用型別(reference type
,又叫託管型別[managed type]
),分配在記憶體堆上。值型別(value type
),值型別分配在堆疊上。如圖
值型別在棧裡,先進後出,值型別變數的生命有先後順序,這個確保了值型別變數在推出作用域以前會釋放資源。比引用型別更簡單和高效。堆疊是從高地址往低地址分配記憶體。
引用型別分配在託管堆(Managed Heap
)上,宣告一個變數在棧上儲存,當使用new
建立物件時,會把物件的地址儲存在這個變數裡。託管堆相反,從低地址往高地址分配記憶體,如圖
.net
中超過80%
的資源都是託管資源。
非託管資源:
ApplicationContext,Brush,Component,ComponentDesigner,Container,Context,Cursor,FileStream,Font,Icon,Image,Matrix,Object,OdbcDataReader,OleDBDataReader,Pen,Regex,Socket,StreamWriter,Timer,Tooltip
,檔案控制程式碼,GDI資源,資料庫連線等等資源。可能在使用的時候很多都沒有注意到!
.NET的GC機制有這樣兩個問題:
首先,GC並不是能釋放所有的資源。它不能自動釋放非託管資源。
第二,GC並不是實時性的,這將會造成系統效能上的瓶頸和不確定性。
GC並不是實時性的,這會造成系統效能上的瓶頸和不確定性。所以有了IDisposable
介面,IDisposable
介面定義了Dispose
方法,這個方法用來供程式設計師顯式呼叫以釋放非託管資源。使用using
語句可以簡化資源管理。
示例
當你用Dispose
方法釋放未託管物件的時候,應該呼叫GC.SuppressFinalize
。如果物件正在終結佇列(finalization queue
),GC.SuppressFinalize
會阻止GC
呼叫Finalize
方法。因為Finalize
方法的呼叫會犧牲部分效能。如果你的Dispose
方法已經對委託管資源作了清理,就沒必要讓GC
再呼叫物件的Finalize
方法(MSDN)。附上MSDN的程式碼,大家可以參考.
public class BaseResource : IDisposable
{
// 指向外部非託管資源
private IntPtr handle;
// 此類使用的其它託管資源.
private Component Components;
// 跟蹤是否呼叫.Dispose方法,標識位,控制垃圾收集器的行為
private bool disposed = false;
// 建構函式
public BaseResource()
{
// Insert appropriate constructor code here.
}
// 實現介面IDisposable.
// 不能宣告為虛方法virtual.
// 子類不能重寫這個方法.
public void Dispose()
{
Dispose(true);
// 離開終結佇列Finalization queue
// 設定物件的阻止終結器程式碼
//
GC.SuppressFinalize(this);
}
// Dispose(bool disposing) 執行分兩種不同的情況.
// 如果disposing 等於 true, 方法已經被呼叫
// 或者間接被使用者程式碼呼叫. 託管和非託管的程式碼都能被釋放
// 如果disposing 等於false, 方法已經被終結器 finalizer 從內部呼叫過,
//你就不能在引用其他物件,只有非託管資源可以被釋放。
protected virtual void Dispose(bool disposing)
{
// 檢查Dispose 是否被呼叫過.
if (!this.disposed)
{
// 如果等於true, 釋放所有託管和非託管資源
if (disposing)
{
// 釋放託管資源.
Components.Dispose();
}
// 釋放非託管資源,如果disposing為 false,
// 只會執行下面的程式碼.
CloseHandle(handle);
handle = IntPtr.Zero;
// 注意這裡是非執行緒安全的.
// 在託管資源釋放以後可以啟動其它執行緒銷燬物件,
// 但是在disposed標記設定為true前
// 如果執行緒安全是必須的,客戶端必須實現。
}
disposed = true;
}
// 使用interop 呼叫方法
// 清除非託管資源.
[System.Runtime.InteropServices.DllImport("Kernel32")]
private extern static Boolean CloseHandle(IntPtr handle);
// 使用C# 解構函式來實現終結器程式碼
// 這個只在Dispose方法沒被呼叫的前提下,才能呼叫執行。
// 如果你給基類終結的機會.
// 不要給子類提供解構函式.
~BaseResource()
{
// 不要重複建立清理的程式碼.
// 基於可靠性和可維護性考慮,呼叫Dispose(false) 是最佳的方式
Dispose(false);
}
// 允許你多次呼叫Dispose方法,
// 但是會丟擲異常如果物件已經釋放。
// 不論你什麼時間處理物件都會核查物件的是否釋放,
// check to see if it has been disposed.
public void DoSomething()
{
if (this.disposed)
{
throw new ObjectDisposedException();
}
}
// 不要設定方法為virtual.
// 繼承類不允許重寫這個方法
public void Close()
{
// 無引數呼叫Dispose引數.
Dispose();
}
public static void Main()
{
// Insert code here to create
// and use a BaseResource object.
}
}
GC.Collect()
方法
作用:強制進行垃圾回收。
GC
的方法:
名稱 | 說明 |
---|---|
Collect() | 強制對所有代進行即時垃圾回收。 |
Collect(Int32) | 強制對零代到指定代進行即時垃圾回收。 |
Collect(Int32, GCCollectionMode) | 強制在 GCCollectionMode 值所指定的時間對零代到指定代進行垃圾回收。 |
GC
注意事項:
1、只管理記憶體,非託管資源,如檔案控制程式碼,GDI資源,資料庫連線等還需要使用者去管理
2、迴圈引用,網狀結構等的實現會變得簡單。GC
的標誌也壓縮演算法能有效的檢測這些關係,並將不再被引用的網狀結構整體刪除。
3、GC
通過從程式的根物件開始遍歷來檢測一個物件是否可被其他物件訪問,而不是用類似於COM
中的引用計數方法。
4、GC
在一個獨立的執行緒中執行來刪除不再被引用的記憶體
5、GC
每次執行時會壓縮託管堆
6、你必須對非託管資源的釋放負責。可以通過在型別中定義Finalizer
來保證資源得到釋放。
7、物件的Finalizer
被執行的時間是在物件不再被引用後的某個不確定的時間。注意並非和C++
中一樣在物件超出宣告週期時立即執行解構函式
8、Finalizer
的使用有效能上的代價。需要Finalization
的物件不會立即被清除,而需要先執行Finalizer.Finalizer
不是在GC
執行的執行緒被呼叫。GC
把每一個需要執行Finalizer
的物件放到一個佇列中去,然後啟動另一個執行緒來執行所有這些Finalizer
.而GC
執行緒繼續去刪除其他待回收的物件。在下一個GC
週期,這些執行完Finalizer
的物件的記憶體才會被回收。
9、.NET GC
使用”代”(generations
)的概念來優化效能。代幫助GC
更迅速的識別那些最可能成為垃圾的物件。在上次執行完垃圾回收後新建立的物件為第0代物件。經歷了一次GC
週期的物件為第1代物件。經歷了兩次或更多的GC
週期的物件為第2代物件。代的作用是為了區分區域性變數和需要在應用程式生存週期中一直存活的物件。大部分第0代物件是區域性變數。成員變數和全域性變數很快變成第1代物件並最終成為第2代物件。
10、GC
對不同代的物件執行不同的檢查策略以優化效能。每個GC
週期都會檢查第0代物件。大約1/10的GC
週期檢查第0代和第1代物件。大約1/100的GC週期檢查所有的物件。重新思考Finalization
的代價:需要Finalization
的物件可能比不需要Finalization
在記憶體中停留額外9個GC週期。如果此時它還沒有被Finalize
,就變成第2代物件,從而在記憶體中停留更長時間。
相關文章
- JVM之垃圾回收機制詳解分析JVM
- Java垃圾回收機制詳解及效能最佳化詳解。Java
- 圖解Golang垃圾回收機制!圖解Golang
- 超詳細的node垃圾回收機制
- java垃圾回收機制Java
- js垃圾回收機制JS
- javascript 垃圾回收機制JavaScript
- Python垃圾回收機制Python
- JVM 垃圾回收機制JVM
- JVM垃圾回收機制JVM
- Java 垃圾回收機制Java
- 剖析垃圾回收機制(上)
- java垃圾回收機制整理Java
- JS的垃圾回收機制JS
- jvm的垃圾回收機制JVM
- JavaScript的垃圾回收機制JavaScript
- PHP的垃圾回收機制PHP
- PHP的垃圾回收機制-回收週期PHP
- JAVA垃圾回收機制和Python垃圾回收對比與分析JavaPython
- JS垃圾回收機制筆記JS筆記
- [效能][JVM]jvm垃圾回收機制JVM
- V8垃圾回收機制
- JVM垃圾回收機制入門JVM
- 談談 JVM 垃圾回收機制JVM
- 【翻譯】PHP 垃圾回收機制PHP
- Flutter中的垃圾回收機制Flutter
- Java虛擬機器詳解(三)------垃圾回收Java虛擬機
- 聊聊JVM的垃圾回收機制GCJVMGC
- 秒懂JVM的垃圾回收機制JVM
- python進階(7)垃圾回收機制Python
- Java的垃圾回收(Garbage Collection)機制Java
- 圖解 Java 垃圾回收機制,寫得非常好!圖解Java
- 用垃圾回收機制解釋JavaScript中的閉包JavaScript
- php底層原理之垃圾回收機制PHP
- 深入理解 JVM 之 垃圾回收機制JVM
- JVM學習(二)——GC垃圾回收機制JVMGC
- 深入理解Go-垃圾回收機制Go
- JDK 18 GC垃圾回收機制比較JDKGC