.NET中棧和堆的比較(四)

 終於翻完了第四篇,本來每次都是週末發的,可惜上週末有些事兒沒忙過來,所以今天中午給補上來。不知道這套文章還能不能繼續了,因爲作者也只寫到了第四篇,連他都不知道第五篇什麼時候出得來...

原文出處
http://www.c-sharpcorner.com/UploadFile/rmcochran/csharp_memory_401282006141834PM/csharp_memory_4.aspx

可以參看該系列文章的前面部分內容:
Part I
http://agassi001.cnblogs.com/archive/2006/05/10/396574.html
Part II
http://agassi001.cnblogs.com/archive/2006/05/13/399080.html
Part III
http://www.cnblogs.com/agassi001/archive/2006/05/20/405018.html

儘管在.NET framework下我們並不需要擔心內存管理和垃圾回收(Garbage Collection),但是我們還是應該瞭解它們,以優化我們的應用程序。同時,還需要具備一些基礎的內存管理工作機制的知識,這樣能夠有助於解釋我們日常程序編寫中的變量的行爲。在本文中我們將深入理解垃圾回收器,還有如何利用靜態類成員來使我們的應用程序更高效。


* 更小的步伐 == 更高效的分配

爲了更好地理解爲什麼更小的足跡會更高效,這需要我們對.NET的內存分配和垃圾回收專研得更深一些。


* 圖解:

讓我們來仔細看看GC。如果我們需要負責"清除垃圾",那麼我們需要擬定一個高效的方案。很顯然,我們需要決定哪些東西是垃圾而哪些不是。
  
爲了決定哪些是需要保留的,我們首先假設所有的東西都不是垃圾(牆角里着的舊報紙,閣樓裏貯藏的廢物,壁櫥裏的所有東西,等等)。假設在我們的生活當中有兩位朋友:Joseph Ivan Thomas(JIT)和Cindy Lorraine Richmond(CLR)。Joe和Cindy知道它們在使用什麼,而且給了我們一張列表說明了我們需要需要些什麼。我們將初始列表稱之爲"根"列表,因爲我們將它用作起始點。我們需要保存一張主列表來記錄出我們家中的必備物品。任何能夠使必備物品正常工作或使用的東西也將被添加到列表中來(如果我們要看電視,那麼就不能扔掉遙控器,所以遙控器將被添加到列表。如果我們要使用電腦,那麼鍵盤和顯示器就得添加到列表)。

這就是GC如何保存我們的物品的,它從即時編譯器(JIT)和通用語言運行時(CLR)中獲得"根"對象引用的列表,然後遞歸地搜索出其他對象引用來建立一張我們需要保存的物品的圖表。

根包括:
  • 全局/靜態指針。爲了使我們的對象不被垃圾回收掉的一種方式是將它們的引用保存在靜態變量中。
  • 棧上的指針。我們不想丟掉應用程序中需要執行的線程裏的東西。
  • CPU寄存器指針。託管堆中哪些被CPU寄存器直接指向的內存地址上的東西必須得保留。

單擊顯示全圖,Ctrl+滾輪縮放圖片

在以上圖片中,託管中的對象1、3、5都被根所引用,其中1和5時直接被引用,而3時在遞歸查找時被發現的。像我們之前的假設一樣,對象1是我們的電視機,對象3是我們的遙控器。在所有對象被遞歸查找出來之後我們將進入下一步--壓縮。


* 壓縮

我們現在已經繪製出哪些是我們需要保留的對象,那麼我們就能夠通過移動"保留對象"來對託管進行整理。
單擊顯示全圖,Ctrl+滾輪縮放圖片

幸運的是,在我們的房間裏沒有必要爲了放入別的東西而去清理空間。因爲對象2已經不再需要了,所以GC會將對象3移下來,同時修復它指向對象1的指針。
單擊顯示全圖,Ctrl+滾輪縮放圖片

然後,GC將對象5也向下移,
單擊顯示全圖,Ctrl+滾輪縮放圖片

現在所有的東西都被清理乾淨了,我們只需要寫一張便籤貼到壓縮後的上,讓Claire(指CLR)知道在哪兒放入新的對象就行了。
單擊顯示全圖,Ctrl+滾輪縮放圖片

理解GC的本質會讓我們明白對象的移動是非常費力的。可以看出,假如我們能夠減少需要移動的物品大小是非常有意義的,通過更少的拷貝動作能夠使我們提升整個GC的處理性能。


*託管堆之外是怎樣的情景呢?

作爲負責垃圾回收的人員,有一個容易出現入的問題是在打掃房間時如何處理車裏的東西,當我們打掃衛生時,我們需要將所有物品清理乾淨。那家裏的檯燈和車裏的電池怎麼辦?

