C#的內存管理原理解析+標準Dispose模式的實現

本文內容是本人蔘考多本經典C#書籍和一些前輩的博文作的總結

儘管.NET運行庫負責處理大部份內存管理工做,但C#程序員仍然必須理解內存管理的工做原理,瞭解如何高效地處理非託管的資源,才能在很是注重性能的系統中高效地處理內存。
C#編程的一個優勢就是程序員沒必要擔憂具體的內存管理,垃圾回收器會自動處理全部的內存清理工做。用戶能夠獲得近乎像C++語言那樣的效率,而沒必要考慮像C++中複雜的內存管理工做。但咱們仍須要理解程序在後臺如何處理內存,纔有助於提升應用程序的速度和性能。程序員

先了解一下Windows系統中的虛擬尋址系統:

該系統把程序可用的內存地址映射到硬件內存中的實際地址上,在32位處理器上的每一個進程均可以使用4GB的硬件內存(64位處理器更大),這個4GB的內存包含了程序的全部部分(包括可執行代碼、代碼加載的全部DLL、程序運行時使用的全部變量的內容)
這個4GB的內存稱爲虛擬地址空間,或虛擬內存。其中的每一個存儲單元都是從0開始排序的。要訪問存儲在內存的某個空間中的一個值,就須要提供表示該存儲單元的數字。編譯器負責把變量名轉換爲處理器能夠理解的內存地址。數據庫

值類型和引用類型在C#中的數據類型分爲值類型和引用類型,對他們使用了不一樣但又類似的內存管理機制。編程

1.值數據類型的內存管理

在進程的虛擬內存中,有一個區域稱爲棧。C#的值類型數據、傳遞給方法的參數副本都存儲在這個棧中。在棧中存儲數據時,是從高內存地址向低內存地址填充的。
操做系統維護一個變量,稱爲棧指針。棧指針爲當前變量所佔內存的最後一個字節地址,棧指針會根據須要隨時調整,它老是會調整爲指向棧中下一個空閒存儲單元的地址。當有新的內存需求時,就根據當前棧指針的值開始往下來爲該需求分配足夠的內存單元,分配完後,棧指針更新爲當前變量所佔內存的最後一個字節地址,它將在下一次分配內存時調整爲指向下一個空閒單元。
如:int a= 10;
聲明一個整型的變量須要32位,也就是4個字節內存,假設當前棧指針爲89999,則系統就會爲變量a分配4個內存單元,分別爲89996~89999,以後,棧指針更新爲89995
double d = 20.13; //須要64位,也就是8個字節內存,存儲在89988~89995windows

棧的工做方式是先進後出(FIFO):在釋放變量時,老是先釋放後面聲明的變量(後面分配內存)。網絡

2.引用數據類型的內存管理

引用類型對象的引用存儲在棧中(佔4個字節的空間),而它的實際數據存儲在主託管堆或大對象堆上,託管堆是可用的4GB虛擬內存中的另外一個內存區域。
大對象堆:在.NET下,由於壓縮較大對象(大於85000個字節)很影響性能,因此爲它們分配了本身的託管堆。.NET垃圾回收器不對大對象堆執行壓縮過程。
如:Person arabel= new Person();
聲明變量arabel時,在棧上爲該變量分配4個字節的空間以存儲一個引用,new運算符爲對象Person對象在堆上分配空間,而後把該空間的地址賦給變量arabel,而構造函數則用來初始化。框架

.NET運行庫爲了給對象arabel分配空間,須要搜索堆,選取第一個未使用的且足夠容納對象全部數據的連續塊。但垃圾回收器程序在回收堆中全部無引用的對象後,會執行壓縮操做,即:把剩下的有用對象移動到堆的端部,挨在一塊兒造成一個連續的內存塊,並更新全部對象的引用爲新地址,同時更新堆指針,方便爲下一個新對象分配堆空間。ide

