這幾天花了些時間,相對仔細的閱讀了《你必須知道的.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的控制(大的Object Storage Service在大對象堆(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.由於本人水平有限,如果大家對上述內容存在異議,煩請留言以糾正,非常感謝。