在一些情況下,GC需要執行代碼來清理非託管資源(如文件,數據庫連接,網絡連接等),一種可能的方式是通過finalizer來進行處理。
class Sample
{
    
~Sample()
    {
        
// FINALIZER: CLEAN UP HERE
    }
}

在對象創建期間,所有帶有finalizer的對象都將被添加到一個finalizer隊列中。對象1、4、5都有finalizer,且都已在finalizer隊列當中。讓我們來看看當對象2和4在應用程序中不再被引用,且系統正準備進行垃圾回收時會發生些什麼。
單擊顯示全圖,Ctrl+滾輪縮放圖片

對象2會像通常情況下那樣被垃圾回收器回收,但是當我們處理對象4時,GC發現它存在於finalizer隊列中,那麼GC就不會回收對象4的內存空間,而是將對象4的finalizer移到一個叫做"freachable"的特殊隊列中。
單擊顯示全圖,Ctrl+滾輪縮放圖片

有一個專門的線程來執行freachable隊列中的項,對象4的finalizer一旦被該線程所處理,就將從freachable隊列中被移除,然後對象4就等待被回收。
單擊顯示全圖,Ctrl+滾輪縮放圖片

因此對象4將存活至下一輪的垃圾回收。

由於在類中添加一個finalizer會增加GC的工作量,這種工作是十分昂貴的,而且會影響垃圾回收的性能和我們的程序。最好只在你確認需要finalizer時才使用它。

在清理非託管資源時有一種更好的方法:在顯式地關閉連接時,使用IDisposalbe接口來代替finalizer進行清理工作會更好些。


* IDisposable

實現IDisposable接口的類需要執行Dispose()方法來做清理工作(這個方法是IDisposable接口中唯一的簽名)。因此假如我們使用如下的帶有finalizer的ResourceUser類:
public class ResourceUser
{
    
~ResourceUser() // THIS IS A FINALIZER
    {
        
// DO CLEANUP HERE
    }
}

我們可以使用IDisposable來以更好的方式實現相同的功能
public class ResourceUser : IDisposable
{
    
#region IDisposable Members
    
public void Dispose()
    {
        
// CLEAN UP HERE!!!
    }
    
#endregion
}


IDisposable被集成在了using塊當中。在using()方法中聲明的對象在using塊的結尾處將調用Dispose()方法,using塊之外該對象將不再被引用,因爲它已經被認爲是需要進行垃圾回收的對象了。

public static void DoSomething()
{
    ResourceUser rec 
= new ResourceUser();
    
using (rec)
    {
        
// DO SOMETHING 
    } // DISPOSE CALLED HERE
    
// DON'T ACCESS rec HERE
}


我更喜歡將對象聲明放到using塊中,因爲這樣可視化很強,而且rec對象在using塊的作用域之外將不再有效。這種模式的寫法更符合IDisposable接口的初衷,但這並不是必須的。

public static void DoSomething()
{
    
using (ResourceUser rec = new ResourceUser())
    {
        
// DO SOMETHING

    } 
// DISPOSE CALLED HERE
}

在類中使用using()塊來實現IDisposable接口,能夠使我們在清理垃圾對象時不需要寫額外的代碼來強制GC回收我們的對象。


* 靜態方法

靜態方法屬於一種類型,而不是對象的實例,它允許創建能夠被類所共享的方法,且能夠達到"減肥"的效果,因爲只有靜態方法的指針(8 bytes)在內存當中移動。靜態方法實體僅在應用程序生命週期的早期被一次性加載,而不是在我們的類實例中生成。當然,方法越大那麼將其作爲靜態就越高效。假如我們的方法很小(小於8 bytes),那麼將其作爲靜態方法反而會影響性能,因爲這時指針比它指向的方法所佔的空間還大些。

接着來看看例子...

我們的類中有一個公共的方法SayHello():
class Dude
{
    
private string _Name = "Don";

    
public void SayHello()
    {
        Console.WriteLine(
this._Name + " says Hello");
    }
}

在每一個Dude類實例中SayHello()方法都會佔用內存空間。
單擊顯示全圖,Ctrl+滾輪縮放圖片

一種更高效的方式是採用靜態方法,這樣我們只需要在內存中放置唯一的SayHello()方法,而不論存在多少個Dude類實例。
因爲靜態成員不是實例成員,我們不能使用this指針來進行方法的引用。
class Dude
{
    
private string _Name = "Don";

    
public static void SayHello(string pName)
    {
        Console.WriteLine(pName 
+ " says Hello");
    }
}

單擊顯示全圖,Ctrl+滾輪縮放圖片

請注意我們在傳遞變量時上發生了些什麼(可以參看<第二部分>)。我們需要通過例子的看看是否需要使用靜態方法來提升性能。例如,一個靜態方法需要很多參數而且沒有什麼複雜的邏輯,那麼在使用靜態方法時我們可能會降低性能。


* 靜態變量:注意了!

