大話 .Net 以內存管理

在一次偶然的機會中,我來到了恆生的你們庭。又在一次偶然的機會中,我很榮幸的被勇哥信任並讓我寫一篇季刊的文章。可能人生之中充滿了無數次的偶然機會,咱們只有抓住眼前的「偶然」,才能夠建立人生。當我接到這個任務的時候,有一些激動又有一些懼怕。激動的是我又有機會去分享本身知道的知識了,可是仍是有些懼怕,在恆生中大牛們太多了,寫什麼其實都是沒有什麼技術含量的。在激動和壓力之中,最終仍是寫下了這一篇文章。用此文章來激勵本身和那些當初選擇了c#而轉行的兄弟們。也許有一些地方說的不是很正確,但願讀者不吝賜教。個人我的郵箱是:codeany@163.com。html

讀者類型:java

這篇文章適應的讀者爲:未學習過c#、剛學c#、或者從未系統的學習過c#底層的程序員。c++

專業術語解析:程序員

       GC堆:Garbage Collection,在c#中,當沒有變量指向一塊GC堆的內存時,他不會當即把這塊內存回收,而是等到系統在合適的時間去回收這一塊內存。編程

       LOH堆:Large Object Heap,用於分配大對象實例。若是引用類型對象的實例大小不小於85000字節時,該實例將被分配到LOH堆上,不一樣於GC堆,垃圾收集器不會對LOH堆進行壓縮。c#

類型句柄:TypeHandle,TypeHandle指向相關連的類型的MethodTable。任何一個聲明瞭的類型都僅有一個MethodTable, 而且全部一樣類型對象的實例都指向同一份MethodTable。windows

SyncBlockIndex:指針指向Synchronization Block的內存塊,用於在多線程環境下對實例對象的同步操做。數組

       IL代碼:Intermediate Language,IL是.NET框架中中間語言的縮寫。在.NET中支持的語言有C#、VB、F#等等,可是這些高級語言,最終生成IL代碼,最後經過IL代碼解析器解析,從而實現多語言的開發。多線程

拆箱裝箱:在c#中,若是把值類型轉化爲引用類型叫作裝箱,反之若是把引用類型轉化爲值類型叫作拆箱。裝箱和拆箱的操做都是很是耗性能的,全部咱們平時編程的時候儘可能避免裝箱拆箱的操做。框架

簡單的new提及

提及 new 我想你們並不會陌生,雖然我沒有去深刻的學習過java、c、c++的new,可是我想在這裏和你們分享一下我自個認爲的c#的new。在new以前咱們仍是先建立一個class類,對於程序員來講,最好的解釋莫過於代碼,那我就直接看代碼吧:

/// <summary>

    /// 定義一個用戶信息的類

    /// </summary>

    public class UserInfo {

        public Int32 age = 12;          // 用戶的年齡

        public char sex = 'M';         // 用戶的性別

    }

 

    /// <summary>

    /// 用來定義一個用戶類

    /// </summary>

    public class Person {

        public Int32 id = 09397;           // 用戶的id號

        public UserInfo user;          // 用戶信息

    }

 

    /// <summary>

    /// 從用戶類中繼承獲得一個恆生用戶類

    /// </summary>

    public class HsPerson : Person {

        public bool isGood = true;      // 是不是優秀 這裏默認是優秀的

}

 

我想上面的代碼你們必定不會陌生,並且確定每一個人都會有多多少少寫過相似的代碼。那咱們今天也就從這些代碼轉入咱們的正題。首先咱們能夠考慮一個問題,咱們本身建立的類佔用了多少內存,而且在內存中是如何分配的。好吧,我想有的人必定會說這個還不簡單,直接調用sizeof來計算佔用空間大小不就解決問題了。其實當初我也是這麼想的,可是很是遺憾的告訴你,在c#中sizeof是計算值類型大小的。可是咱們本身建立的類是引用類型。因此問題並不會這麼簡單。可是咱們能夠手動的計算獲得咱們須要的答案。在計算以前咱們先來看一下什麼叫值類型、引用類型。

值類型與引用類型(參考微軟的MSDN):

值類型:

若是數據類型在它本身的內存分配中存儲數據,則該數據類型就是值類型。 值類型包括:

  • 全部數字數據類型
  • Boolean 、Char 和 Date
  • 全部結構,即便其成員是引用類型
  • 枚舉,由於其基礎類型老是 SByteShortIntegerLongByteUShortUInteger 或 ULong

