首頁  >  文章  >  後端開發  >  .Net 垃圾回收機制原理(二)

.Net 垃圾回收機制原理(二)

黄舟
黄舟原創
2017-02-17 11:20:181375瀏覽

英文原文:Jeffrey Richter

編譯:趙玉開

連結http://www.php.cn/


上一篇文章介紹了.Net 垃圾回收的基本原理和垃圾回收Finalize內部機制;這篇我們看下弱引用對象,代,多線程垃圾回收,大對象處理以及和垃圾回收相關的性能計數器。
讓我們從弱引用物件說起,弱引用物件可以減輕大物件帶來的記憶體壓力。
弱引用(Weak References)
當程式的根物件指向一個物件時,這個物件是可達的,垃圾回收器不能回收它,這稱為對物件的強引用。和強引用相對的是弱引用,當一個物件上存在弱引用時,垃圾回收器可以回收此對象,但是也允許程式存取這個物件。這是怎麼回事?請往下看。

如果一個物件上僅存在弱引用,並且垃圾回收器在運行,這個物件就會被回收,之後如果程式中要存取這個對象,存取就會失敗。另一方面,要使用弱引用的對象,程式必須先對這個對象進行強引用,如果程式在垃圾回收器回收這個對象之前對對象進行了強引用,這樣(有了強引用之後)垃圾回收器就不能回收此物件了。這有點繞,讓我們用一段程式碼來說明一下:

void Method() {
//创建对象的强引用
Object o = new Object(); 
// 用一个短弱引用对象弱引用o.
WeakReference wr = new WeakReference(o);

o = null; // 移除对象的强引用

o = wr.Target; //尝试从弱引用对象中获得对象的强引用
if (o == null) {
// 如果对象为空说明对象已经被垃圾回收器回收掉了
} else {
// 如果垃圾回收器还没有回收此对象就可以继续使用对象了
}
}

為什麼需要弱物件呢?因為,有一些資料創建起來很容易,但是卻需要很多記憶體。例如:你有一個程序,這個程序需要訪問用戶硬碟上的所有資料夾和文件名;你可以在程序第一次需要這個數據時訪問用戶磁碟生成一次數據,數據生成之後你就可以訪問內存中的數據來得到使用者文件數據,而不是每次都去讀磁碟取得數據,這樣做可以提升程式的效能。

问题是这个数据可能相当大,需要相当大的内存。如果用户去操作程序的另外一部分功能了,这块相当大的内存就没有占用的必要了。你可以通过代码删除这些数据,但是如果用户马上切换到需要这块数据的功能上,你就必须重新从用户的磁盘上构建这个数据。弱引用为这种场景提供了一种简单有效的方案。
当用户切换到其他功能时,你可以为这个数据创建一个弱引用对象,并把对这个数据的强引用解除掉。这样如果程序占用的内存很低,垃圾回收操作就不会触发,弱引用对象就不会被回收掉;这样当程序需要使用这块数据时就可以通过一个强引用来获得数据,如果成功得到了对象引用,程序就没有必要再次读取用户的磁盘了。

WeakReference类型提供了两个构造函数:


WeakReference(object target);
WeakReference(object target, bool trackResurrection);

target参数显然就是弱引用要跟踪的对象了。trackResurrection参数表示当对象的Finalize方法执行之后是否还要跟踪这个对象。默认这个参数是false。有关对象的复活请参考这里。

