asp.net C#基礎知識之記憶體回收機制介紹

來源:互聯網
上載者:User

第一節 記憶體回收機制早期的C/C++開發中,一個對象的生命週期大概像這樣:計算對象大小——尋找可用記憶體——初始化對象——使用對象——摧毀對象。如果在上面的過程中,開發人員忘記了“摧毀對象”這一步驟,則很有可能導致記憶體泄露!這是一個非常可怕的事情!幸好,CLR的開發人員為我們解決了這一問題,在.NET Framework中引入了記憶體回收機制,使得開發人員不需要再過多地關注記憶體釋放的問題,CLR會在合適的時候進行執行記憶體回收來釋放不再使用的記憶體。這裡就像一個邪惡的男人所說的話:給我一個女人,我能創造一個民族!其實一個新世界你都可以去創造,前提是要有一個足夠大的星球記憶體來容納你的子孫!CLR就是這麼認為的。

在啟用一個進程時,CLR會先保留一塊連續的記憶體,在主線程啟動過程中,可能會初始化一系列對象,CLR先計算對象大小及其開銷所佔用的位元組數,接著會在連續的記憶體塊中為這些對象分配記憶體,這些對象被配置在第0代記憶體,在構造第0代記憶體的時候會分配一個預設大小的記憶體,隨著程式的運行,可能會初始化更多的對象,CLR發現第0代記憶體不能裝載更多的新生對象,此時CLR會啟動記憶體回收行程對第0代記憶體進行回收,不再使用的對象所佔用的記憶體會被釋放,接著把0代對象提升為第1代,然後把新生對象配置在第0代記憶體區中。CLR使用了3個階段的代,每次新分配的對象都會被配置在第0代記憶體中,最老的對象在第2代記憶體中,每次為新對象分配記憶體時,都可能會進行記憶體回收以釋放記憶體,很顯然CLR認為“記憶體永遠也使用不完”,很顯然CLR為我們自動管理了記憶體垃圾,很顯然CLR的這個“認為”在我們開發人員看來是不成立的,我們從以下幾個方面來解讀記憶體回收機制。

 

第二節 記憶體配置記憶體回收是對參考型別而言的。

CLR要求參考型別的對象從託管堆中分配記憶體的,實值型別是從棧中分配記憶體。在C#中通常使用new操作符來建立一個對象,編譯器將會在IL中產生newobj指令,執行一個newobj指令會有以下過程:(在前一節中我們已經知道,在一個進程啟動時會先保留一個連續的記憶體塊)先計算類型及其基底類型的欄位所需要的位元組數A,再計算類型對象的指標和一個同步索引塊共8或16個位元組,到此總共需要(A+8或18)位元組的記憶體,CLR會檢查當前進程區是否有足夠的記憶體來容納(A+8或16)個位元組的對象,如果有,則將新對象放其中,否則CLR進行記憶體回收,釋放不再使用的記憶體來容納新的對象,在整個進程的生命週期中,CLR會維護一個指標P,它一直指向當前進程所分配的最後一個對象記憶體的結尾處而不會跑出當前進程記憶體區邊界,如圖:

 

每次計算新的將要建立的對象所需要的位元組數時,CLR都是通過P加上新的需要的位元組數進行檢查可用記憶體區,如果超出了地址末尾,則表示當前的託管堆已經被用完,準備進行記憶體回收了。由於進程擁有一個獨立連續的記憶體區,所以CLR能保證建立的新對象基本上都是緊挨著放置的。

 

第三節 代當託管堆的記憶體被用完,新生的對象無處放置時,CLR就要開始進行記憶體回收了,隨著程式的持續運行,託管堆可能越來越大,如果要對整個託管堆進行記憶體回收(下面會講到如何回收),勢必會嚴重影響效能,因為有時可能僅僅需要數十個位元組就能容納新的對象,有時候可能要對可達的對象進行搬遷,為了小範圍有目的性地進行記憶體回收,CLR使用了“代”概念來最佳化記憶體回收行程,代是記憶體回收機制使用的一個邏輯技術,也是一種演算法,它把託管堆中的記憶體分為3個代(截止到目前.NET Framework4.0有3個代:0、1、2)。

進程在初始化時,CLR為託管堆初始化為包含0個對象的一塊記憶體地區,新添加到堆中的對象為第0代對象,CLR在初始化第0代記憶體區時會分配一個預設的配額,假設為512K,不同的.NET架構和版本,可能這個配額不相同。假設進程及其線程初始化完成後分配了4個對象,如下圖:

 