對於靜態變量,有兩件事情我們需要注意。假如我們的類中有一個靜態方法用於返回一個唯一值,而下面的實現會造成bug:
class Counter
{
    
private static int s_Number = 0;
    
public static int GetNextNumber()
    {
        
int newNumber = s_Number;
        
// DO SOME STUFF        
        s_Number = newNumber + 1;
        
return newNumber;
    }
}

假如有兩個線程同時調用GetNextNumber()方法,而且它們在s_Number的值增加前都爲newNumber分配了相同的值,那麼它們將返回同樣的結果!

我們需要顯示地爲方法中的靜態變量鎖住讀/寫內存的操作,以保證同一時刻只有一個線程能夠執行它們。線程管理是一個非常大的主題,而且有很多途徑可以解決線程同步的問題。使用lock關鍵字能讓代碼塊在同一時刻僅能夠被一個線程訪問。一種好的習慣是,你應該儘量鎖較短的代碼,因爲在程序執行lock代碼塊時所有線程都要進入等待隊列,這是非常低效的。
class Counter
{
    
private static int s_Number = 0;
    
public static int GetNextNumber()
    {
        
lock (typeof(Counter))
        {
            
int newNumber = s_Number;
            
// DO SOME STUFF
            newNumber += 1;
            s_Number 
= newNumber;
            
return newNumber;
        }
    }
}


* 靜態變量:再次注意了!

靜態變量引用需要注意的另一件事情是:記住,被"root"引用的事物是不會被GC清理掉的。我遇到過的一個最煩人的例子:
class Olympics
{
    
public static Collection<Runner> TryoutRunners;
}

class Runner
{
    
private string _fileName;
    
private FileStream _fStream;
    
public void GetStats()
    {
        FileInfo fInfo 
= new FileInfo(_fileName);
        _fStream 
= _fileName.OpenRead();
    }
}

由於Runner集合在Olympics類中是靜態的,不僅集合中的對象不會被GC釋放(它們都直接被根所引用),而且你可能注意到了,每次執行GetStats()方法時都會爲那個文件開放一個文件流,因爲它沒有被關閉所以也不會被GC釋放,這個代碼將會給系統造成很大的災難。假如我們有100000個運動員來參加奧林匹克,那麼會由於太多不可回收的對象而難以釋放內存。天啦,多差勁的性能呀!


* Singleton

有一種方法可以保證一個類的實例在內存中始終保持唯一,我們可以採用Gof中的Singleton模式。(Gof:Gang of Four,一部非常具有代表性的設計模式書籍的作者別稱,歸納了23種常用的設計模式)
public class Earth
{    
private static Earth _instance = new Earth();    
private Earth() { }    
public static Earth GetInstance() { return _instance; }
}

我們的Earth類有一個私有構造器,所以Earth類能夠執行它的構造器來創建一個Earth實例。我們有一個Earth類的靜態實例,還有一個靜態方法來獲得這個實例。這種特殊的實現是線程安全的,因爲CLR保證了靜態變量的創建是線程安全的。這是我認爲在C#中實現singleton模式最爲明智的方式。


* .NET Framework 2.0中的靜態類

在.NET 2.0 Framework中我們有一種靜態類,此類中的所有成員都是靜態的。這中特性對於工具類是非常有用的,而且能夠節省內存空間,因爲該類只存在於內存中的某個地方,不能在任何情況下被實例化。


* 總結一下...

總的來說,我們能夠提升GC表現的方式有:

1. 清理工作。不要讓資源一直打開!儘可能地保證關閉所有打開的連接,清除所有非託管的資源。當使用非託管對象時,初始化工作儘量完些,清理工作要儘量及時點。

2. 不要過度地引用。需要時才使用引用對象,記住,如果你的對象是活動着的,所有被它引用的對象都不會被垃圾回收。當我們想清理一些類所引用的事物,可以通過將這些引用設置爲null來移除它們。我喜歡採用的一種方式是將未使用的引用指向一個輕量級的NullObject來避免產生null引用的異常。在GC進行垃圾回收時,更少的引用將減少映射處理的壓力。

3. 少使用finalizer。Finalizer在垃圾回收時是非常昂貴的資源,我們應該只在必要時使用。如果我們使用IDisposable來代替finalizer會更高效些,因爲我們的對象能夠直接被GC回收而不是在第二次回收時進行。

4. 儘量保持對象和它們的子對象在一塊兒。GC在複製大塊內存數據來放到一起時是很容易的,而複製中的碎片是很費勁的,所以當我們聲明一個包含許多其他對象的對象時,我們應該在初始化時儘量讓他們在一塊兒。

5. 最後,使用靜態方法來保持對象的輕便也是可行的。


下一次,我們將更加深入GC的處理過程,看看在你的程序執行時GC是如何發現問題並清除它們的。 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章