方便起見,不追蹤復活對象的弱引用稱為「短弱引用」;而要追蹤復活對象的弱引用稱為「長弱引用」。如果物件沒有實作Finalize方法,那麼長弱引用和短弱引用是完全一樣的。強烈建議你盡量避免使用長弱引用。長弱引用允許你使用復活的對象,而復活對象的行為可能是不可以預測的。
一旦你使用WeakReference引用了一個對象,建議你將這個對象的所有強用都設置為null;如果強引用存在的話,垃圾回收器是永遠都不可能回收弱引用指向的對象的​​。
當你要使用弱引用目標物件時,你必須為目標物件建立一個強引用,這很簡單,只要用object a = weekRefer.Target;就可以了,然後你必須判斷a是否為空,弱不為空才可以繼續使用,弱為空表示物件已經被垃圾回收器回收了,得透過其他方法重新取得此物件。
弱引用的內部實現
從前文中的描述中我們可以推斷出弱引用對象肯定和一般對象的處理是不一樣的。一般情況下如果一個物件引用了另一個物件就是強引用,垃圾回收器就不能回收被引用的對象,而WeakReference物件卻不是這樣子,它所引用的物件是有可能被回收的。
要完全理解弱物件是如何運作的,我們還需要看一下託管堆。託管堆上有兩個內部資料結構他們的唯一作用是管理弱引用:我們可以把它們稱作長弱引用表和短弱引用表;這兩個表存放託管堆上的弱引用目標對象指針。
程式運作之初,這兩個表都是空的。當你建立一個WeakReference物件時,這個物件並不是分配到託管堆上的,而是在弱物件表中建立一個空槽(Empty Slot)。短弱引用物件被放在短弱物件表中,長弱引用物件被放在長弱引用表中。
一旦發現空槽,空槽的值會被設定成弱引用目標物件的位址;顯然長短弱物件表中的物件是不會當作應用程式的根物件的。垃圾回收器不會回收長短弱物件表中的資料。
讓我們來看看下垃圾回收執行時發生了什麼:
1. 垃圾回收器建立可達物件圖,建置步驟請參考上文
2. 垃圾回收器掃描短弱物件表,如果弱物件表中指向的對象沒有在可達對象圖中,那麼這個對象就被標識為垃圾對象,然後短對象表中的對象指針被設置為空
3. 垃圾回收器掃描終結隊列(參考上文),如果隊列中的對像不在可達對象圖中,這個對象從終結隊列中移動到Freachable隊列中,這時候,這個對象又被標識為可達對象,不再是垃圾了
4. 垃圾回收器掃描長弱引用表。如果表中的物件不在可達物件圖中(可達物件圖中包含在Freachable佇列中物件),則將長引用物件表中對應的物件指標設定為null
5. 垃圾回收器移動可達物件
一旦你了解垃圾回收器的工作過程,就很容易理解弱引用是如何運作了。存取WeakReference的Target屬性導致系統傳回弱物件表中的目標物件指針,如果是null,表示物件已經被回收了。
短弱引用不會追蹤復活,這意味著垃圾回收器可以在掃描終結隊列之前檢查弱引用表中指向的物件是否是垃圾物件。
而長弱引用追蹤復活對象,這意味著垃圾回收器必須在確認對象回收之後才可以將弱引用表中的指針設為null。
代:
提起.Net的垃圾回收,c++或c程式設計師可能就會想,這麼管理記憶體會不會出現效能問題呢。 GC的開發人員一直在調整垃圾回收器來提升它的效能。代是為了降低垃圾回收對效能影響的機制。垃圾回收器在工作時會假定如下說法是成立的:
1. 一個物件越新,那麼這個物件的生命週期就越短
2. 一個物件越老,那麼這個物件的生命週期就越長
3 . 新物件之間通常更可能和新物件之間存在引用關係
4. 壓縮堆的一部分要比壓縮整個堆要快
當然大量研究證明以上幾個假設在很多程序上是成立的。那就讓我們來談談這幾個假設是如何影響垃圾回收器的工作。
在程式初始化時,託管堆上沒有物件。這時候新添到託管堆上的對像是的代是0.如下圖所示,0代對像是最年輕的對象,他們從來沒有經過垃圾回收器的檢查。



圖1 託管堆上的0代對象

現在如果堆上添加了更多的對象,堆填滿時就會觸發垃圾回收。當垃圾回收器分析託管堆時,會建立一個垃圾物件(圖2中淺紫色區塊)和非垃圾物件的圖。所有沒有被回收的物件會被移動壓縮到堆的最底端。這些沒有被回收掉的對象就成為了1代對象,如圖2所示



圖2 託管堆上的0代1代對象

當堆上分配了更多的對象時,新物件被放在了0代區。如果0代堆填滿了,就會觸發一次垃圾回收。這時候活下來的物件成為1代物件被移動到堆的底部;再此發生垃圾回收後1代物件中存活下來的物件會提升為2代物件並被移動壓縮。如圖3所示:



圖3 託管堆上的0、1、2代物件