這4個對象佔據了512K的記憶體,程式繼續運行,當再分配第5個對象Obj5的時候,發現第0代已無可用記憶體,此時CLR會啟動記憶體回收行程進行記憶體回收,假如上面的Obj3已經無效,此是Obj3的記憶體會被釋放出來,接著搬遷Obj4對象到Obj3的位置(在Obj2的記憶體位址末尾處),存活下來的對象Obj1、Obj2和Obj4會被提升為第1代對象,第1代的記憶體地區根據程式啟動並執行情況,CLR可能會為其分配20M(也可能是其他值)大小的記憶體區,第0代記憶體暫時為空白,接著將Obj5分配到第0代記憶體區,如下:

 

程式繼續運行,並又新分配了4個對象Obj6-Obj9,且此時Obj2和Obj5都不再使用,即為不可達對象,此時需要再建立一個新對象Obj10,但發現第0代的512K記憶體已經用完,所以CLR再一次啟動記憶體回收行程進行記憶體回收,這一次記憶體回收行程會認為第0代的新對象生命週期短,所以先對第0代進行回收,並將存活對象提升到第1代中,記憶體回收行程發現此時第1代中的對象遠遠小於20M,所以放棄對第1代的回收,程式繼續運行,分配N多的新對象,當把第0代的對象提升到第1代,而第1代對象超20M時,則會對第1代的對象進行回收,第1代存活的對象被提升為第2代,第0代存活的對象被提升為第1代,如下圖:

 

每一次記憶體回收的過程,記憶體回收行程會根據實際使用方式自動調整第0、1、2代的預設配額大小,比如可能將第2代調整為200M,幾分鐘過後可能將其調整為120M,也有可能是1024M,程式繼續運行,當對3個全部進行了記憶體回收且重新調整配額後,可用記憶體還不足以放置新對象,CLR就會拋出OutOfMemoryException異常,此時活神仙也無法施救了。原來CLR認為“記憶體永遠也使用不完”也是有條件的啊!

 

第四節 記憶體回收過程託管堆中的一個對象,當線程中有變數對其引用則為可達對象,否則為不可達對象。

在一次記憶體回收過程開始時,記憶體回收行程會認為堆中的所有對象都是垃圾。

第一步是標記對象,記憶體回收行程沿著線程棧上行檢查所有根,靜態欄位、方法參數、活動中的局部變數以及寄存器指向的對象等都是根,當發現有根引用了託管堆中的對象A時,記憶體回收行程會對此對象A進行標記,在標記A時,如果檢測到對象A內又引用了另一個對象B,則也對B進行標記,對一個根檢測完畢後會接著檢測下一個根,執行同樣的標記過程,代碼中很有可能多個對象中引用了同一個對象C,記憶體回收行程只要檢測到對象C已經被標記過,則不再對對象C內所引用的對象進行檢測,以防止無限迴圈標記。有標記的對象就是可達對象,未標記的對象就是不可達對象。

第二步是搬遷對象壓縮堆,記憶體回收行程遍曆堆中的所有對象來尋找未標記的對象,因為未標記的對象是垃圾對象,可以進行回收,如果發現對象較小,則忽略,否則會先釋放這些垃圾對象所佔的記憶體,再把可達對象搬遷到這裡以壓縮堆,在搬遷可達對象之後,所有指向這些對象的變數將無效,接著記憶體回收行程要重新遍曆應用程式的所有根來修改它們的引用。在這個過程中如果各個線程正在執行,很可能導致變數引用到無效的對象地址,所以整個進程的正在執行Managed 程式碼的線程是被掛起的。

其實在記憶體回收行程準備開始一次回收時,正在執行Managed 程式碼的所有線程都必須被掛起,掛起時,CLR會記錄每個線程的指令指標以確定線程當前執行到哪裡以便將來在記憶體回收結束後進行恢複。如果一個線程的指令指標恰好到達了一個安全點,則可以掛起該線程,否則CLR會嘗試劫持該線程,如果還未到達安全點,則等待幾百毫秒後CLR會嘗試再一次劫持該線程,有可能經過多次嘗試,最終掛起該線程,噹噹前進程的所有執行Managed 程式碼的線程都掛起後,記憶體回收行程就可以開始工作了。(有關線程劫持可尋找相關資料)。記憶體回收行程回收完畢後,CLR恢複所有線程,程式繼續運行。可見,記憶體回收對效能影響之巨大!

 

第五節 大對象在建立新對象時,任何大於等於85000位元組的對象都被認為是大對象,這些對象的記憶體是從大對象堆中分配的,大對象總是被認為是第2代對象,要盡量避免分配大對象來減少效能損傷,為了提高效能,記憶體回收行程不對大對象進行搬遷壓縮,只在回收第2代記憶體時進行回收。

 

