該隨筆受啟發于《CLR Via C#(第三版)》第四章4.4運行時的相互聯系
一、內存分配的幾個區域
1、線程棧
局部變量的值類型 和 局部變量中引用類型的指針(或稱引用)會被分配到該區域上(引用類型的一部分內存被分配到該區域內)。
該區域由系統管控,不受垃圾收集器的控制。當所在方法執行完畢后,局部變量會自動釋放(引用類型只釋放指針,而不釋放指針指向的數據)。
堆棧的執行效率很高,但容量有限。
2、GC Heap(回收堆)
用于分配小對象(引用類型),如果引用類型的實例大小 小于85000個字節,則會被分配到該區域上。
3、LOH(Large Object Heap)
超過 85000個字節的大對象(引用類型)會被分配到該區域上。
LOH 和 GC Heap區別在于:當有內存分配或者回收時,垃圾收集器可能會對GC Heap進行壓縮,而 LOH 不會被壓縮,只是在垃圾回收時被回收。
二、棧楨(stack frame)
棧楨是實現函數調用的數據結構,從邏輯上看,棧楨是一個函數執行的環境(上下文),包括:函數參數、返回地址和函數局部變量。
注意:上述的“返回地址” 不是函數的返回值,而是完成函數調用后的CPU接下來要執行的代碼的位置。
更多參見: 維基百科 call stack
三、淺拷貝 和 深拷貝(Clone、或克隆)
淺拷貝和深拷貝是內存拷貝的兩種方式,在傳參的過程中通常會發生內存拷貝并且是淺拷貝,比如下面代碼、兩個參數就是兩次淺拷貝。
public void Test1() { string name = "test1"; int size = 1; Test2(name, size);// 兩次淺拷貝 } public void Test2(string pName, int pSize) { }淺拷貝 前后的內存結果如下圖所示:

淺拷貝對于值類型(例如:int),拷貝的是棧中的具體值。
淺拷貝對于引用類型(例如:string),拷貝的是棧中的引用、其新引用 所指向的 托管堆中的地址 還是原來的地址。
深拷貝和淺拷貝的區別在于對 堆中數據(即對象)的處理:
淺拷貝只拷貝棧中的引用,不拷貝堆中的對象,新引用指向原有對象。
深拷貝會拷貝棧中的引用,并且會在堆中創建新的對象,新對象的屬性值 “可能” 來源于原有對象(因為、在C#中實現深拷貝需要自己編寫額外的代碼,
即實現 ICloneable 接口,深拷貝的結果是不確定的。不過建議大家盡可能避免使用該接口),新引用地址指向新的對象。因此,深拷貝只針對于引用類型,
對于值類型沒有太多的意義。
四、Demo
如果說前三項是做鋪墊,那么該Demo算是正文了。
事先已經定義好的如下類:
public class Data { public string Name; public int Size; }假設代碼即將調用Test1函數:
public void Test1() { string name = "test1"; int size = 1; Data data = new Data() { Name = "test1", Size = 1 }; Test2(name, size, data); object obj = size; int temp = (int)obj; } public void Test2(string pName, int pSize, Data pData) { pName = "test2"; pSize = 2; pData.Name = "test2"; pData.Size = 2; pData = new Data() { Name = "test3", Size = 3 }; }此時線程棧和托管堆內的情況如下圖所示(圖1):

1、開始調用Test1函數,這時會向棧中壓入一個棧楨(stack frame)。
棧楨包含3部分數據:
1)函數參數,當然Test1函數沒有參數。
2)返回地址,在Test1函數中,以當前代碼環境為例該地址沒有什么實際意義不作說明。
3)函數局部變量,此時Test1函數中的所有的局部變量都會被壓入棧。 有如下五個局部變量會被壓入棧:
string name, int32 size, Data data, object obj, int32 temp。(當然實際是六個局部變量,第六個是編譯器自動生成的,在隨筆結尾處再做解釋。)
壓入一個棧楨后,內存結果如下圖所示(圖2):

2、接下來執行Test1函數的代碼,給name賦值,給size賦值,給data賦值。內存結果將會如下圖所示(圖3):

上圖中的紅線和編譯器生成的變量 將在隨筆結尾出做解釋。
3、接下來將調用Test2函數,這時還會向棧中壓入一個棧楨。
棧楨包含3部分數據:
1)函數參數,Test2函數有三個參數,三個參數都是淺拷貝。
2)返回地址,該地址為調用Test2函數代碼位置的下一行(或者稱下一個指令)的位置,即:
obj = size; // 在調用Test1函數壓入棧楨的時候已經完成了 object obj; 的操作
完成函數調用后,將執行上述代碼。
3)函數局部變量,編譯器會自動生成一個變量(隨筆結尾處,將做出解釋)
故,此時的內存結果如下圖所示(圖4):

兩條橙色線是參數淺拷貝的結果。
4、 在執行完Test2函數的函數體,未返回Test1函數之前,內存結果如下圖所示(圖5):

紅線 將在隨筆結尾處做解釋。
黃線 需要注意,string是特殊的引用類型,其外在表現和值類型一致,但其本質上還是引用類型。由于string的不可變性,對string的每一次賦值
都會產生一個新的對象(如果新對象不存在),所以導致了黃線的出現。
淺綠線 我想這條線應該沒有問題吧。
5、 完成Test2函數的調用,代碼將返回到 Test2函數棧楨返回地址所示位置,在這一過程中 Test2函數棧楨 將被彈出,結果如下圖所示(圖6):

托管堆中的不被使用的對象將由GC進行自動回收,被回收的時間不確定。
6、 由3可以知,接下來將執行 obj = size; 的操作——裝箱,從字面上可以這樣理解:將值類型裝進引用類型的箱子里。
大致的操作過程如下:
1)首先、在堆中創建一個新的對象。(由于該操作導致裝箱操作的性能降低)
2)將size的值復制到對象中。
3)最后將obj的引用指向新創建的對象。
最后的結果如下圖所示(圖7):

7、最后一個操作——拆箱。
裝箱,是將值類型裝進引用類型的箱子里,在這一過程中,發生對象創建和內存復制。
然而拆箱,并沒有那么復雜,也沒有類似的“拆”的過程,只是將對象中的值類型讀取出來(最耗時的操作應該是尋找值類型所在的內存地址的操作),相對于裝箱,其性能要好很多。
最終結果如下圖所示(圖8):

五、 語法糖——對象初始化器
如下的兩個代碼片段是完全等效的:
Data temp = new Data(); // 編譯器自動生成的變量名不一定叫temp temp.Name = "test1"; temp.Size = 1; data = temp;
data = new Data() { Name = "test1", Size = 1 }; 第二種寫法,可以看作是一種簡寫,編譯器在編譯的時候會將第二種寫法的代碼進行轉化,轉化成第一種寫法。所以對象初始化器,C#3.0的語法新特性,
完全是一種語法糖,CLR沒有起到任何作用。
當看到這的時候,再想想上面的紅線 一切迎刃而解。
新聞熱點
疑難解答