2代对象是目前垃圾回收器的最高代,当再次垃圾回收时,没有回收的对象的代数依然保持2.
垃圾回收分代为什么可以优化性能
如前所述,分代回收可以提高性能。当堆填满之后会触发垃圾回收,垃圾回收器可以只选择0代上的对象进行回收,而忽略更高代堆上的对象。然而,由于越年轻的对象生命周期越短,因此,回收0代堆可以回收相当多的内存,而且回收所耗的性能也比回收所有代对象要少得多。
这是分代垃圾回收的最简单优化。分代回收不需要便利整个托管堆,如果一个根对象引用了一个高代对象,那么垃圾回收器可以忽略高代对象和其引用对象的遍历,这会大大减少构建可达对象图的时间。
如果回收0代对象没有释放出足够的内存,垃圾回收器会尝试回收1代和0代堆;如果仍然没有获得足够的内存,那么垃圾回收器会尝试回收2,1,0代堆。具体会回收那一代对象的算法不是确定的,微软会持续做算法优化。
多数堆(像c-runtime堆)只要找到足够的空闲内存就分配给对象。因此,如果我连续分配多个对象时,这些对象的地址空间可能会相差几M。然而在托管堆上,连续分配的对象的内存地址是连续的。
前面的假设中还提到,新对象之间更可能存在相互引用关系。因此新对象分配到连续的内存上,你可以获得就近引用的性能优化(you gain performance from locality of reference)。这样的话很可能你的对象都在CPU的缓存中,这样CPU的很多操作就不需要去存取内存了。
微软的性能测试显示托管堆的分配速度比标准的win32 HeapAlloc方法还要快。这些测试也显示了200MHz的Pentium的CPU做一次0代回收时间可以小于1毫秒。微软的优化目的是让垃圾回收耗用的时间小于一次普通的页面错误。
使用System.GC类控制垃圾回收
类型System.GC运行开发人员直接控制垃圾回收器。你可以通过GC.MaxGeneration属性获得GC的最高代数,目前最高代是定值2.

你可以调用GC.Collect()方法强制垃圾回收器做垃圾回收,Collect方法有两个重载:

void GC.Collect(Int32 generation)

void GC.Collect()

第一个方法允许你指定要回收那一代。你可以传0到GC.MaxGeneration的数字做参数,传0只做0代堆的回收,传1会回收1代和0代堆,而传2会回收整个托管堆。而无参数的方法调用GC.Collect(GC.MaxGeneration)相当于整个回收。

在通常情况下,不应该去调用GC.Collect方法;最好让垃圾回收器按照自己的算法判断什么时候该调用Collect方法。尽管如此,如果你确信比运行时更了解什么时候该做垃圾回收,你就可以调用Collect方法去做回收。比如说程序可以在保存数据文件之后做一次垃圾回收。比如你的程序刚刚用完一个长度为10000的大数组,你不再需要他了,就可以把它设置为null然后执行垃圾回收,缓解内存的压力。
GC还提供了WaitForPendingFinalizers方法。这个方法简单的挂起执行线程,知道Freachable队列中的清空之后,执行完所有队列中的Finalize方法之后才继续执行。
GC还提供了两个方法用来返回某个对象是几代对象,他们是


Int32 GC.GetGeneration(object o);

Int32 GC.GetGeneration(WeakReference wr)

第一个方法返回普通对象是几代,第二个方法返回弱引用对象的代数。

下面的代码可以帮助你理解代的意义:

private static void GenerationDemo() {
// Let's see how many generations the GCH supports (we know it's 2)
Display("Maximum GC generations: " + GC.MaxGeneration);

// Create a new BaseObj in the heap
GenObj obj = new GenObj("Generation");

// Since this object is newly created, it should be in generation 0
obj.DisplayGeneration(); // Displays 0

// Performing a garbage collection promotes the object's generation
GC.Collect();
obj.DisplayGeneration(); // Displays 1

GC.Collect();
obj.DisplayGeneration(); // Displays 2

GC.Collect();
obj.DisplayGeneration(); // Displays 2 (max generation)

obj = null; // Destroy the strong reference to this object

GC.Collect(0); // Collect objects in generation 0
GC.WaitForPendingFinalizers(); // We should see nothing

GC.Collect(1); // Collect objects in generation 1
GC.WaitForPendingFinalizers(); // We should see nothing

GC.Collect(2); // Same as Collect()
GC.WaitForPendingFinalizers(); // Now, we should see the Finalize 
// method run

Display(-1, "Demo stop: Understanding Generations.", 0);
}
class GenObj{
public void DisplayGeneration(){
Console.WriteLine(“my generation is ” + GC.GetGeneration(this));
}

~GenObj(){
Console.WriteLine(“My Finalize method called”);
}
}

