C#-垃圾回收機制(GC)

来源:https://www.cnblogs.com/caden/archive/2023/03/30/17273006.html
-Advertisement-
Play Games

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)"演算法,該演算法分兩個本領:

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演算法的設計考慮到了4個因素:

  1. 對於較大記憶體的對象,頻繁的進行GC將耗費大量的資源,成本很高且效果較差
  2. 大量新創建的對象生命周期都較短,老對象的生命周期都較長
  3. 小部分的進行GC比大塊的進行GC效率更高,消耗更少
  4. 新創建的對象在記憶體分配上多為連續,且關聯程度較強,關聯度較強有利於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機制有這樣兩個問題:

  1. GC並不是能釋放所有的資源。它不能自動釋放非托管資源。
  2. GC並不是實時性的,這將會造成系統性能上的瓶頸和不確定性。

GC並不是實時性的,這會造成系統性能上的瓶頸和不確定性。所以有了IDisposable介面,IDisposable介面定義了Dispose方法,這個方法用來供程式員顯式調用以釋放非托管資源。使用using語句可以簡化資源管理。

示例:

  1. /// <summary>  
  2. /// 執行SQL語句,返回影響的記錄數  
  3. /// </summary>  
  4. /// <param name="SQLString">SQL語句</param>  
  5. /// <returns>影響的記錄數</returns>  
  6. public static int ExecuteSql(string SQLString)  
  7. {  
  8.     using (SqlConnection connection = new SqlConnection(connectionString))  
  9.     {  
  10.         using (SqlCommand cmd = new SqlCommand(SQLString, connection))  
  11.         {  
  12.             try  
  13.             {  
  14.                 connection.Open();  
  15.                 int rows = cmd.ExecuteNonQuery();  
  16.                 return rows;  
  17.             }  
  18.             catch (System.Data.SqlClient.SqlException e)  
  19.             {  
  20.                 connection.Close();  
  21.                 throw e;  
  22.             }  
  23.             finally  
  24.             {  
  25.                 cmd.Dispose();  
  26.                 connection.Close();  
  27.             }  
  28.         }  
  29.     }  
  30. }  

當你用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代對象,從而在記憶體中停留更長時間。

總結

垃圾回收背後有這樣一個基本的觀念:編程語言(大多數的)似乎總能訪問無限的記憶體。而開發者可以一直分配、分配再分配——像魔法一樣,取之不盡用之不竭。

GC的基本工作原理是:通過最基本的標記清除原理,清除不可達對象;再像磁碟碎片整理一樣壓縮、整理可用記憶體;最後通過分代演算法實現性能最優化。

問題記錄

  1. 第0代時幸存的對象變成第1代,那麼有沒有可能這個時候第0代幸存對象的空間算到第1代,導致第1代滿了呢?

答:不會,因為第0代和第1代的預算容量相差懸殊,而且不是在第1代空間完全滿的時候才清理記憶體的,而是差不多快滿的時候就會清理記憶體,這個"快滿"的空間是大於第0代的預算容量的;

  1. 同步塊索引,你剛剛說又可以鎖住對象標記同步,又可以用來標記可達?

答:同步塊索引的功能很多,即可以標記同步位,又可以標記可達,還可以存儲哈希碼

https://blog.csdn.net/acmilanvanbasten/article/details/14521051 具體可以看看這篇文章,寫的很詳細。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • C++中的explicit關鍵字只能用於修飾只有一個參數的類構造函數,它的作用是表明該構造函數是顯示的,而非隱式的,跟它相對應的另一個關鍵字是implicit,意思是隱藏的,類構造函數預設情況下即聲明為implicit(隱式)。 那麼顯示聲明的構造函數和隱式聲明的有什麼區別呢? 來看下麵的例子: c ...
  • Java重寫toString的意義 一.toString()方法 toString()方法在Object類里定義的,其返回值類型為String類型,返回類名和它的引用地址. 在進行String類與其他類型的連接操作時,自動調用toString()方法,demo如下: Date time = new ...
  • 一、問題引入 單鏈表的實現【01】:Student-Management-System 只體現了項目功能實現,未對代碼部分做出說明。 故新增隨筆進行補充說明代碼部分。 重構代碼,迭代版本:Student Mangement System(Version 2.0) 二、解決過程 基於單鏈表實現就離不開 ...
  • 在java,c#類的成員修飾符包括,公有、私有、程式集可用的、受保護的。 對於python來說,只有兩個成員修飾符:公有成員,私有成員 成員修飾符是來修飾誰呢?當然是修飾成員了。那麼python類的成員包括什麼呢? python成員: 欄位,方法,屬性 每個類成員的修飾符有兩種: 公有成員:內部外部 ...
  • 前言 RocketMQ是阿裡巴巴旗下一款開源的MQ框架,經歷過雙十一考驗、Java編程語言實現,有非常好完整生態系統。RocketMQ作為一款純java、分散式、隊列模型的開源消息中間件,支持事務消息、順序消息、批量消息、定時消息、消息回溯等 本篇文章第一部分屬於一些核心概念和工作流程的講解;第二部 ...
  • 針對大量log日誌快速定位錯誤地方 動態查看日誌 tail -f catalina.ou 從頭打開日誌文件 cat catalina.ou 可以使用 >nanjiangtest.txt 輸出某個新日誌去查看 [root@yesky logs]# cat -n catalina.out |grep 7 ...
  • 近段時間忙於各種項目和對【易排平臺】的優化,沒顧得上分享APS相關的小技巧,回頭看看小公眾號的關註人數早已達1500+,在此爭取時間寫一下這段時間在項目上及平臺優化過程中遇到的一些小技巧,以感謝諸位的關註。過去數月的解決的問題中,涉及最多的是規劃模型中,實現各種時間維度的功能,目前在平臺上也稍有成果 ...
  • 呆了2個大屏行業的公司,對大屏幕有一些瞭解,所以整理下所瞭解的觸摸屏相關概念。方便自己以及進入這個行業的小伙伴們,能有個系統、快速的認知。 觸摸屏詳細的知識點,網上其實都有。整理資料過程中,我也瞭解了更多的觸摸屏知識,像聲波屏、光學屏之類的之前就沒接觸。下麵分不同的模塊,給大家介紹 交互觸摸屏類型 ...
