.NET架構-記憶體管理story與變數建立和銷毀詳解

來源:互聯網
上載者:User

前言

.net運行庫通過記憶體回收行程自動處理回收託管資源,非託管的資源需要手動編碼處理。理解記憶體管理的工作原理,有助於提高應用程式的速度和效能。廢話少說,切入正題。主要闡述的概念見:


概念

 記憶體:又稱為虛擬記憶體,或虛擬位址空間,windows使用虛擬定址系統,在後台自動將可用的記憶體位址映射到硬體記憶體中的實際地址上,其結果便是32位處理器上的每個進程都可以使用4GB的記憶體,用來存放程式的所有部分,包括可執行代碼(exe檔案),代碼載入的所有DLL,程式運行時使用的所有變數的內容。
記憶體棧
 在進程的虛擬記憶體中,存在的一個變數的生存期必須嵌套的地區。
記憶體堆
 在進程的虛擬記憶體中,在方法退出後的很長一段時間內資料仍是可用的地區。
託管資源
 記憶體回收行程在後台能自動處理的資源
非託管資源
 需要手動編碼,通過解構函式,Finalize,IDisposable,Using等機制或方法處理的資源。

記憶體棧

 實值型別資料存放區在記憶體棧中,參考型別的執行個體地址值也放在記憶體棧中(見記憶體堆的討論),記憶體棧的工作原理,透過下面一段代碼理解:

{ //block1開始    int a;        //solve something    {//block2開始       int b;              // solve something else    }//block2結束}//block1結束

以上代碼注意2點:
 1)C#中變數的範圍,遵循先聲明的後超出範圍,後聲明的先超出範圍,即b先釋放,a後釋放,釋放順序總是與它們分配記憶體的順序相反。
 2)b在一個單獨的塊範圍(block2)中,而a所在的塊名稱為block1,其內嵌套著block2
 
 請看下面:



 棧記憶體管理中,始終都維護著一個棧指標,它始終指向站地區中下一個可用的地址,名字為sp,,假定它指向編號為1000的地址。
 變數a 首先入棧,假定機子是32位的,int型佔4個位元組,即997~1000,入棧後,sp指向996,可見記憶體棧的增長方向為從高地址向低地址方向。
 然後b入棧,佔據993~996,sp指向992。當超越塊block2 時,變數b立即釋放在記憶體棧上的儲存,sp增加4個位元組,指向996。
 向外走,超越塊block1 時,變數a 立即釋放,此時sp再增加4個位元組,指向原來的初始地址1000,後面再入棧時,這些地址再被佔用,然後再被釋放,迴圈往複。

記憶體堆

 儘管棧有非常高的效能,但對於所有的變數它還是不太靈活,因為位於記憶體棧上的變數的生存期必須嵌套。許多情況下,這種要求過於苛刻,因為我們希望有些資料在方法退出後的很長一段時間內還是可用的。
 只要是用new運算子來請求的堆儲存空間,就滿足資料聲明期延時性,例如所有的參考型別。在.net中使用託管堆來管理記憶體堆上的資料。
 .net中的託管堆和C++使用的堆不同,它在記憶體回收行程的控制下工作,而C++的堆是低級的。
 既然參考型別的資料存放區在託管堆上,那麼它們是如何儲存的呢?請看下面代碼
 