垃圾回收机制的多线程性能优化

在前面的部分,我解釋了GC的演算法和最佳化,然後討論的前提都是在單線程情況下的。而在真實的程式中,很可能是多個執行緒一起工作,多個執行緒一起操縱託管堆上的物件。當一個線程觸發了垃圾回收,其他所有的線程都應該暫停訪問任何引用對象(包括他們自己棧上引用的對象),因為垃圾回收器有可能要移動對象,修改對象的內存地址。
因此當垃圾回收器開始回收時,所有執行託管程式碼的執行緒必須掛起。運行時有幾種不同的機制可以安全的掛起執行緒來執行垃圾回收。這一塊的內部機制我並不打算詳細說明。但是微軟會持續修改垃圾回收的機制來降低垃圾回收帶來的效能損耗。
下面幾段描述了垃圾回收器在多執行緒情況下是如何運作的:
完全中斷程式碼執行 當垃圾回收開始執行時,掛起所有應用程式執行緒。垃圾回收器隨後將執行緒掛起的位置記錄到一個just-in-time(JIT)編譯器產生的表中,垃圾回收器負責將執行緒掛起的位置記錄在表中,記錄目前正在存取的對象,以及物件存放的位置(變數中,CPU暫存器中,等等)
劫持:垃圾回收器可以修改執行緒的堆疊讓返回位址指向一個特殊的方法,噹噹前執行的方法返回時,這個特殊的方法將會執行,掛起線程,這種改變線程執行路徑的方式稱為劫持線程。當垃圾回收完成之後,執行緒會重新回到之前執行的方法。
安全點:​​ 當JIT編譯器編譯一個方法時,可以在某個點插入一​​段程式碼判斷GC是否掛起,如果是,執行緒就掛起等待垃圾回收完成,然後執行緒重新開始執行。 JIT編譯器插入檢查GC程式碼的位置被稱為「安全點」
請注意,執行緒劫持允許正在執行非託管程式碼的執行緒在垃圾回收過程中執行。如果非託管程式碼不存取託管堆上的物件時這是沒有問題的。如果這個執行緒目前執行非託管程式碼然後回傳執行託管程式碼,這個執行緒將會被劫持,直到垃圾回收完成之後再繼續執行。
除了我剛提到的集中機制之外,垃圾回收器還有其他改進來增強多執行緒程式中的物件記憶體分配和回收。
同步釋放分配(Synchronization-free Allocations):在一個多執行緒系統中,0代堆被分成幾個區域,一個執行緒使用一個區域。這允許多執行緒同時分配對象,並不需要一個執行緒獨佔堆。
可伸縮回收(Scalable Collections):在多執行緒系統中執行執行引擎的伺服器版本(MXSorSvr.dll).託管堆會被分成幾個不同的區域,一個CPU一個區域。當回收初始化時,每個CPU執行一個回收線程,各個線程回收各自的區域。而工作站版本的執行引擎(MXCorWks.dll)則不支援這個功能。
大物件回收
這一塊就不翻譯了,有一篇專門的文章談這件事兒
監視垃圾回收
如果你安裝了.Net framework你的性能計數器(開始菜單—管理工具—性能進入)中就會有.Net CLR Memory一項,你可以從實例清單中選擇某個程式進行觀察,如下圖所示。



這些性能指標的具體意義如下:

# GC Handles(GC 處理數目)# Gen 0 Collections(第 2 級回收次數)

此計數器顯示最近的觀察所得值。 _Global_ 計數器值不準確,應該忽略。

顯示第 1 級中的目前位元組數;此計數器

性能計數器

說明