第六節 手工進行回收一般的情況下,CLR會智能地在必要的時候更行記憶體回收,但我們也可以在我們願意的情況下手動啟動記憶體回收行程,System.GC類提供了重載版本的靜態方法來啟動記憶體回收行程:

//對所有代進行記憶體回收。
GC.Collect();
//對指定的代進行記憶體回收。
GC.Collect(int generation);
//強制在 System.GCCollectionMode 值所指定的時間對零代到指定代進行記憶體回收。
GC.Collect(int generation, GCCollectionMode mode); 

在上一節中我們已經知道,每一次記憶體回收過程都會導致效能損傷,所以我們盡量避免調用這3個方法進行記憶體回收,當然必要的時候也可以調用。

不僅僅以上談到幾種情況下會啟動記憶體回收行程,當CLR接到Windwos發出記憶體告急通知時也會啟動記憶體回收、CLR卸載AppDomain時也會啟動記憶體回收。

執行個體

 

 代碼如下 複製代碼

//File: MyClass.cs  
using System;  
using System.Collections.Generic;  
using System.Text;  
 
namespace ConsoleApplication2  
{  
    class MyClass  
    {  
        ~MyClass()  
        {  
            Console.WriteLine("In MyClass destructor+++++++++++++++++++++++++++");  
        }  
    }  
}//File: MyAnotherClass.cs  
using System;  
using System.Collections.Generic;  
using System.Text;  
 
namespace ConsoleApplication2  
{  
    public class MyAnotherClass  
    {  
        ~MyAnotherClass()  
        {  
            Console.WriteLine("In MyAnotherClass destructor___________________________________");  
        }  
    }  
}//File: Program.cs  
using System;  
using System.Collections.Generic;  
using System.Text;  
 
namespace ConsoleApplication2  
{  
    class Program  
    {  
        static void Main(string[] args)  
        {  
            MyClass myClass = new MyClass();  
            MyAnotherClass myAnotherClass = new MyAnotherClass();  
            WeakReference myShortWeakReferenceObject = new WeakReference(myClass);  
            WeakReference myLongWeakReferenceObject = new WeakReference(myAnotherClass, true);  
            Console.WriteLine("Release managed resources by setting locals to null.");  
            myClass = null;  
            myAnotherClass = null;  
 
            Console.WriteLine("Check whether the objects are still alive.");  
            CheckStatus(myShortWeakReferenceObject, "myClass ", "myShortWeakReferenceObject");  
            CheckStatus(myLongWeakReferenceObject, "myAnotherClass", "myLongWeakReferenceObject");  
 
            Console.WriteLine("Programmatically cause GC.");  
            GC.Collect();  
 
            Console.WriteLine("Wait for GC runs the finalization methods.");  
            GC.WaitForPendingFinalizers();  
 
            //Check whether the objects are still alive.  
            CheckStatus(myShortWeakReferenceObject, "myClass ", "myShortWeakReferenceObject");  
            CheckStatus(myLongWeakReferenceObject, "myAnotherClass", "myLongWeakReferenceObject");  
 
            Console.WriteLine("Programmatically cause GC again. Let's see what will happen this time.");  
            GC.Collect();  
 
            //Check whether the objects are still alive.  
            CheckStatus(myShortWeakReferenceObject, "myClass ", "myShortWeakReferenceObject");  
            CheckStatus(myLongWeakReferenceObject, "myAnotherClass", "myLongWeakReferenceObject");  
 
            myAnotherClass = (MyAnotherClass)myLongWeakReferenceObject.Target;  
 
            Console.ReadLine();  
        }  
 