void Shout(){   Monkey xingxing; //猴子類   xingxing = new Monkey();}

  在這段代碼中,假定兩個類Monkey和AIMonkey,其中AIMonkey類擴充了Monkey對象。
  
  在這裡,我們稱Monkey為一個對象,稱xingxing為它的一個執行個體。
  
  首先,聲明了一個Monkey引用xingxing,在棧上給這個引用分配儲存空間,記住這僅是一個引用,而不是實際的Monkey對象。記住這一點很重要!!!
  然後看下第2行代碼:

xingxing = new Monkey();

  它完成的操作:首先,它分配堆上的記憶體,以儲存Monkey對象,注意了!!!這是一個真正的對象,它不是一個佔用4個位元組的地址!!! 假定Monkey對象佔用64個位元組,這64個位元組包含了Monkey執行個體的欄位,和.NET中用於識別和管理Monkey類執行個體的一些資訊。這64個位元組實在記憶體堆上分配的,假定記憶體堆上的地址1937~2000。new操作符返回一個記憶體位址,假定為997~1000,並賦值給xingxing。如下所示:



記住一點:
 與記憶體棧不同的是,堆上的記憶體是向上分配的,由低地址到高地址。
 從上面的例子中,可以看出建立引用執行個體的過程要比建立值變數的過程更複雜,系統開銷更大。那麼既然開銷這麼大,它到底優勢何在呢?引用資料類型強大到底在哪裡???
 
 請看下面代碼:

 {//block1    Monkey xingxing; //猴子類    xingxing = new Monkey();    {//block2      Monkey jingjing = xingxing; //jingjing也引用了Monkey對象      //do something    }    //jinjing超出範圍,它從棧中刪除    //現在只有xingxing還在引用Monkey}//xingxing超出範圍,它從棧中刪除//現在沒有在引用Monkey的了

  把一個引用執行個體的值xingxing賦值予另一個相同類型的執行個體jingjing,這樣的結果便是有兩個引用記憶體中的同一個對象Monkey了。當一個執行個體超出範圍時,它會從棧中刪除,但引用對象的資料還是保留在堆中,一直到程式終止,或記憶體回收行程回收它位置,而只有該資料不再有任何執行個體引用它時,它才會被刪除!
  隨便舉一個實際應用引用的簡單例子:
  

//從介面抓取資料放到list中List<Person> persons = getPersonsFromUI();//retrieve these persons from DBList<person> personsFromDB = retrievePersonsFromDB();//do something to personsFromDBgetSomethingToPersonsFromDB();

  請問對personsFromDB的改變,能在介面上及時相應出來嗎?
  不能!
 請看下面修改代碼:

//從介面抓取資料放到list中List<Person> persons = getPersonsFromUI();//retrieve these persons from DBList<Person> personsFromDB = retrievePersonsFromDB();int cnt = persons.Count;for(int i=0;i<cnt;i++){  persons[i]= personsFromDB [i] ;} //do something to personsFromDBgetSomethingToPersonsFromDB();

 修改後,資料能立即響應在介面上。因為persons與UI綁定,所有修改在persons上,自然可以立即響應。
  這就是引用資料類型的強大之處,在C#.NET中廣泛使用了這個特性。這表明,我們可以對資料的生存期進行非常強大的控制,因為只要保持對資料的引用,該資料就肯定位於堆上!!!
  這也表明了基於棧的執行個體與基於堆的對象的生存期不匹配!

記憶體回收行程 GC

   記憶體堆上會有片段形成,.NET記憶體回收行程會壓縮記憶體堆,移動對象和修改對象的所有引用的地址,這是託管的堆與非託管的堆的區別之一。
   .NET的託管堆只需要讀取堆積指標的值即可,但是非託管的舊堆需要遍曆地址鏈表,找出一個地方來放置新資料,所以在.NET下執行個體化對象要快得多。
  堆的第一部分稱為第0代,這部分駐留了最新的對象。在第0代記憶體回收過程中遺留下來的舊對象放在第1代對應的部分上,依次遞迴下去。。。

承上啟下

  以上部分便是對託管資源的記憶體管理部分,這些都是在後台由.NET自動執行的。下面看下非託管資源的記憶體管理,比如這些資源可能是UI控制代碼,network串連,檔案控制代碼,Image對象等。.NET主要通過三種機制來做這件事。分別為解構函式、IDisposable介面,和兩者的結合處理方法,以此實現最好的處理結果。下面分別看一下。

解構函式

  C#編譯器在編譯解構函式時,它會隱式地把解構函式的代碼編譯為等價於Finalize()方法的代碼,並確定執行父類的Finalize()方法。看下面的代碼:

public class Person{   ~Person()   {      //析構實現   }}

~Person()解構函式產生的IL的C#代碼:

protected override void Finalize(){   try   {      //析構實現   }   finally   {     base.Finalize();   }}

  放在finally塊中確保父類的Finalize()一定調用。
  C#解構函式要比C++解構函式的使用少很多,因為它的問題是不確定性。在銷毀C++對象時,其解構函式會立即執行。但由於C#使用記憶體回收行程,無法確定C#對象的解構函式何時執行。如果對象佔用了 寶貴的資源,而需要儘快釋放資源,此時就不能等待記憶體回收行程來釋放了。
  第一次調用解構函式時,有解構函式的對象需要第二次調用解構函式,才會真正刪除對象。如果頻繁使用析構,對效能的影響非常大。

IDisposable介面

  在C#中,推薦使用IDisposable介面替代解構函式,該模式為釋放非託管資源提供了確定的機制,而不像析構那樣何時執行不確定。
  假定Person對象依賴於某些外部資源,且實現IDisposable介面,如果要釋放它,可以這樣:

class Person:IDisposable{  public void Dispose()  {    //implementation  }}Person xingxing = new Person();//dom somethingxingxing .Dispose();

  上面代碼如果在處理過程中出現異常,這段代碼就沒有釋放xingxing,所以修改為:

Person xingxing = null;try{   xingxing  = new Person();   //do something}finally{   if(xingxing !=null)    {        xingxing.Dispose();    }}

  C#提供了一種文法糖,叫做using,來簡化以上操作。

using(Person xingxing = new Person()){  // do something}

  using在此處的語義不同於普通的引用類庫作用。using用在此處的功能,僅僅是簡化了代碼,這種文法糖可以少用!!!
  總之,實現IDisposable的對象,在釋放非託管資源時,必須手動調用Dispose()方法。因此一旦忘記,就會造成資源泄漏。如下所示:

                Image backImage = this.BackgroundImage;                                if (backImage != null)                {                    backImage.Dispose();                    SessionToImage.DeleteImage(_imageFilePath, _imageFileName);                                        this.BackgroundImage = null;                }

  在上面那個例子中,backImage已經確定不再用了,並且backImage又是通過Image.FromFile(fullPathWay)從物理磁碟上讀取的,是非託管的資源,所以需要Dispose()一下,這樣讀取Image的這個進程就被關閉了。如果忘記寫backImage.Dispose();就會造成資源泄漏!

結合 解構函式和IDisposable這2種機制

  一般情況下,最好的方法是實現兩種機制,獲得這兩種機制的優點。因為正確調用Dispose()方法,同時把實現解構函式作為一種安全機制,以防沒有調用Dispose()方法。請參考一種結合兩種方法釋放託管和非託管資源的機制:
  

public class Person:IDisposable{   private bool isDisposed = false;   //實現IDisposable介面   public void Dispose()   {         //為true表示清理託管和非託管資源      Dispose(true);            //告訴記憶體回收行程不要調用解構函式了      GC.SuppressFinalize(this);   }      protected virtual void Dispose(bool disposing)   {         //isDisposed: 是否對象已經被清理掉了      if(!isDisposed)      {                if(disposing)          {                      //清理託管資源           }                      //清理非託管資源       }       isDisposed = true;   }   ~Person()   {        //false:調用後只清理非託管資源     //託管資源會被記憶體回收行程的一個單獨線程Finalize()     Dispose(false);   }}

  當這個對象的使用者,直接調用了Dispose()方法,比如

Person xingxing = new Person();//do somethingperson.Dispose();

  此時調用IDisposable.Dispose()方法,指定應清理所有與該對象相關的資源,包括託管和非託管資源。

  如果未調用Dispose()方法,則是由解構函式處理掉託管和非託管資源。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.