GC總結

內存基礎知識

下面的列表總結了重要的 CLR內存概念。

·        每個進程都有其自己單獨的虛擬地址空間。同一臺計算機上的所有進程共享相同的物理內存,如果有頁文件,則也共享頁文件。

·        默認情況下,32 位計算機上的每個進程都具有 2 GB 的用戶模式虛擬地址空間。

·                    作爲一名應用程序開發人員,您只能使用虛擬地址空間,請勿直接操控物理內存。垃圾回收器爲您分配和釋放託管堆上的虛擬內存。如果您編寫的是本機代碼,請使用Win32 函數處理虛擬地址空間。 這些函數爲您分配和釋放本機堆上的虛擬內存。

·        虛擬內存有三種狀態:

o   可用。 該內存塊沒有引用關係,可用於分配。

o   保留。 內存塊可供您使用,並且不能用於任何其他分配請求。但是,在該內存塊提交之前,您無法將數據存儲到其中。

o   提交。 內存塊已指派給物理存儲。

·        可能會存在虛擬地址空間碎片。就是說地址空間中存在一些被稱爲孔的可用塊。 當請求虛擬內存分配時,虛擬內存管理器必須找到滿足該分配請求的足夠大的單個可用塊。 即使您具有 2 GB 的可用空間,2 GB 的分配請求也有可能會不成功,除非所有這些空間必須位於單個的地址塊中。

·        如果用完保留的虛擬地址空間或提交的物理空間,則可能會用盡內存。

進程初始化期間,CLR保留兩個區段(segment)的虛擬地址空間:一個區段是普通堆,另一個區段是大對象堆。每個區段的大小是不同的。對於客戶端應用程序,每個區段約爲16MB;對於服務器應用程序,每個區段大約爲64MB。還有一些其他因素會影響區段的大小,例如是32位還是64位操作系統上運行,以及機器安裝的CPU數量,在CPU較多的機器上,區段會小一些。隨着區段裝滿非垃圾對象,CLR會分配更多的區段,這個操作一直繼續,直到整個進程都滿了爲止。

垃圾回收是在第0代滿的時候發生的。使用代(generation)的機制的唯一目的就是提高性能。基本思路是,第0代是最近分配的對象,從未被垃圾回收算法檢查過。在一次垃圾回收中存活下來的對象被提升到另一代。如經過一次垃圾回收,第0代被提升爲第1代;第1代被提升爲第2代。

GC會檢查託管堆中是否有應用程序不再使用的對象。如果有,他們使用的內存就可以被回收,如果回收完畢後,仍然沒有可用內存,那麼new操作符會拋出一個OutOfMemoryException。

垃圾回收

有5種事件導致垃圾回收:

1.     第0代滿

2.     顯式調用GC.Collect

3.     Windows報告內存不足

4.     CLR卸載AppDomain

5.     CLR關閉(進程關閉)

CLR使用一個高優先級,專用的線程來調用Finalize方法。對於前四種情況,如果一個Finalize方法進入了無限循環,那麼這個特殊的線程就會被阻塞,其他Finalize方法就得不到調用。因爲應用程序永遠都不能回這些可終結對象的內存,只要程序還在運行,內存就會一直泄露。

對於第五種情況,每個Finalize方法大約有2秒的時間返回。如果2秒之內沒有返回,CLR將直接殺死該進程。另外調用所有Finalize方法的時間超過了40秒,CLR也會殺死進程。這些值在將來有可能會發生改變。

即使實例的構造函數拋出了異常,實例的Finalize方法也會被調用,所以不應在Finalize方法中假定對象處於一致的狀態。

對於一個可終結對象,在其構造函數被調用之前,此對象將被放入終結列表(finalization list)中。該列表是一個由GC控制的數據結構,它包含了所有可被終結的對象。在垃圾回收開始後,GC掃描終結列表,將垃圾對象從列表中移除(因爲非垃圾對象會被標記,所以垃圾對象就是那些沒有被標記的對象),然後將其添加到Freachable隊列中,該隊列是CLR的另一個內部數據結構。其中的每一個對象的Finalize方法都已經準備好被調用。當Freachable隊列爲空時,負責調用Finalize方法的線程會睡眠。當隊列進入記錄項,線程被喚醒,每一項都會被移除並調用Finalize方法。因此在Finalize方法中不應該對執行線程做任何假設。

雖然System.Object類型定義了Finalize方法,但是CLR知道忽略它。

當對象被添加到Freachable隊列時,這個對象(及其引用的對象)復活了,因爲對象現在是可達的了。現在GC不再認爲對象是垃圾,所以GC不會回收這些對象的內存。此時特殊的線程清空Freachable隊列並調用Finalize方法。當下一次垃圾回收的時候,會發現被終結的對象是垃圾對象,將會進行內存回收。整個過程中,需要兩次垃圾回收才能收回這些對象佔有的內存。實際情況下可終結對象會被提升代,所以需要不止兩次回收才能收回這些對象佔有的內存。

調用GC.SupressFinalize方法,將會打開當前對象的一個標誌位,次標誌位被打開後,CLR就不會把對象添加到Freachable隊列。

大對象堆

任何大於或等於85000字節的對象都被視爲大對象,大對象從大對象堆中分配。大對象和小對象一樣終結和釋放,但是大對象永不壓縮。但是此行爲在4.5.1中可以更改。大對象總是屬於第二代,所以大對象很難被回收,所以儘量不要分配臨時大對象。

在4.5.1中,使用如下代碼壓縮大對象堆,每次壓縮完畢,GCSettings.LargeObjectHeapCompactionMode被重置爲默認值。還要注意的是,後臺垃圾回收器永遠不會壓縮大對象堆。

GCSettings.LargeObjectHeapCompactionMode =GCLargeObjectHeapCompactionMode.CompactOnce;

GC.Collect(); 

預測大量內存操作是否成功

實現一個算法時,可能需要事先知道該算法需要大量內存。如果直接執行算法,有可能拋出OutOfMemoryException,這種情況下,之前的工作算是白做了。.NetFramework提供System.Runtime.MemoryFailPoint類,它允許在消耗內存的算法開始之前檢測是否有充足內存。

創建 MemoryFailPoint 對象並指定下操作 (MB)需要使用MB 的內存位數。 如果沒有足夠的內存不可用, 將引發InsufficientMemoryException 異常。 MemoryFailPoint 運行在 16 MB 粒度。 所有值小於 16MB 視爲 16 MB,同時,其他值視爲 16 MB 的下一個更大的多個。

以下代碼保留了1G內存用於執行算法,

try

{

    using (MemoryFailPoint mfp =new MemoryFailPoint(1000))

    {

        //Do Anything needs 1G memory

    }

}

catch (InsufficientMemoryException)

{

    Console.WriteLine("InsufficientMemory");

}

如果MemoryFailPoint類的構造器沒有拋出異常,表明邏輯上已經保留了請求的內存。但是請求的物理內存尚未分配。這表示,算法只是更有可能獲得所需的內存並允許成功,並不表示一定會分配到所需的物理內存。此類只是幫助你寫更健壯的程序。

線程劫持

當GC開始內測回收的時候,所有線程都必須掛起,因爲這些線程不能再訪問內存中的對象。這是因爲GC線程要壓縮內存,壓縮過程中,對象的引用可能變得無效,所以只能在GC完成後,其他線程才能繼續執行。

GC開始的時候,CLR會對其他線程進行劫持,使其進入一個特殊的函數,而這個函數將會進入一個臨界區。所以如果在GC線程正在垃圾回收的時候,使用WinDbg查看進程中所有其他線程的堆棧,那麼回發現,所有這些線程都進入了同一個方法,並且都試圖獲取同一個臨界區。

並不是任何時刻都可以劫持線程,必須等待線程進入一個安全點。如果CLR創建了一個新對象,但是還沒有將對象賦值給變量,此時劫持線程就不可行的,或者說線程不在一個安全點上。若是此時劫持線程並進行垃圾回收,那麼新對象將被垃圾回收,而變量指向一個無效地址。

垃圾回收模式

垃圾回收模式包括工作站模式和服務器模式,使用如下配置節來配置模式,默認情況下使用工作站模式。

<runtime>

   <gcServer enabled="true"/>

 </runtime>

還可以在運行時獲取垃圾回收模式是否爲服務器模式,如下:

GCSettings.IsServerGC{get;}

該模式不能在運行時修改。

GC延遲模式

有四種延遲模式可供選擇

·        Batch

·        Interactive

·        LowLatency

·        SustainedLowLatency

該模式可以在運行時獲取或者設置,如下:

GCSettings.LatencyMode= GCLatencyMode.SustainedLowLatency;