通常狀況下,垃圾回收器在.NET運行庫認爲須要它時運行。
System.GC類是一個表示垃圾回收器的.NET類,能夠調用System.GC.Collect()方法,強迫垃圾回收器在代碼的某個地方運行。
當代碼中有大量的對象剛剛取消引用,就比較適合調用垃圾回收器,但不能保證全部未引用的對象都能從堆中刪除。
垃圾回收器運行時,它實際上會下降程序的性能,由於在它執行期間,將會暫停應用程序的其它全部線程。函數

但.NET垃圾回收器使用了"世代垃圾回收器(generational)":

託管堆分爲幾個部分:第0代,第1代,第2代
全部新對象都被分配在第0代部分,在給新對象分配堆空間時,若是超出了第0代對應的部分的容量(),或者調用了GC.Collect()方法,就會開始進行垃圾回收。
每當垃圾回收器執行壓縮時,第0代部分留下來的對象將會被移動到第1代上,此時第0代部分就變成空,用來放置下一個新對象。
相似的,當第一代滿時,也會進行壓縮,剩下對象移到下一代。
託管堆有一個堆指針,功能和棧指針相似。性能

3.總結:

使用.Net框架開發程序的時候,咱們無需關心內存分配問題,由於有GC這個大管家給咱們料理一切。C#中棧是編譯期間就分配好的內存空間,所以你的代碼中必須就棧的大小有明確的定義;堆是程序運行期間動態分配的內存空間,你能夠根據程序的運行狀況肯定要分配的堆內存的大小優化

C#程序在CLR上運行的時候,內存從邏輯上劃分兩大塊:棧,堆。這倆基本元素組成咱們C#程序的運行環境
棧一般保存着咱們代碼執行的步驟,如 AddFive()方法,int pValue變量,int result變量等。而堆上存放的則可能是對象,數據等。咱們能夠把棧想象成一個接着一個疊放在一塊兒的盒子。當咱們使用的時候,每次從最頂部取走一個盒子。棧也是如此,當一個方法(或類型)被調用完成的時候,就從棧頂取走(called a Frame:調用幀),接着下一個。
堆則否則,像是一個倉庫,儲存着咱們使用的各類對象等信息,跟棧不一樣的是他們被調用完畢不會當即被清理掉(等待垃圾回收器來清理)。
棧內存無需咱們管理,也不受GC管理。當棧頂元素使用完畢,立馬釋放。而堆則須要GC(Garbage collection:垃圾收集器)清理。
當咱們的程序執行的時候,在棧和堆中分配有四種主要的類型:值類型,引用類型,指針,指令。

  • 值類型:在C#中,繼承自System.ValueType的類型被稱爲值類型,bool byte char decimal double enum float int long sbyte short struct uint ulong ushort`
  • 引用類型:繼承自System.Objectclass interface delegate object string
  • 指針:在內存區中,指向一個類型的引用,一般被稱爲「指針」,它是受CLR( Common Language Runtime:公共語言運行時)管理,咱們不能顯式使用。指針在內存中佔一塊內存區,它自己只表明一個內存地址(或者null),它所指向的另外一塊內存區纔是咱們真正的數據或者類型。

    值類型、引用類型的內存分配:
  • 引用類型老是被分配在堆上
  • 值類型和指針老是分配在被定義的地方,他們不必定被分配到棧上,若是一個值類型被聲明在一個方法體外而且在一個引用類型中,那它就會在堆上進行分配。

棧(Stack),在程序運行的時候,每一個線程(Thread)都會維護一個本身的專屬線程堆棧。
當一個方法被調用的時候,主線程開始在所屬程序集的元數據中,查找被調用方法,而後經過JIT即時編譯並把結果(通常是本地CPU指令)放在棧頂。CPU經過總線從棧頂取指令,驅動程序以執行下去。

當程序須要更多的堆空間時,GC須要進行垃圾清理工做,暫停全部線程,找出全部不可達到對象,即無被引用的對象,進行清理、壓縮。並通知棧中的指針從新指向地址排序後的對象。

4.釋放非託管的資源

有了垃圾回收器,意味着咱們只要讓再也不須要的對象的全部引用都超出做用域,並容許垃圾回收器在須要時釋放內存便可。
原則:在.net中,沒有必要調用Dispose的時候,你就不要調用它(垃圾回收器運行時會佔用/阻塞主線程)。
可是,垃圾回收器不知道如何釋放非託管的資源(如文件句柄、網絡鏈接、數據庫鏈接)。
在定義一個類時,有兩種機制來自動釋放非託管的資源:(更保險的作法是同時使用兩種機制,防止忘記調用Dispose()方法)

  1. 聲明一個析構函數(終結器);
  2. 爲類實現System.IDiposable接口,實現Dispose()方法;

5.析構函數:

C#編譯器在編譯析構函數時,它會隱式地把析構函數編譯爲等價於Finalize()方法,從而確保執行父類的Finalize()方法。
定義方式以下:析構函數無返回值、無參數、無訪問修飾符

class MyClass
{
    ~MyClass()
    {
    }
}
//如下版本是編譯析構函數實際調用的等價代碼:
protected override void Finalize()
{
    try
    { //釋放自身資源 }
    finally
    { base.Finalize(); }
}
析構函數的缺點:

因爲C#使用垃圾回收器的工做方式,沒法肯定C#對象的析構函數什麼時候執行。
定義了析構函數的對象須要通過兩次垃圾回收處理才能被銷燬(第二次調用析構函數時才真正刪除對象),而沒有定義析構函數的對象反而只須要一次處理便可刪除。
若是頻繁使用析構函數,並且執行長時間的清理任務,會嚴重影響性能。

6.IDiposable接口:

因此,推薦經過爲類實現System.IDisposable接口,實現Dispose()方法,來替代析構函數。IDisposable接口定義的模式爲釋放非託管資源提供了肯定的機制,並避免了對垃圾回收器依賴的問題。
IDisposable接口聲明瞭Dispose()方法,無參數,無返回值。能夠爲Dispose()方法實現代碼來顯式地釋放由對象直接使用的全部非託管資源,並在全部也實現IDisposable接口的封裝對象中調用Dispose()方法。這樣,該方法能夠能夠精確地控制非託管資源的釋放。
注意:若是在Dispose()方法調用以前的運行代碼拋出了異常,則該方法就執行不到了,因此應該使用try...finally,並把Dispose()方法放在finally塊內,以確保它的執行。以下:

Person person = null;  //假設Person類實現了IDisposable接口
try
{
    person = new Person();
}
finally
{
    if(person != null)
    {
          person.Dispose();
    }
}

C#提供了using關鍵字語法,能夠確保在實現了IDisposable接口的對象的引用超出做用域時,在該對象上自動調用Dispose()方法,以下:

using ( Person person = new Person() )
{ ..... }

using語句後面是一對"()",其中是引用變量的聲明和實例化,該語句是其中的變量放在隨後的語句塊中,而且在變量超出做用域時,即便拋出異常,也會自動調用Dispose()方法。
而後,在須要捕獲其它異常時,使用try...finally的方式就會比較清晰。而經常爲Dispose()方法定義一個包裝方法Close(),這樣顯得更清晰明瞭(Close()方法內僅調用Dispose()方法)

爲了防止忘記調用Dispose()方法,更保險的作法是同時實現兩種機制:即實現IDisposable接口的Dispose()方法,也定義析構函數。

7.C#中標準Dispose模式的實現

摘要:C#程序中的Dispose方法,一旦被調用了該方法的對象,雖然尚未垃圾回收,但實際上已經不能再使用了。
先了解一下C#程序(或者說.NET)中的資源分類。簡單的說來,C#中的每個類型都表明一種資源,而資源又分爲兩類:

  • 託管資源:由CLR管理分配和釋放的資源,即由CLR裏new出來的對象;
  • 非託管資源:不受CLR管理的對象,windows內核對象,如文件、數據庫鏈接、套接字、COM對象等;
      毫無例外地,若是咱們的類型使用到了非託管資源,或者須要顯式釋放的託管資源,那麼,就須要讓類型繼承接口IDisposable。這至關因而告訴調用者,該類型是須要顯式釋放資源的,你須要調用個人Dispose方法。
      不過,這一切並不這麼簡單,一個標準的繼承了IDisposable接口的類型應該像下面這樣去實現。這種實現咱們稱之爲Dispose模式:
public class SampleClass : IDisposable
    {
        //演示建立一個非託管資源
        private IntPtr nativeResource = Marshal.AllocHGlobal(100);
        //演示建立一個託管資源
        private AnotherResource managedResource = new AnotherResource();
        private bool disposed = false;
 
        /// <summary>
        /// 實現IDisposable中的Dispose方法,用於手動調用
        /// </summary>
        public void Dispose()
        {
            //必須爲true
            Dispose(true);
            //通知垃圾回收機制再也不調用終結器(析構器)由於咱們已經本身清理了,不必繼續浪費系統資源
            //即:從等待終結的Finalize隊列中移除this
            GC.SuppressFinalize(this);
        }
 
        /// <summary>
        /// 不是必要的,提供一個Close方法僅僅是爲了更符合其餘語言(如C++)的規範
        /// </summary>
        public void Close()
        {
            Dispose();
        }
 
        /// <summary>
        /// 必須,以備程序員忘記了顯式調用Dispose方法
        /// </summary>
        ~SampleClass()
        {
            //必須爲false,跳過託管資源的清理,隻手動清理非託管的資源,垃圾回收器會自動清理託管資源
            Dispose(false);
        }
 
        /// <summary>
        /// 非密封類修飾用protected virtual
        /// 密封類修飾用private
        /// </summary>
        /// <param name="disposing"></param>
        protected virtual void Dispose(bool disposing)
        {
            if (disposed)
            {
                return;
            }
            if (disposing)
            {
                // 清理託管資源
                if (managedResource != null)
                {
                    managedResource.Dispose();
                    managedResource = null;
                }
            }
            // 清理非託管資源
            if (nativeResource != IntPtr.Zero)
            {
                Marshal.FreeHGlobal(nativeResource);
                nativeResource = IntPtr.Zero;
            }
            //讓類型知道本身已經被釋放
            disposed = true;
        }
        public void SamplePublicMethod()
        {
            //確保在執行對象的任何方法以前,該對象可用(未被釋放)
            if (disposed) 
            {
                throw new ObjectDisposedException("SampleClass", "SampleClass is disposed");
            }
            //在這裏可使用對象
        }
    }

  在Dispose模式中,幾乎每一行都有特殊的含義。
  在標準的Dispose模式中,咱們注意到一個以~開頭的方法:

/// <summary>
        /// 必須,以備程序員忘記了顯式調用Dispose方法
        /// </summary>
        ~SampleClass()
        {
            //必須爲false
            Dispose(false);
        }

  這個方法叫作類型的終結器。提供終結器的所有意義在於:咱們不能奢望類型的調用者確定會主動調用Dispose方法,基於終結器會被垃圾回收器調用這個特色,終結器被用作資源釋放的補救措施。
  一個類型的Dispose方法應該容許被屢次調用而不拋異常。鑑於這個緣由,類型內部維護了一個私有的布爾型變量disposed:
private bool disposed = false;
  在實際處理代碼清理的方法中,加入了以下的判斷語句:

if (disposed)
            {
                return;
            }
            //省略清理部分的代碼,並在方法的最後爲disposed賦值爲true
            disposed = true;

  這意味着類型若是被清理過一次,則清理工做將再也不進行。
  應該注意到:在標準的Dispose模式中,真正實現IDisposable接口的Dispose方法,並無實際的清理工做,它實際調用的是下面這個帶布爾參數的受保護的虛方法:

/// <summary>
        /// 非密封類修飾用protected virtual
        /// 密封類修飾用private
        /// </summary>
        /// <param name="disposing"></param>
        protected virtual void Dispose(bool disposing)
        {
            //省略代碼
        }

  之因此提供這樣一個受保護的虛方法,是爲了考慮到這個類型會被其餘類繼承的狀況。若是類型存在一個子類,子類也許會實現本身的Dispose模式。受保護的虛方法用來提醒子類必須在實現本身的清理方法的時候注意到父類的清理工做,即子類須要在本身的釋放方法中調用base.Dispose方法。
  還有,咱們應該已經注意到了真正撰寫資源釋放代碼的那個虛方法是帶有一個布爾參數的。之因此提供這個參數,是由於咱們在資源釋放時要區別對待託管資源和非託管資源。
  在供調用者調用的顯式釋放資源的無參Dispose方法中,調用參數是true:

public void Dispose()
        {
            //必須爲true
            Dispose(true);
            //其餘省略
        }

  這代表,這個時候代碼要同時處理託管資源和非託管資源。
  在供垃圾回收器調用的隱式清理資源的終結器中,調用參數是false:

~SampleClass()
        {
            //必須爲false
            Dispose(false);
        }

  這代表,隱式清理時,只要處理非託管資源就能夠了。
  那麼,爲何要區別對待託管資源和非託管資源。在認真闡述這個問題以前,咱們須要首先弄明白:託管資源須要手動清理嗎?不妨先將C#中的類型分爲兩類,一類繼承了IDisposable接口,一類則沒有繼承。前者,咱們暫時稱之爲非普通類型,後者咱們稱之爲普通類型。
  非普通類型由於包含非託管資源,因此它須要繼承IDisposable接口,可是,這個包含非託管資源的類型自己,它是一個託管資源。因此說,託管資源須要手動清理嗎?這個問題的答案是:託管資源中的普通類型,不須要手動清理,而非普通類型,是須要手動清理的(即調用Dispose方法)。
  Dispose模式設計的思路基於:若是調用者顯式調用了Dispose方法,那麼類型就該循序漸進爲本身的因此資源所有釋放掉。若是調用者忘記調用Dispose方法,那麼類型就假定本身的全部託管資源(哪怕是那些上段中闡述的非普通類型)所有交給垃圾回收器去回收,而不進行手工清理。理解了這一點,咱們就理解了爲何Dispose方法中,虛方法傳入的參數是true,而終結器中,虛方法傳入的參數是false。

8.及時讓再也不須要的靜態字段的引用等於null:

在CLR託管應用程序中,存在一個根的概念,類型的靜態字段、方法參數以及局部變量均可以做爲根存在(值類型不能做爲根,只有引用類型的指針才能做爲根)。垃圾回收器會沿着線程棧上行檢查根,若是發現該根的引用爲空,則標記該根爲可被釋放。 而JIT編譯器是一個通過優化的編譯器,不管咱們是否爲變量賦值爲null,該語句都會被忽略掉,在咱們將項目設置爲Release模式下,該語句將根本不會被編譯進運行時內。 可是,在另一種狀況下,卻要注意及時爲變量賦值爲null。那就是類型的靜態字段。並且,爲類型對象賦值爲null,並不意味着同時爲該類型的靜態字段賦值爲null:當執行垃圾回收時,當類型的對象被回收的時候,該類型的靜態字段並無被回收(由於靜態字段是屬於類的,它往後可能會被該類型的其它實例繼續使用)。 實際工做中,一旦咱們感受到本身的靜態引用類型參數佔用內存空間比較大,而且使用完畢後再也不使用,則能夠馬上將其賦值爲null。這也許並沒必要要,但這絕對是一個好習慣。

相關文章
相關標籤/搜索