# Bytes in all Heaps(所有字節中的第一個位元組中的總數大小」計數器、「第1 級堆大小」計數器、「第2 級堆大小」計數器和「大物件堆大小」計數器。此計數器指示在垃圾回收堆上分配的當前記憶體(以位元組為單位)。

顯示正在使用的垃圾回收處理的當前數目。垃圾回收處理是對公共語言運作庫和託管環境外部的資源的處理。

# Gen 0 Collections(第 2 級回收次數)

顯示自應用程式啟動後第 0 級物件(即最年輕、最近分配的物件)被垃圾回收的物件)。

當第 0 級中的可用記憶體不足以滿足分配請求時發生第 0 級垃圾回收。此計數器在第 0 級垃圾回收結束時遞增。較高級的垃圾回收包括所有較低等級的垃圾回收。當較高級(第 1 級或第 2 級)垃圾回收發生時此計數器會明確遞增。

# Gen 1 Collections(第 2 級回收次數)

顯示自應用程式啟動後對第 1 級物件進行垃圾回收的次數。

此計數器在第 1 級垃圾回收結束時遞增。較高級的垃圾回收包括所有較低等級的垃圾回收。當較高級(第 2 級)垃圾回收發生時此計數器會明確遞增。

此計數器顯示最近的觀察所得值。 _Global_ 計數器值不準確,應該忽略。

# Gen 2 Collections(第 2 級回收次數)

顯示自應用程式啟動後對第 2 級物件進行垃圾回收的次數。此計數器在第 2 級垃圾回收(也稱為完整垃圾回收)結束時遞增。

此計數器顯示最近的觀察所得值。 _Global_ 計數器值不準確,應該忽略。

# Induced GC(引發的 GC 的數目)

顯示由於對 GC.Collect 的顯式調用而執行的垃圾回收的峰值次數。讓垃圾回收器對其回收的頻率進行微調是切實可行的。

# of Pinned Objects(釘住的物件的數目)

顯示上次垃圾回收中遇到的釘住的物件的數目。釘住的物件是垃圾回收器不能移入記憶體的物件。此計數器只追蹤被進行垃圾回收的堆中的釘住的物件。例如,第 0 級垃圾回收導致僅列舉第 0 級堆中釘住的物件。

# of Sink Blocks in use(正在使用的接收區塊的數目)

顯示正在使用的同步區塊的當前數目。同步區塊是為儲存同步資訊分配的基於物件的資料結構。同步區塊保留對託管物件的弱參考並且必須由垃圾回收器掃描。同步區塊不限於只儲存同步資訊;它們還可以儲存 COM interop 元資料。此計數器指示與同步基元的過度使用有關的效能問題。

# Total committed Bytes(提交位元組的總數)

顯示垃圾回收器目前提交的虛擬記憶體量(以位元組為單位)。提交的記憶體是在磁碟頁面檔案中保留的空間的實體記憶體。

# Total reserved Bytes(保留位元組的總數)

顯示垃圾回收器目前保留的虛擬記憶體量(以位元組為單位)。保留記憶體是為應用程式保留(但尚未使用任何磁碟或主記憶體頁)的虛擬記憶體空間。

% Time in GC(GC 中時間的百分比)

顯示自上次垃圾回收週期後執行垃圾回收所用運行時間的百分比。此計數器通常指示垃圾回收器代表該應用程式為收集和壓縮記憶體而執行的工作。只在每次垃圾回收結束時更新此計數器。此計數器不是一個平均值;它的值反映了最近觀察所得值。

Allocated Bytes/second(每秒分配的位元組數)

顯示每秒在垃圾回收堆上分配的位元組數。此計數器在每次垃圾回收結束時(而不是在每次分配時)進行更新。此計數器不是一段時間內的平均值;它顯示最近兩個樣本中觀測的值的差除以取樣間隔時間所得的結果。

Finalization Survivors(完成時存留物件數目)

顯示因正等待完成而從回收後保留下來的進行垃圾回收的物件的數目。如果這些物件保留對其他物件的引用,則那些物件也保留下來,但此計數器不會對它們計數。 「從第 0 級提升的完成記憶體」和「從第 1 級提升的完成記憶體」計數器表示因完成而保留的所有記憶體。