        static void CheckStatus(WeakReference weakObject, string strLocalVariableName, string strWeakObjectName)  
        {  
            Console.WriteLine(strLocalVariableName + (weakObject.IsAlive ? " is still alive." : " is not alive."));  
            Console.WriteLine(strWeakObjectName + (weakObject.Target != null ? ".Target is not null." : ".Target is null."));  
            Console.WriteLine();  
        }  
    }  

//File: MyClass.cs
using System;
using System.Collections.Generic;
using System.Text;

namespace ConsoleApplication2
{
    class MyClass
    {
        ~MyClass()
        {
            Console.WriteLine("In MyClass destructor+++++++++++++++++++++++++++");
        }
    }
}//File: MyAnotherClass.cs
using System;
using System.Collections.Generic;
using System.Text;

namespace ConsoleApplication2
{
    public class MyAnotherClass
    {
        ~MyAnotherClass()
        {
            Console.WriteLine("In MyAnotherClass destructor___________________________________");
        }
    }
}//File: Program.cs
using System;
using System.Collections.Generic;
using System.Text;

namespace ConsoleApplication2
{
    class Program
    {
        static void Main(string[] args)
        {
            MyClass myClass = new MyClass();
            MyAnotherClass myAnotherClass = new MyAnotherClass();
            WeakReference myShortWeakReferenceObject = new WeakReference(myClass);
            WeakReference myLongWeakReferenceObject = new WeakReference(myAnotherClass, true);
            Console.WriteLine("Release managed resources by setting locals to null.");
            myClass = null;
            myAnotherClass = null;

            Console.WriteLine("Check whether the objects are still alive.");
            CheckStatus(myShortWeakReferenceObject, "myClass ", "myShortWeakReferenceObject");
            CheckStatus(myLongWeakReferenceObject, "myAnotherClass", "myLongWeakReferenceObject");

            Console.WriteLine("Programmatically cause GC.");
            GC.Collect();

            Console.WriteLine("Wait for GC runs the finalization methods.");
            GC.WaitForPendingFinalizers();

            //Check whether the objects are still alive.
            CheckStatus(myShortWeakReferenceObject, "myClass ", "myShortWeakReferenceObject");
            CheckStatus(myLongWeakReferenceObject, "myAnotherClass", "myLongWeakReferenceObject");

            Console.WriteLine("Programmatically cause GC again. Let's see what will happen this time.");
            GC.Collect();

            //Check whether the objects are still alive.
            CheckStatus(myShortWeakReferenceObject, "myClass ", "myShortWeakReferenceObject");
            CheckStatus(myLongWeakReferenceObject, "myAnotherClass", "myLongWeakReferenceObject");

            myAnotherClass = (MyAnotherClass)myLongWeakReferenceObject.Target;

            Console.ReadLine();
        }

        static void CheckStatus(WeakReference weakObject, string strLocalVariableName, string strWeakObjectName)
        {
            Console.WriteLine(strLocalVariableName + (weakObject.IsAlive ? " is still alive." : " is not alive."));
            Console.WriteLine(strWeakObjectName + (weakObject.Target != null ? ".Target is not null." : ".Target is null."));
            Console.WriteLine();
        }
    }
}

 

記憶體回收機制要點整理

1. ASP.NET資源分託管資源和非託管資源,對於託管資源,.ASP.NET GC可以很好的回收無用的垃圾,而對於非託管(例如檔案訪問,網路訪問等)需要手動清理垃圾(顯式釋放)。
2. 非託管資源的釋放,ASP.NET提供了兩種方式:
2-1.Finalizer:寫法貌似C++的解構函式,本質上卻相差甚遠。Finalizer是對象被GC回收之前調用的終結器,初衷是在這裡釋放非託管資源,但由於GC運行時機的不確定性,通常會導致非託管資源釋放不及時。另外,Finalizer可能還會有意想不到的副作用,比如:被回收的對象已經沒有被其他可用對象所引用,但Finalizer內部卻把它重新變成可用,這就破壞了GC垃圾收集過程的原子性,增大了GC開銷。
2-2.Dispose模式:C#提供using關鍵字支援Dispose Pattern進行資源釋放。這樣能通過確定的方式釋放非託管資源,而且using結構提供了異常安全性。所以,一般建議採用Dispose Pattern,並在Finalizer中輔以檢查,如果忘記顯式Dispose對象則在Finalizer中釋放資源。
3. 託管資源的回收,判斷對象是否要被回收只要判定此對象或者其包含的子物件沒有任何引用是有效
4. GC的代價:一則喪失了託管資源回收的即時性,二是沒有把C#託管資源和非託管資源的管理統一起來,造成概念割裂
5. ASP.NET類型分兩大類:參考型別、實值型別,實值型別分配在棧上,不需要GC回收;參考型別分配在堆上,它的釋放和回收需要GC來完成。一個參考型別的對象要被回收,需要要成為垃圾
6. 系統為GC安排了獨立線程,對於記憶體回收GC採取了一定的優先演算法進行輪循回收記憶體資源
7. Generation(代),為了提高效能,越老的對象存活的越久。ASP.NET中一般分為三代,G0,G1,G2;G0最先被回收。

 


何時回收?

垃圾收集器周期性的執行記憶體清理工作,一般在以下情況出現時垃圾收集器將會啟動:

(1)記憶體不足溢出時,更確切地應該說是第0代對象充滿時。

(2)調用GC.Collect方法強制執行記憶體回收。

(3)Windows報告記憶體不足時,CLR將強制執行記憶體回收。

(4)CLR卸載AppDomain時,GC將對所有代齡的對象執行記憶體回收。

(5)其他情況,例如實體記憶體不足,超出短期存活代的記憶體段門限,運行主機拒絕分配記憶體等等。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在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.