每一個結構是值類型,所以,即便它包含引用類型成員。 所以,值類型 (如 Char 和 Integer 由 .NET framework 結構實現。

能夠經過使用保留關鍵字(例如 Decimal)聲明值類型。 也可使用 New 關鍵字初始化值類型。 這對於值類型有一個帶參數的構造函數的狀況尤其有用。此示例有 Decimal(Int32, Int32, Int32, Boolean, Byte) 構造函數,它從提供的部分生成新的 Decimal 值。

 

引用類型:

引用類型包含指向存儲數據的其餘內存位置的指針。 引用類型包括:

  • String
  • 全部數組,即便其元素是值類型
  • 類類型,如 Form
  • 委託

類是一種「引用類型」。 所以,諸如 Object 和 String 之類的引用類型都受 .NET Framework 類支持。 請注意,每一個數組都是一種引用類型,即便其成員是值類型。

在瞭解了值類型和引用類型以後,咱們迴歸到咱們的本文的正題。值類型的變量保存到內存的線程的堆棧中;而引用類型的變量會保存到託管堆中,其中這裏說的託管堆又能夠分爲GC堆、LOH堆。其中GC堆、LOH堆是根據建立的對象的大小來分配到不一樣的堆中的,判斷的平衡點是這個對象是否超過85000字節,若是小於85000字節,則系統把對象保存到GC堆中;若是大於或者等於85000字節,則系統保存到LOH堆中(通常LOH建立的對象是數組)。因此咱們常說的託管堆就是指GC堆和LOH堆的集合。固然,我這裏寫的也不是徹底正確的,其實在c#中建立對象是一個很是複雜過程,當中會涉及到系統程序域、共享程序域和默認程序域等等。我這麼寫只是想把問題簡單化,讓更多地人先了解一個大概的過程。

接下來咱們來開始計算一下第一個問題——對象佔用了多少空間,在UserInfo中咱們定義了一個int32的age(4byte),一個char的sex(2byte),在Person中咱們定義了int32的id(4byte),指針類型的user(4byte)類型,在HsPerson中咱們繼承了Person類,並添加了bool的isGood(1byte),因此咱們一共佔用了4+2+4+4+1=15byte。其實計算並無結束,實例對象所佔字節數還要加上對象附加成員所需的字節總數,其中附加成員包括TypeHandle和SyncBlockIndex,共計8Byte(在32位CPU平臺下面),因此咱們一共佔用了23byte字節。然而在堆上分配的內存老是按照4byte的整數倍,因此最後咱們能夠得知咱們在GC堆上要了24byte空間(咱們建立的對象通常是不會超過85000byte字節的,全部大部分都是在GC堆中,有一些特別大的數組byte[85000],此事CLR就會把它保存到LOH堆中)。第二個問題解答從里氏替換原則圖中找答案吧!

里氏替換內存管理

在上面的類的基礎上,咱們來建立幾個對象,來看看內存是怎麼分配的。

static void Main(string[] args)

{

// 完成一個簡單的 class 操做

    HsPerson hs = new HsPerson();

    hs.user = new UserInfo();

    Console.WriteLine(@"顯示信息1:

                          用戶{0}優秀

                          用戶id號是{1}

                          用戶年齡是{2}

                          用戶性別是{3}

                   ",hs.isGood==true?"是":"不是",hs.id,hs.user.age,hs.user.sex);

 

    // 引入一個難點 --氏替換原則

    Person hsperson = hs;

 

   //下面這樣子打印會出錯

    /*Console.WriteLine(@"顯示信息2:

                            用戶{0}優秀

                            用戶id號是{1}

                            用戶年齡是{2}

                            用戶性別是{3}",

hsperson.isGood == true ? "是":"不是",hsperson.id, hsperson.user.age, hsperson.user.sex);*/

   Console.WriteLine(@"顯示信息2:

                          {0}

                          用戶id號是{1}

                          用戶年齡是{2}

                          用戶性別是{3}

                        ", "這裏因爲變量hsperson指向變量範圍的控制,讀取不到isGood 字段", hsperson.id, hsperson.user.age, hsperson.user.sex);

           

     Console.ReadKey();

}

  

 

圖 里氏替換內存分配圖

這裏有一點但願你們不要誤解,就是我把Person類放到了HsPerson類中,這樣只是方便的讓讀者更好地理解里氏替換原則的一些細節(實際上Person類也是在一塊連續的GC堆中)。咱們仍是一邊從代碼分析,一邊從圖分析,首先咱們聲明瞭一個hs類型爲HsPerson的變量,這個變量會在線程堆棧中佔用4byte的空間,緊接着咱們用new在GC堆中建立了一個HsPerson的實例,其實咱們用「=」把這個建立的對象的實例的地址告訴了變量hs,此時變量已經初始化完成了。緊接着咱們建立了一個hsperson類型爲Person的類,而且從hs的變量中把地址拿過來也做爲本身的對象的地址。可是奇怪的問題發生了,咱們不能夠用hsperson去調用isGood這個字段。爲何呢?其實這個是變量的偏移量在搞鬼,當咱們建立變量的同時已經告訴了這個對象他能夠有多少的偏移量,hs他具備訪問全部的HsPerson這個對象的偏移量,而hsperson他只具備訪問在HsPerson中的Person的偏移量。可能你還不是很明白,那我這裏舉一個例子,假如咱們有一根1米的棒子,去摘樹上的桃子,那麼咱們就只能摘到1米如下的桃子,而當咱們去摘1米以上的桃子時,此時編譯器就會報錯,告訴咱們超越咱們能力了。(這裏咱們去掉了我的身高也去掉了有些特殊的方法去摘桃),因此當咱們想要摘到全部的桃子,咱們只須要有一根最高桃子樹高度的棒子就能夠了。可是這樣你的變量可能擁有了所有的訪問權限,可是在面向對象的編程之中,咱們提倡基於接口編程,這樣只須要對象訪問到本身高度的桃子並可以完成任務就能夠了。 固然里氏替換用的最爲普遍的應該是動態的調用對象了(因爲不是本文重點,就不一一展開了)。

原本還想寫一些IL代碼分析內存、拆箱裝箱內存知識,因爲時間有限,就先寫到這裏,若是你該興趣,我能夠和你在私下交流。寫了這麼多,我只是想告訴你在c#中對象的建立是個複雜的過程,主要包括內存分配和初始化兩個環節。

參考:

http://www.cnblogs.com/anytao/

http://msdn.microsoft.com/zh-tw/library/dd229211.aspx

http://www.cnblogs.com/dudu/

http://www.cnblogs.com/artech/archive/2010/10/20/CLR_Memory_Mgt_01.html

http://msdn.microsoft.com/zh-cn/library/x9h8tsay.aspx

http://msdn.microsoft.com/zh-cn/library/t63sy5hs

相關文章
相關標籤/搜索