此計數器不是累積計數器;它在每次垃圾回收結束時由僅在該特定回收期間存留物件的計數更新。此計數器指示由於完成應用程式可能導致系統開銷過高。

Gen 0 heap size(第 2 級堆大小)

顯示在第 0 級中可以分配的最大位元組數;它不指示在第 0 級中目前分配的位元組數。

當自最近回收後的分配超出此大小時發生第 0 級垃圾回收。第 0 級大小由垃圾回收器進行微調並且可在應用程式執行期間​​變更。在第 0 級回收結束時,第 0 級堆的大小是 0 位元組。此計數器顯示呼叫下一個第 0 級垃圾回收的分配的大小(以位元組為單位)。

此計數器在垃圾回收結束時(而不是在每次分配時)進行更新。

Gen 0 Promoted Bytes/Sec(從第 1 級提升的位元組數/秒)

顯示每秒從第 0 級提升到第 1 級的位元組。內存在從垃圾回收保留下來後被提升。此計數器是每秒建立的在相當長時間保留下來的物件的指示符。

此計數器顯示在最後兩個樣本(以取樣間隔持續時間來劃分)中觀察到的值之間的差異。

Gen 1 heap size(第 2 級堆疊大小)

顯示第 1 級中的目前位元組數;此計數器

顯示第 1 級中的目前位元組數;此計數器不顯示第 1 級的最大大小。不直接在此代中分配物件;這些物件是從前面的第 0 級垃圾回收提升的。此計數器在垃圾回收結束時(而不是在每次分配時)進行更新。

Gen 1 Promoted Bytes/Sec(從第 1 級提升的位元組數/秒)🎜🎜🎜🎜🎜顯示每秒從第 1 級提升到第 2 級的位元數。在此計數器中不包括只因正等待完成而被提升的物件。 🎜

內存在從垃圾回收保留下來後被提升。不會從第 2 級進行任何提升,因為它是最舊的一級。此計數器是每秒建立的非常長時間保留下來的物件的指示符。

此計數器顯示在最後兩個樣本(以取樣間隔持續時間來劃分)中觀察到的值之間的差異。

Gen 2 heap size(第 2 級堆疊大小)

顯示第 2 級中當前位元組數。不直接在此代中分配物件;這些物件是在以前的第 1 級垃圾回收期間從第 1 級提升的。此計數器在垃圾回收結束時(而不是在每次分配時)進行更新。

Large Object Heap size(大物件堆大小)

顯示大物件堆的目前大小(以位元組為單位)。垃圾回收器將大於 20 KB 的物件視為大物件並且直接在特殊堆中分配大物件;它們不是透過這些層級提升的。此計數器在垃圾回收結束時(而不是在每次分配時)進行更新。

Promoted Finalization-Memory from Gen 0(從第1 級提升的完成內存)

顯示只因等待完成而從第00 級的內存數。此計數器不是累積計數器;它顯示在最後一次垃圾回收結束時觀察到的值。

Promoted Finalization-Memory from Gen 1(從第 1 級提升的完成記憶體)

顯示只因等待完成而從第 1 級提升到第 2 級的記憶體的位元組數。此計數器不是累積計數器;它顯示在最後一次垃圾回收結束時觀察到的值。如果最後一次垃圾回收就是第 0 級回收,則此計數器則重設為 0。

Promoted Memory from Gen 0(從第 1 級提升的記憶體)

顯示在垃圾回收後保留下來並且從第 0 級提升到第 1 級的記憶體的記憶體數字。此計數器中不包括那些只因等待完成而提升的物件。此計數器不是累積計數器;它顯示在最後一次垃圾回收結束時觀察到的值。

Promoted Memory from Gen 1(從第 1 級提升的記憶體)

顯示在垃圾回收後保留下來並且從第 1 級提升到第 2 級的記憶體的記憶體數字。此計數器中不包括那些只因等待完成而提升的物件。此計數器不是累積計數器;它顯示在最後一次垃圾回收結束時觀察到的值。如果最後一次垃圾回收就是第 0 級回收,則此計數器則重設為 0。

這個表來自MSDN

 以上就是.Net 垃圾回收機制原理(二)的內容,更多相關內容請關注PHP中文網(www.php.cn)! 


陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn