[翻譯]理解C#對象生命周期


  看到網上的一篇講C#對象生命周期(Object Lifetime)的文章,通俗易懂,而且有圖,很適合初學者學習,就翻譯過來了。后來發現這是Pro C# 2010 and the .NET 4 Platform的第八章中的一部分。(感謝 大乖乖 提醒)。文中的專業名詞第一次出現時,括號里會標注對應的英文單詞。
  請尊重作者勞動,轉載請注明出處: http://www.cnblogs.com/Jack47/archive/2012/11/14/2770748.html

----2012年11月15日修改----

  找到了文章的出處,並添加了最后一部分代碼的截圖。

----正文-----

  .NET 對象是在一個叫做托管堆(managed heap)的內存中分配的,它們會被垃圾回收器(garbage collector)自動銷毀。
  在講解之前,你必須知道類(class),對象(object),引用(reference),棧(stack)和堆(heap)的意思。

  一個類只是一個描述這種類型的實例(instance)在內存中布局的藍圖。當然,類是定義在一個代碼文件中(在C#中代碼文件以.cs作為后綴)。

  一個簡單的Car 類,定義在一個叫做SimpleGC的C# Console Application中:

 1     //Car.cs
 2     public class Car
 3     {
 4         private int currSp;
 5         private string petName;
 6         public Car(){}
 7         public Car(String name, int speed)
 8         {
 9             petName = name;
10             currSp = speed;
11         }
12         public override string ToString(){
13             return string.Format("{0} is going {1} MPH", petName, currSp);
14         }        
15     }

    當一個類被定義好了,就可以使用C# new關鍵字來分配任意數量的這個類。

  需要理解的是,new關鍵字返回的是一個在堆上面的對象的引用,不是這個對象本身。這個引用變量存儲在棧上,以便之后在程序中使用。
    當想在某個對象上調用成員函數時,對存儲的這個對象的引用使用C# . 操作符:

1     class Program{
2         static void Main(string[] args){
3             //在托管堆上面創建一個新的Car對象。返回的是一個指向這個對象的引用
4             Car refToMyCar = new Car("Benz", 50);
5             //C# . 操作符用來在引用變量引用的對象上調用函數
6             Console.WriteLine(refToMyCar.ToString());
7             Console.ReadLine();
8         }    
9     }  

  下圖展示了類,對象和引用之間的關系

  對象生命周期的基礎知識

  當你在創建C#應用程序時,托管堆不需要你的直接干預。實際上,.NET 內存管理的黃金原則很簡單:
    使用new關鍵字在托管堆上申請一個對象
    一旦實例化后,當對象不再被使用的,垃圾回收器會銷毀它。
    對於讀者來說,下一個明顯的問題是:
    “垃圾回收器怎么確定托管堆中的對象是不再被使用?”
    簡潔的答案是:
     當你的代碼不再使用堆上面的這個對象,垃圾回收器會將這個對象刪除。

  假設你在程序的類里有一個方法分配了一個Car對象的局部變量:

1      static void MakeACar(){
2         //如果myCar是Car對象的唯一引用
3         //它可能會在這個方法返回時被銷毀
4         Car myCar = new Car();
5      }

   注意:Car對象的引用 (myCar) 是在MakeACar()函數中直接創建的並且沒有被傳遞到函數外部(通過一個返回值或者 ref/out 參數)。

  因此,一旦這個函數調用完成,myCar的引用不再可訪問,並且和這個引用相關聯的Car對象可以被垃圾回收了。但是,不能保證在MakeACar()函數調用完成后這個對象被立即從內存中銷毀。
     在此時只能假設當CLR 執行下次垃圾回收時,myCar對象能夠被安全的銷毀。

  The CIL of new

  當C#編譯器遇到new關鍵字,它會在函數實現中插入一個 CIL newobj指令。如果你編譯當前的例子代碼並使用ildasm.exe來瀏覽生成的代碼,你會在MakeACar()函數中發現如下的CIL語句:

  在我們知道托管堆中的對象什么時候被移除的確切條件之前,仔細查看一下CIL newobj指令的作用。

  首先,需要明白托管堆不僅僅是一個可由CLR訪問的隨機內存塊。.NET垃圾回收器是一個整潔的堆管家,出於優化的目的它會壓縮空閑的內存塊(當需要時)。為了輔助壓縮,托管堆會維護一個指針(通常被叫做下一個對象指針(the next object pointer)或者是新對象指針(new object pointer)),這個指針用來標識下一個對象在堆中分配的地址。( 譯者注:為了改進性能,運行時會在一個單獨的堆中為大型對象(>85,000Bytes)分配內存 。一般情況下都是數組,很少有這么大的對象。 垃圾回收器會自動釋放大型對象的內存。 但是,為了避免移動內存中的大型對象(耗時),不會壓縮此內存。 )

  這些信息表明,newobj指令通知CLR來執行下列的核心任務:

  • 計算要分配的對象所需的全部內存(包括這個類型的數據成員和類型的基類所需的內存)。
  • 檢查托管堆來確保有足夠的空間來放置所申請的對象。如果有足夠的空間,會調用這個類型的構造函數,構造函數會返回一個指向內存中這個新對象的引用,這個新對象的地址剛好就是下一個對象指針上一次所指向的位置。
  • 最后,在把引用返回給調用者之前,讓下一個對象指針指向托管堆中下一個可用的位置。

  下面的圖解釋了在托管堆上分配對象的細節。  

  由於你的程序忙着分配對象,在托管堆上的空間最終會滿。當處理newobj指令的時候,CLR 發現托管堆沒有足夠空間分配請求的類型時,它會執行一次垃圾回收來釋放內存。因此,垃圾回收的下一個規則也很簡單:

  如果托管堆沒有足夠的空間分配一個請求的對象,則會執行一次垃圾回收。

  當執行垃圾回收時,垃圾收集器臨時掛起當前進程中的所有的活動線程來保證在回收過程中應用程序不會訪問到堆。(一個線程是一個正在執行的程序中的執行路徑)。一旦垃圾回收完成,掛起的線程又可以繼續執行了。還好,.NET 垃圾回收器是高度優化過的。

  把對象引用置為null

  有了這些知識,你可能會想在C#里,把對象引用置為null會有什么事發生。
  例如,假設MakeACar()更新如下:

1     static void MakeACar(){
2         Car myCar = new Car();
3         myCar = null;
4     }
5     

  當你給對象引用賦值為null,編譯器會生成CIL代碼來確保這個引用(這個例子中是myCar)不會指向任何對象。如果還是用idasm.exe查看修改后MakeACar()的CIL 代碼,會發現ldnull這個操作碼(它會向虛擬執行棧上面壓入一個null)之后是一個 stloc.0操作碼(它在分配的Car對象上設置null的引用):  

  但是,你必須理解的是,設置引用為null不會強制垃圾回收器在此時啟動並從堆上刪除這個對象。你唯一完成的事是顯式地切斷了引用和它之前指向的對象之間的聯系。

  應用程序的根的作用(application roots)

  回到垃圾回收器如何決定一個對象是不再被使用的。為了理解細節,你需要知道應用程序根的概念。
  簡單來說,一個根是一個引用,這個引用指向堆上面的一個對象的。嚴格來說,一個根可以有以下幾種情況:

  • 指向全局對象(global objects)的引用(盡管C#不支持,但CIL代碼允許分配全局對象)
  • 指向任何靜態對象(static objects)/(static fields)
  • 指向一個應用程序代碼中的局部對象
  • 指向傳入到一個函數中的對象參數
  • 指向等待被終結(finalized)的對象
  • 任何一個指向對象的CPU寄存器

  譯者注:每個應用程序都有一組根。 

  在一次垃圾回收的過程中,運行環境會檢查托管堆上面的對象是否仍然是從應用程序根可到達的。為了檢查可達,CLR會建立一個代表堆上每個可達對象的圖。對象圖用來記錄所有可達的對象。同時,注意垃圾回收器絕不會在圖上標記一個對象兩次,因此避免了煩人的循環引用。

  假設托管堆上有名字為A,B,C,D,E,F和G的對象集合。在一次垃圾回收過程中,會檢查這些對象(同時包括這些對象可能包含的內部對象引用)是否是根可達的。一旦圖被建立起來,不可達的對象(在此是對象C和F)被標記為垃圾。

  下圖是上述場景的一個可能的對象圖(你可以把箭頭讀作依賴或者需要,例如"E依賴於G,間接依賴於B,“A不依賴任何對象”等)。

  創建的對象圖是用來決定哪些對象是應用程序根可達的。

  一旦一個對象已經被標記為終結(此例子中是C和F--在圖中沒有他倆),它在內存中就被清理掉了。在此時,堆上的剩余內存空間被壓縮(compact 翻譯為壓縮不太合適,但也不知道啥更好的詞了,壓縮后,分配的內存空間都是在一起,連續的),這會導致CLR修改活動的應用程序根集合(和對應的指針)來指向正確的內存位置(這個操作是自動透明的)。最后,調整下一個對象指針來指向下一個可用的內存位置。

  下圖闡明了清除和壓縮堆的過程。

  理解對象的代(object generations)

  在嘗試找到不可達的對象時,CLR並不是檢查托管堆上的每個對象。很明顯,這樣做會消耗大量時間,尤其在大型(例如現實中)程序中。

  為了幫助優化這個過程,堆上的每個對象被分配到一個特殊的"代”。代這個概念背后的想法很簡單:對象在堆上存活的時間越長,接下來它繼續存在的可能性也就越大,即較舊的對象生存期長,較新的對象生存期短。例如,實現Main()的對象一直在內存中,直到程序結束。相反,最近才被放到堆中的對象(例如在一個函數范圍里分配的對象)很可能很快就不可達。在堆上的每個對象屬於以下的某一個代:

  • Generation 0: 標識一個最近分配的還沒有被標記為回收的對象
  • Generation 1: 標識一個經歷了一次垃圾回收而存活下來的對象(例如,他被標記為回收,但由於堆空間夠用而沒有被清除掉)
  • Generation 2:標識一個經歷了不止一輪垃圾回收而存活下來的對象。

  垃圾回收器首先會檢查generation 0的所有對象。如果標記並清理這些對象(譯者注:因為新對象的生存期往往較短,並且期望在執行回收時,應用程序不再使用第 0 級托管堆中的許多對象)后產生了足夠使用的內存空間,任何存活下來的對象就被提升到Generation 1。為了理解一個對象的代如何影響回收的過程,可以查看下圖。下圖解釋了generation 0中一次垃圾回收后,存活的對象被提升的過程。  

  generation 0 中的存活對象被提升到generation 1

  如果所有的generation 0對象都被檢查了,但是產生的內存空間仍然不夠用,就檢查一遍generation 1中的所有對象的可達性並回收。存活下來的generation 1對象被提升到generation 2。如果垃圾回收器仍然需要額外的內存,generation 2的對象就經歷檢查並被回收。此時,如果一個generation 2的對象存活下來,它仍然是一個generation 2的對象。

  通過給堆中的對象賦予一個generation的值,新對象(比如局部變量)會被很快回收,而老一些的對象(如一個應用程序對象)不會被經常騷擾。
    為了解釋如何用System.GC類來獲得垃圾回收的細節信息,考慮下面的Main函數,里面用到了一些GC的成員函數。

 1     static void Main(string[] args){
 2         Console.WriteLine("*****Fun with System.GC *****");
 3         //打印出堆上面的大致字節數
 4         Console.WriteLine("Estimated bytes on heap:{0}",
 5         GC.GetTotalMemory(false));
 6         //MaxGeneration基於0,所以為了顯示而加一
 7         Console.WriteLine("This OS has{0} object generations.\n"
 8         (GC.MaxGeneration+1));
 9         Car refToMyCar = new Car("Zippy", 100);
10         Console.WriteLine(refToMyCar.ToString());
11         //打印出refToMyCar對象的generation
12         Console.WriteLine("Generation of refToMyCar is:{0}",
13         GC.GetGeneration(refToMyCar));
14         Console.ReadLine();
15     }
16     

  強制進行一次垃圾回收

.NET 垃圾回收器就是用來自動管理內存的。但是,在某些極端情況下,通過使用GC.Collect()來強制一次垃圾回收是有用的。尤其是:

  • 你的程序將要進入一段不想被可能的垃圾回收所中斷的代碼。
  • 你的程序剛完成分配大量數目的對象的過程,你想盡可能清理出更多內存來。

  如果你覺得讓垃圾回收器檢查不可達內存是十分有必要的,你像如下代碼一樣顯示觸發一次垃圾回收:

1     static void Main(string[] args){
2     //強制一次垃圾回收並等待每個對象被終結。
3     GC.Collect();
4     GC.WaitForPendingFinalizers();    
5    }

  當你手動強制執行一次垃圾回收,你應該調用GC.WaitForPendingFinalizers()。利用這個函數,你可以確保所有的等待被終結的對象在你的程序繼續往下執行之前擁有機會執行一些所需的清理工作。GC.WaitForPendingFinalizers()會在清理期間掛起調用它的線程。這是一個好事,它確保你的代碼不會在當前正要被銷毀的對象上調用函數。

  GC.Collect函數接收一個數字參數來標識在哪個generation上執行垃圾回收。例如,如果你想讓CLR只檢查generation 0上的對象,你需要這樣寫:

1         static void Main(string[] args){
2             //只檢查generation 0上的對象
3             GC.Collect(0);
4             GC.WaitForPendingFinializers();
5         }

  .Net 3.5中,Collect函數可以傳入一個值為GCCollectionMode的枚舉類型作為第二個參數,來精確定義執行環境如何執行垃圾回收。這個枚舉定義了如下的值:

1         public enum GCCollectionMode{
2             Default,//Fored是當前的默認值
3             Forced,//告訴執行環境立即執行回收
4             Optimized//讓運行環境來決定當前時間是否是清理對象的最佳時間
5         }

  像其他的垃圾回收一樣,調用GC.Collect()會提升存活下來的對象。假設Main函數代碼更新如下:

 1         static void Main(string[] args)
 2         {
 3             Console.WriteLine("***** Fun with System.GC *****");
 4             //打印出堆上的大致字節數
 5             Console.WriteLine("Estimated bytes on heap:{0}", GC.GetTotalMemory(false));
 6             //MaxGeneration基數為0.
 7             Console.WriteLine("This OS has {0} object generations.\n", (GC.MaxGeneration+1));
 8             Car refToMyCar = new Car("Zippy", 100);
 9             Console.WriteLine(refToMyCar.ToString());
10             //Print out generation of refToMyCar.
11             Console.WriteLine("\nGeneration of refToMyCar is:{0}", GC.GetGeneration(refOfMyCr));
12             //為了測試創建大量對象
13             object[] tonsOfObjects = new object[50000];
14             for(int i=0;i<50000;i++)
15                 tonsOfObjects[i] = new object();
16             //只回收gen0的對象
17             GC.Collect(0, GCCollectionMode.Forced);
18             GC.WaitForPendingFinalizers();
19             //打印出refToMyCar的generation
20             Console.WriteLine("Generation of refToMyCar is:{0}", GC.GetGeneration(refToMyCar));
21             //查看tonsOfObjects[9000] 是否還存活着
22             if(tonsOfObjects[9000]!=null)
23             {
24                 Console.WriteLine("Generation of tonsOfObjects[9000] is :{0}", GC.GetGeneration(tonsOfObjects[9000]));                
25             }
26             else 
27                 Console.WriteLine("tonsOfObjects[9000] is no longer alive.");
28             //打印出每個Generation經歷了多少次回收
29             Console.WriteLine("\nGen 0 has been swept {0} times", GC.CollectionCount(0));
30             Console.WriteLine("Gen 1 has been swept {0} times", GC.CollectionCount(1));
31             Console.WriteLine("Gen 2 has been swept {0} times", GC.CollectionCount(2));
32             Console.ReadLine();
33         }
34     

  這里,我們為了測試目的有意的創建了一個非常大的對象數組(50,000個)。你可以從下圖中看到輸出,盡管這個Main()函數只顯示調用了一次垃圾回收(通過GC.Collect()函數), 但CLR 在幕后執行了多次垃圾回收。  

  ---- 全文完

 


如果您看了本篇博客,覺得對您有所收獲,請點擊右下角的“推薦”,讓更多人看到!

資助Jack47寫作,打賞一個雞蛋灌餅錢吧
pay_weixin
微信打賞
pay_alipay
支付寶打賞


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM