C#-垃圾回收機制(GC) 什麼是GC 官網中有這麼一句話: The garbage collector is a common language runtime component that controls the allocation and release of managed memory ...
C#-垃圾回收機制(GC)
什麼是GC
The garbage collector is a common language runtime component that controls the allocation and release of managed memory。
垃圾回收機制(Garbage Collection)簡稱GC,是CLR的一個組件,它控制記憶體的分配與釋放。
概括:就是GC會幫你自動管理記憶體,分配記憶體,回收記憶體,採用的就是對應的GC的演算法。
GC產生的背景
每個程式都要使用這樣或那樣的資源,比如文件、記憶體緩衝區、屏幕空間、網路連接、資料庫資源等。在面向對象的環境中,每個類型都代表可供程式使用的一種資源。要使用這些資源,必須為代表資源的類型分配記憶體。
上述步驟如果最後一步是由程式員負責,可能會產生一些無法預測的問題,如忘記釋放不再使用的記憶體、試圖使用已被釋放的記憶體(即野指針),這種bug會造成資源泄露(浪費記憶體)和對象損壞(影響穩定性)。而正確的進行資源管理通常很難而且很枯燥,它會極大的分散程式員的註意力。而GC能簡化程式員的記憶體管理工作。
GC工作原理
垃圾收集器的本質,就是跟蹤所有被引用到的對象,整理不再被引用的對象,回收相應的記憶體。
以應用程式的root為基礎,遍歷應用程式在Heap上動態分配的所有對象,通過識別它們是否被引用來確定哪些對象是已經死亡的、哪些仍需要被使用。已經不再被應用程式的root或者別的對象所引用的對象就是已經死亡的對象,即所謂的垃圾,需要被回收。
垃圾回收的演算法有有多種,在.Net中採用了一種叫做"標記與清除(Mark-Sweep)"演算法,該演算法分兩個本領:
- "標記"本領——垃圾的識別:從應用程式的root出發,利用相互引用關係,遍歷其在Heap上動態分配的所有對象,沒有被引用的對象不被標記,即成為垃圾;存活的對象被標記,即維護成了一張"根-對象可達圖"。其實,CLR會把對象關係看做"樹圖",這樣會加快遍歷對象的速度。.Net中利用棧來完成檢測並標記對象引用,在不斷的入棧與出棧中完成檢測:先在樹圖中選擇一個需要檢測的對象,將該對象的所有引用壓棧,如此反覆直到棧變空為止。棧變空意味著已經遍歷了這個局部根能夠到達的所有對象。樹圖節點範圍包括局部變數、寄存器、靜態變數,這些元素都要重覆這個操作。一旦完成,便逐個對象地檢查記憶體,沒有標記的對象變成了垃圾。
- "清除"本領——回收記憶體:啟用壓縮(Compact)演算法,對記憶體中存活的對象進行移動,修改它們的指針,使之在記憶體中連續,這樣空閑的記憶體也就連續了,這就解決了記憶體碎片問題,當再次為新對象分配記憶體時,CLR不必在充滿碎片的記憶體中尋找適合新對象的記憶體空間,所以分配速度會大大提高。但是大對象(large object heap)除外,GC不會移動一個記憶體中巨無霸,因為它知道現在的CPU不便宜。通常,大對象具有很長的生存期,當一個大對象在.NET托管堆中產生時,它被分配在堆的一個特殊部分中,移動大對象所帶來的開銷超過了整理這部分堆所能提高的性能。
Compact演算法除了會提高再次分配記憶體的速度,如果新分配的對象在堆中位置很緊湊的話,高速緩存的性能將會得到提高,因為一起分配的對象經常被一起使用(程式的局部性原理),所以為程式提供一段連續空白的記憶體空間是很重要的。
簡單地把.NET的GC演算法看作Mark-Compact演算法。階段1: Mark-Sweep 標記清除階段,先假設heap中所有對象都可以回收,然後找出不能回收的對象,給這些對象打上標記,最後heap中沒有打標記的對象都是可以被回收的;階段2: Compact 壓縮階段,對象回收之後heap記憶體空間變得不連續,在heap中移動這些對象,使他們重新從heap基地址開始連續排列,類似於磁碟空間的碎片整理。Heap記憶體經過回收、壓縮之後,可以繼續採用前面的heap記憶體分配方法,即僅用一個指針記錄heap分配的起始地址就可以。
主要處理步驟:將線程掛起→確定roots→創建reachable objects graph→對象回收→heap壓縮→指針修複。可以這樣理解roots:heap中對象的引用關係錯綜複雜(交叉引用、迴圈引用),形成複雜的graph,roots是CLR在heap之外可以找到的各種入口點。
GC搜索roots的地方包括全局對象、靜態變數、局部對象、函數調用參數、當前CPU寄存器中的對象指針(還有finalization queue)等。主要可以歸為2種類型:已經初始化了的靜態變數、線程仍在使用的對象(stack+CPU register) 。
Reachable objects:指根據對象引用關係,從roots出發可以到達的對象。例如當前執行函數的局部變數對象A是一個root object,他的成員變數引用了對象B,則B是一個reachable object。從roots出發可以創建reachable objects graph,剩餘對象即為unreachable,可以被回收 。
指針修複是因為compact過程移動了heap對象,對象地址發生變化,需要修複所有引用指針,包括stack、CPU register中的指針以及heap中其他對象的引用指針。
Debug和release執行模式之間稍有區別,release模式下後續代碼沒有引用的對象是unreachable的,而debug模式下需要等到當前函數執行完畢,這些對象才會成為unreachable,目的是為了調試時跟蹤局部對象的內容。傳給了COM+的托管對象也會成為root,並且具有一個引用計數器以相容COM+的記憶體管理機制,引用計數器為0時,這些對象才可能成為被回收對象。Pinned objects指分配之後不能移動位置的對象,例如傳遞給非托管代碼的對象(或者使用了fixed關鍵字),GC在指針修複時無法修改非托管代碼中的引用指針,因此將這些對象移動將發生異常。pinned objects會導致heap出現碎片,但大部分情況來說傳給非托管代碼的對象應當在GC時能夠被回收掉。
Generational 分代演算法
- 對於較大記憶體的對象,頻繁的進行GC將耗費大量的資源,成本很高且效果較差
- 大量新創建的對象生命周期都較短,老對象的生命周期都較長
- 小部分的進行GC比大塊的進行GC效率更高,消耗更少
- 新創建的對象在記憶體分配上多為連續,且關聯程度較強,關聯度較強有利於CPU Cache命中。
基於此,按照壽命長短,托管堆被分為了三個年齡層,分別是Generation 0,Generation 1, Generation 2。垃圾收集器在第 0 代存儲新對象。在應用程式生命周期早期創建的在收集過程中幸存下來的對象被提升並存儲在第 1 代和第 2 代中。因為壓縮托管堆的一部分比壓縮整個堆要快,因此該方案允許垃圾收集器在特定代中釋放記憶體,而不是在每次執行收集時釋放整個托管堆的記憶體。
第 0 代:這是最年輕的一代,包含生命周期很短的對象。短期對象的一個例子是臨時變數。垃圾收集在這一代發生得最頻繁。新分配的對象形成了第0代的對象,並且是隱式的第 0 代集合。但是,對象很大,它們將進入大對象堆 (LOH),有時也稱為第3 代。第3 代可以理解為物理代,作為第二代的衍生。 大多數對象在第 0 代被回收用於垃圾收集,並且不會存活到下一代。
如果應用程式在第 0 代已滿時嘗試創建新對象,垃圾收集器將執行收集以嘗試釋放對象的地址空間。垃圾收集器首先檢查第 0代中的對象,而不是托管堆中的所有對象。單獨的第 0 代集合通常會回收足夠的記憶體,使應用程式能夠繼續創建新對象。
第 1 代:這一代包含短期對象,並作為短期對象和長期對象之間的緩衝區。在垃圾收集器執行第 0代的收集後,它會壓縮可訪問對象的記憶體並將它們提升到第 1代。因為在收集中幸存下來的對象往往具有更長的生命周期,所以將它們提升到更高的代是有意義的。垃圾收集器不必在每次執行第 0代收集時重新檢查第 1 代和第 2 代中的對象。 如果第 0 代的集合沒有為應用程式回收足夠的記憶體來創建新對象,則垃圾收集器可以執行第1 代的收集,然後是第 2 代。第 1 代中在集合中幸存下來的對象將被提升到第 2 代。
第 2 代:這一代包含長期存在的對象。長壽命對象的一個示例是伺服器應用程式中的對象,其中包含在進程持續期間有效的靜態數據。在集合中存活的第 2 代對象將保留在第 2 代中,直到它們被確定在未來的集合中不可訪問。 大對象堆(有時稱為第3 代)上的對象也在第 2代中收集。
當條件允許時,垃圾收集發生在特定的世代。收集一代意味著收集該一代及其所有年輕一代的對象。第 2 代垃圾回收也稱為完整垃圾回收,因為它回收所有代中的對象(即托管堆中的所有對象)。
當垃圾收集器檢測到某一代存活率較高時,會增加該代的分配閾值。 下一個集合獲得大量回收記憶體。 CLR 不斷平衡兩個優先順序:不讓應用程式的工作集因延遲垃圾收集而變得太大,以及不讓垃圾收集運行得太頻繁。
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的GC機制有這樣兩個問題:
GC並不是實時性的,這會造成系統性能上的瓶頸和不確定性。所以有了IDisposable介面,IDisposable介面定義了Dispose方法,這個方法用來供程式員顯式調用以釋放非托管資源。使用using語句可以簡化資源管理。
- /// <summary>
- /// 執行SQL語句,返回影響的記錄數
- /// </summary>
- /// <param name="SQLString">SQL語句</param>
- /// <returns>影響的記錄數</returns>
- public static int ExecuteSql(string SQLString)
- {
- using (SqlConnection connection = new SqlConnection(connectionString))
- {
- using (SqlCommand cmd = new SqlCommand(SQLString, connection))
- {
- try
- {
- connection.Open();
- int rows = cmd.ExecuteNonQuery();
- return rows;
- }
- catch (System.Data.SqlClient.SqlException e)
- {
- connection.Close();
- throw e;
- }
- finally
- {
- cmd.Dispose();
- connection.Close();
- }
- }
- }
- }
當你用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.
}
}
說明 | |
Collect() | 強制對所有代進行即時垃圾回收。 |
Collect(Int32) | 強制對零代到指定代進行即時垃圾回收。 |
Collect(Int32, GCCollectionMode) | 強制在 GCCollectionMode 值所指定的時間對零代到指定代進行垃圾回收 |
GC註意事項
- 只管理記憶體,非托管資源,如文件句柄,GDI資源,資料庫連接等還需要用戶去管理。
- 迴圈引用,網狀結構等的實現會變得簡單。GC的標誌-壓縮演算法能有效的檢測這些關係,並將不再被引用的網狀結構整體刪除。
- GC通過從程式的根對象開始遍歷來檢測一個對象是否可被其他對象訪問,而不是用類似於COM中的引用計數方法。
- GC在一個獨立的線程中運行來刪除不再被引用的記憶體。
- GC每次運行時會壓縮托管堆。
- 你必須對非托管資源的釋放負責。可以通過在類型中定義Finalizer來保證資源得到釋放。
- 對象的Finalizer被執行的時間是在對象不再被引用後的某個不確定的時間。註意並非和C++中一樣在對象超出聲明周期時立即執行析構函數
- Finalizer的使用有性能上的代價。需要Finalization的對象不會立即被清除,而需要先執行Finalizer.Finalizer,不是在GC執行的線程被調用。GC把每一個需要執行Finalizer的對象放到一個隊列中去,然後啟動另一個線程來執行所有這些Finalizer,而GC線程繼續去刪除其他待回收的對象。在下一個GC周期,這些執行完Finalizer的對象的記憶體才會被回收。
- NET GC使用"代"(generations)的概念來優化性能。代幫助GC更迅速的識別那些最可能成為垃圾的對象。在上次執行完垃圾回收後新創建的對象為第0代對象。經歷了一次GC周期的對象為第1代對象。經歷了兩次或更多的GC周期的對象為第2代對象。代的作用是為了區分局部變數和需要在應用程式生存周期中一直存活的對象。大部分第0代對象是局部變數。成員變數和全局變數很快變成第1代對象並最終成為第2代對象。
- GC對不同代的對象執行不同的檢查策略以優化性能。每個GC周期都會檢查第0代對象。大約1/10的GC周期檢查第0代和第1代對象。大約1/100的GC周期檢查所有的對象。重新思考Finalization的代價:需要Finalization的對象可能比不需要Finalization在記憶體中停留額外9個GC周期。如果此時它還沒有被Finalize,就變成第2代對象,從而在記憶體中停留更長時間。
總結
垃圾回收背後有這樣一個基本的觀念:編程語言(大多數的)似乎總能訪問無限的記憶體。而開發者可以一直分配、分配再分配——像魔法一樣,取之不盡用之不竭。
GC的基本工作原理是:通過最基本的標記清除原理,清除不可達對象;再像磁碟碎片整理一樣壓縮、整理可用記憶體;最後通過分代演算法實現性能最優化。
問題記錄
答:不會,因為第0代和第1代的預算容量相差懸殊,而且不是在第1代空間完全滿的時候才清理記憶體的,而是差不多快滿的時候就會清理記憶體,這個"快滿"的空間是大於第0代的預算容量的;
答:同步塊索引的功能很多,即可以標記同步位,又可以標記可達,還可以存儲哈希碼
https://blog.csdn.net/acmilanvanbasten/article/details/14521051 具體可以看看這篇文章,寫的很詳細。