一周排行
    -Advertisement-
    Play Games
  • 1. 說明 /* Performs operations on System.String instances that contain file or directory path information. These operations are performed in a cross-pla ...
  • 視頻地址:【WebApi+Vue3從0到1搭建《許可權管理系統》系列視頻:搭建JWT系統鑒權-嗶哩嗶哩】 https://b23.tv/R6cOcDO qq群:801913255 一、在appsettings.json中設置鑒權屬性 /*jwt鑒權*/ "JwtSetting": { "Issuer" ...
  • 引言 集成測試可在包含應用支持基礎結構(如資料庫、文件系統和網路)的級別上確保應用組件功能正常。 ASP.NET Core 通過將單元測試框架與測試 Web 主機和記憶體中測試伺服器結合使用來支持集成測試。 簡介 集成測試與單元測試相比,能夠在更廣泛的級別上評估應用的組件,確認多個組件一起工作以生成預 ...
  • 在.NET Emit編程中,我們探討了運算操作指令的重要性和應用。這些指令包括各種數學運算、位操作和比較操作,能夠在動態生成的代碼中實現對數據的處理和操作。通過這些指令,開發人員可以靈活地進行算術運算、邏輯運算和比較操作,從而實現各種複雜的演算法和邏輯......本篇之後,將進入第七部分:實戰項目 ...
  • 前言 多表頭表格是一個常見的業務需求,然而WPF中卻沒有預設實現這個功能,得益於WPF強大的控制項模板設計,我們可以通過修改控制項模板的方式自己實現它。 一、需求分析 下圖為一個典型的統計表格,統計1-12月的數據。 此時我們有一個需求,需要將月份按季度劃分,以便能夠直觀地看到季度統計數據,以下為該需求 ...
  • 如何將 ASP.NET Core MVC 項目的視圖分離到另一個項目 在當下這個年代 SPA 已是主流,人們早已忘記了 MVC 以及 Razor 的故事。但是在某些場景下 SSR 還是有意想不到效果。比如某些靜態頁面,比如追求首屏載入速度的時候。最近在項目中回歸傳統效果還是不錯。 有的時候我們希望將 ...
  • System.AggregateException: 發生一個或多個錯誤。 > Microsoft.WebTools.Shared.Exceptions.WebToolsException: 生成失敗。檢查輸出視窗瞭解更多詳細信息。 內部異常堆棧跟蹤的結尾 > (內部異常 #0) Microsoft ...
  • 引言 在上一章節我們實戰了在Asp.Net Core中的項目實戰,這一章節講解一下如何測試Asp.Net Core的中間件。 TestServer 還記得我們在集成測試中提供的TestServer嗎? TestServer 是由 Microsoft.AspNetCore.TestHost 包提供的。 ...
  • 在發現結果為真的WHEN子句時,CASE表達式的真假值判斷會終止,剩餘的WHEN子句會被忽略: CASE WHEN col_1 IN ('a', 'b') THEN '第一' WHEN col_1 IN ('a') THEN '第二' ELSE '其他' END 註意: 統一各分支返回的數據類型. ...
  • 在C#編程世界中,語法的精妙之處往往體現在那些看似微小卻極具影響力的符號與結構之中。其中,“_ =” 這一組合突然出現還真不知道什麼意思。本文將深入剖析“_ =” 的含義、工作原理及其在實際編程中的廣泛應用,揭示其作為C#語法奇兵的重要角色。 一、下劃線 _:神秘的棄元符號 下劃線 _ 在C#中並非 ...