這裏面有一些版本問題,第四種模式只被.Net Framework4.0支持,而且只能用於工作站模式,但是到了.Net Framework 4.5,也可以用於服務器模式。

.Net Framework提供了配置節用於配置延遲模式,如下:

<gcConcurrentenabled="true"/>

要注意的是,這是個bool值,而且名稱也不是gcLatencyMode。所以不能完全對應到四種延遲模式,具體的對應關係如下:

 

gcServer=true

gcServer=false

gcConcurrent =true

·         .Net4.5之前:Batch

·         .Net 4.5之後:SustainedLowLatency

 

·         .Net4.0之前:Interactive

·         .Net 4.0之後:SustainedLowLatency

 

gcConcurrent =false

Batch

Batch

對於服務器模式Interactive和LowLatency模式是無效的,但是在運行卻能看到LatencyMode是Interactive。這也許是.Net的Bug。

總結一下,工作站GC支持所有四種模式,服務器GC支持Batch和SustainedLowLatency模式。

對於四種延遲模式的具體作用,解釋如下:

·        Batch

Batch模式是全阻塞模式,一旦GC開始運行,所有非GC線程都會被掛起,直到垃圾回收結束。

·        Interactive

此模式是工作站GC的默認模式(在.Net 4.0之前)。GC會使用多個線程進行併發垃圾回收,但是此併發只針對第二代對象。第零代和第一代對象永遠使用全阻塞模式回收。具體的回收場景是,某個對象的創建引發了垃圾回收,GC使用創建此對象的線程回收第0,1代垃圾,同時有一個專用線程用於在後臺回收第二代垃圾。

·        LowLatency

此模式用於對時間敏感的進程。GC會全力避免回收第二代,所以應該在短時間內應用此模式,並在不需要此模式的時候設置回原來的模式。

·        SustainedLowLatency

作用域Interactive基本相同,但是可以應用在服務器GC模式。

區別是:Interactive模式的專用線程在GC的過程中,不允許發起另外一個GC過程,而且只能在內存段中剩餘的空間中分配內存。

SustainedLowLatency模式允許在後臺GC運行中啓動另一次針對第0和1代的GC過程,甚至允許創建另一個新段來進行內存分配。

工作站和服務器垃圾回收比較

以下是工作站垃圾回收的線程處理和性能注意事項:

·        回收發生在觸發垃圾回收的用戶線程上,並保留相同優先級。因爲用戶線程通常以普通優先級運行,所以垃圾回收器(在普通優先級線程上運行)必須與其他線程競爭 CPU 時間。

·        不會掛起運行本機代碼的線程。(針對後臺回收且只限於標記過程)

·        工作站垃圾回收始終用在只有一個處理器的計算機上,而不管 <gcServer> 設置如何。 如果您指定服務器垃圾回收,則CLR 會使用工作站垃圾回收,並禁用併發。

以下是服務器垃圾回收的線程處理和性能注意事項:

·        回收發生在以 THREAD_PRIORITY_HIGHEST 優先級運行的多個專用線程上。

·        爲每個 CPU(邏輯CPU)提供一個用於執行垃圾回收的專用線程和一個堆,並將同時回收這些堆。 每個堆都包含一個小對象堆和一個大對象堆,並且所有的堆都可由用戶代碼訪問。 不同堆上的對象可以相互引用。

·        因爲多個垃圾回收線程一起工作,所以對於相同大小的堆,服務器垃圾回收比工作站垃圾回收更快一些。

·        服務器垃圾回收通常具有更大的段。

·        服務器垃圾回收會佔用大量資源。例如,如果在一臺具有 4 個處理器的計算機上運行了12 個進程,則在它們都使用服務器垃圾回收的情況下,將有 48 個專用垃圾回收線程。在高內存加載的情況下,如果所有進程開始執行垃圾回收,則垃圾回收器將要計劃 48 個線程。如果運行應用程序的數百個實例,請考慮使用工作站垃圾回收並禁用併發垃圾回收。這可以減少上下文切換,從而提高性能。如果啓用後臺回收,啓用的線程將會更多。

如果要手動回收內存,那麼就可以指定是否使用後臺回收功能,如下:

public static void Collect(int generation,GCCollectionMode mode, bool blocking);

blocking參數爲false,則表示使用後臺回收功能。

如果要分配超過2G內存的對象,需要使用如下配置:

<gcAllowVeryLargeObjects  enabled="true|false" />

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章