天天看點

讀書感受 - 軟體設計師 - 你必須知道的.NET (C#類型存儲方式分析)

      這幾天花了些時間,相對仔細的閱讀了《你必須知道的.NET》這本書,因為沒有多少時間,請大家在看該書的時候一定要了解内容,轉變成自己的經驗。下面僅做簡單的書評。

      該書詳細的介紹了C#類型的存儲配置設定問題,對于值類型和引用類型的存儲和類型的轉換,都用了大篇幅來進行說明,如果還想再詳細些,就得去看.net framework中的底層方法和機制了。其實它的這個存儲配置設定,不該說是C#,應該是.net這個架構的存儲配置設定方式。對于其它語言,比如VB.NET,VC++.NET也是一緻的,因為在該.NET架構下,任何語言都是編譯為IL。這個由底層的公共類型語言CTL來處理語言間的類型統一,即把各語言的不同類型統一成CTL中定義的某種類型。

      該書前面對于面向對象設計的思想描述得很精彩,強烈建議大家體會其中的滋味。

      在這裡還是補充一下,關于值類型和引用類型的存儲問題,看過的好幾本書都說得不夠具體,原理沒有詳細覆寫,且給下定義的時候容易誤導讀者。我在這裡做個相對全面的分析。(如果我這裡沒提到的有遺漏的地方,請大家留言補充,一起學習)

      “值類型執行個體存儲在棧空間裡“,”引用類型執行個體存儲在托管堆裡,由GC控制釋放“等等相關的說法,都是片面的,不完全正确。

      1、首先從命名空間說起。如果在源代碼中沒有指定命名空間,則代碼就處于C#的全局命名空間(Global Namespace)下。在所有命名空間下,才能定義結構、枚舉這兩個值類型,以及類類型、接口,委托等引用類型,即你需要用到的類型的所有聲明和結構定義(所有的類型都是一種資料結構)。這個由代碼編輯器做了限制,即文法限制。C# 程式是利用命名空間組織起來的,無論是在内部組織系統,還是在外部組織系統,C#程式是封閉在命名空間裡的。

      2、根據1中進行分析。

      類型定義于所有命名空間下,而資料是封裝在某個類中的,方法是定義,繼承,重寫,重載在某個類中的。是以,所有的類型執行個體,包括值類型執行個體和引用類型執行個體以及指針類型執行個體,隻存在于三個地方:某個類的字段;類靜态方法和對象動态方法中的局部變量;類靜态方法和對象動态方法的形式參數(即值類型的拷貝及引用類型的引用拷貝,實際上也是方法中的變量了)。 即對象的基本作用域。從作用域才能具體分析出類型的存儲方式。

      3、根據2進行分析。

      在這個大環境下,必須先從引用類型開始說明,因為對象隻存在于類中(作用域在類中),而類是引用類型。

      在程式運作時,CLR将所有使用到的類型在記憶體中配置設定一個空間來存儲,即類型定義的影像。所有在類中使用到的類型,都是這個空間裡存儲的類型的副本。比如int i=9;将根據該影像空間裡的int類型定義,在棧上建立一個int型的影像副本,同時将9這個值指派給該棧上的這個變量。這樣的好處是不需要去查找int型的定義,然後根據定義去建立記憶體空間,再對該空間進行操作。而是直接根據類型在記憶體中影像的資料結構來建立類型副本用以儲存對象。進而縮短了對象建立的過程和消耗的資源,但是也占用了記憶體空間來存儲那些雖然沒有使用到的類型。是以,在VS中,程式裡如果沒有使用到的程式集,則應該從該程式集的引用中删除,以減少程式集中存儲的程式集中繼資料的存儲(雖然這些程式集中的類型沒有被加載則不會被加載到記憶體中)。在VS2008中,右鍵菜單裡就有”移除未使用到的using“操作項,但是對于程式集引用的移除,還得手工處理。

           3.1 靜态類的字段和方法。

           靜态類因為沒有執行個體,且它的字段,方法都是靜态的,是以,在程式加載的時候,它已經在堆上配置設定一個空間來存儲其資料結構。是以,靜态類的字段,無論是值類型還是引用類型,無論是靜态和動态的,無論是隻讀的還是可讀可寫的,都是存儲在堆(加載堆 Loader Heap)上的。而它的方法,則映射存儲在配置設定的虛拟函數表裡,在調用方法的時候,CLR将該方法的原型複制到記憶體中運作。靜态類的加載效率上比動态類(對象)的效率高。

           3.2 動态類(對象)的字段和方法

           對象在建立時,将根據類定義原型,在堆上配置設定該原型的影像副本來建立資料結構,并初始化該副本,同時在棧上配置設定一個空間來儲存對該副本的引用。對象中的字段的存儲與靜态類一緻,都是存儲在堆(GC堆或大對象堆)上。對象的方法,也是映射在虛拟函數表中。因為對象在建立的時候,需要回溯該對象所有的父類,并複制父類的影像副本到自己的存儲空間裡并根據類的繼承性決定方法的覆寫,重寫等内容,是以,對象的建立是一個複雜的過程,消耗的資源比較大,是以就是為什麼要在程式中盡量減少的建立對象的原因。依賴注入使對象的初始化方式發生了改變,在此不贅述。

           3.3 類方法的局部變量和參數存儲

           無論是靜态類還是動态類,對于其類方法裡的形參,都由CLR在調用方法時配置設定對應的類型空間來進行存儲,是以類方法的參數,其實也是方法開辟的與形參名稱一緻的局部變量。對于類方法,CLR在調用時将該方法原型代碼複制到記憶體中的副本來運作。是以,方法運作時的局部變量,才是配置設定存儲在棧上的。是以方法的調用,會涉及到壓棧和出棧的概念。對于值類型,直接存儲在棧上;對于引用類型,在堆上配置設定類型的資料結構,并在棧上存儲該資料結構的引用;對于指針類型,同樣是直接存儲在棧上。

           3.4 總結

           類中的字段,因為配置設定在堆上,是以它受GC的控制(大的對象存儲在大對象堆(Large Object Heap)上,隻在GC完全回收時控制)。對于類方法中的局部變量,因為配置設定在棧上,是以不受GC控制,由作業系統處理(因為函數運作的機制)。對于局部變量,不需要手工釋放資源;對于類字段,由GC控制資源的釋放。當然,你的資料結構也不能建立得過多,否則将導緻記憶體資源不足,而導緻GC的效率低下。如果是這樣,則你将需要添加記憶體硬體了。

      4、值類型的裝箱記憶體存儲分析。

      通過裝箱操作,比如 int num=1;object obj = num;這裡,因為obj是引用類型,是以,将在GC堆上建立一個object類型的變量,存儲num的值的一個拷貝,即1,原num值将不變化,而obj這個變量,将占用棧上的空間,來存儲該GC堆上配置設定的這個空間的位址。是以,在裝箱後,對obj的操作,實際上是對GC堆上這個内容的操作。而不是值類型的對棧上的變量進行操作。是以裝箱操作将消耗系統資源(建立堆對象),且對該對象的操作消耗的資源比操作棧上的變量消耗的資源多(棧的效率相對來說比堆的效率高,因為棧直接對應棧位址裡的内容直接尋址擷取,而堆是通過引用間接尋址來擷取)。

      5、類型的ref和out記憶體存儲分析。

      對于值類型形參函數void Test(ref int  it){};來說,CLR将在調用函數的時候,将建立一個int變量it,并将該it變量添加到棧中,而該it變量存儲的是一個指向實際調用該函數的實參int型變量在棧中所對應的位址。即使用一個棧空間存儲棧空間裡的另一個位址,該位址儲存的是被調用的變量的值。

     而對于引用類型形參函數void Test(ref object obj)來說,同樣的,使用一個棧空間存儲棧空間裡的另一個位址,但是該位址儲存的是一個指向GC堆上變量的引用,即GC堆裡的一個記憶體位址。

     指針類型同樣使用一個棧空間存儲引用的位址,但是它這個位址可能是棧空間的位址,也可能是托管堆裡的位址。

      通過上述類型存儲的分析總結,可以在系統設計時根據值類型和引用類型的存儲方式知道其優缺點,進而為系統的存儲和運作性能提高奠定了基礎。

      注:上述分析基于VS2008中驗證,這裡不提供代碼,請大家自行編碼驗證,加深印象。ref和out因為必須使用固定的變量位址,無法使用指針類型來驗證,是以隻能通過IL中的代碼進行分析。 同樣的,因為無法擷取托管堆中的位址,是以也是根據使用指針類型來間接推斷對象的存儲問題。

     ps.由于本人水準有限,如果大家對上述内容存在異議,煩請留言以糾正,非常感謝。