深入理解 c#堆疊和託管堆
堆疊stack
堆疊中儲存值型別。
堆疊實際上是向下填充,即由高記憶體地址指向低記憶體地址填充。
堆疊的工作方式是先分配記憶體的變數後釋放(先進後出原則)。
堆疊中的變數是從下向上釋放,這樣就保證了堆疊中先進後出的規則不與變數的生命週期起衝突!
堆疊的效能非常高,但是對於所有的變數來說還不太靈活,而且變數的生命週期必須巢狀。
通常我們希望使用一種方法分配記憶體來儲存資料,並且方法退出後很長一段時間內資料仍然可以使用。此時就要用到堆(託管堆)!
堆(託管堆)heap
堆(託管堆)儲存引用型別。
此堆非彼堆,.NET中的堆由垃圾收集器自動管理。
與堆疊不同,堆是從下往上分配,所以自由的空間都在已用空間的上面。
比如建立一個物件:
Customer cus;
cus = new Customer();
申明一個Customer的引用cus,在堆疊上給這個引用分配儲存空間。這僅僅只是一個引用,不是實際的Customer物件!
cus佔4個位元組的空間,包含了儲存Customer的引用地址。
接著分配堆上的記憶體以儲存Customer物件的例項,假定Customer物件的例項是32位元組,為了在堆上找到一個儲存Customer物件的儲存位置。
.NET執行庫在堆中搜索第一個從未使用的,32位元組的連續塊儲存Customer物件的例項!
然後把分配給Customer物件例項的地址賦給cus變數!
從這個例子中可以看出,建立物件引用的過程比建立值變數的過程複雜,且不能避免效能的降低!
實際上就是.NET執行庫儲存對的狀態資訊,在堆中新增新資料時,堆疊中的引用變數也要更新。效能上損失很多!
有種機制在分配變數記憶體的時候,不會受到堆疊的限制:把一個引用變數的值賦給一個相同型別的變數,那麼這兩個變數就引用同一個堆中的物件。
當一個應用變量出作用域時,它會從堆疊中刪除。但引用物件的資料仍然保留在堆中,一直到程式結束 或者 該資料不被任何變數應用時,垃圾收集器會刪除它。
裝箱轉化
using System;
class Boxing
{
public static void Main()
{ int i=110;
object obj=i;
i=220;
Console.WriteLine("i={0},obj={1}",i,obj);
obj=330;
Console.WriteLine("i={0},obj={1}",i,obj);
}
}
定義整數型別變數I的時候,這個變數佔用的記憶體是記憶體棧中分配的,第二句是裝箱操作將變數 110存放到了記憶體堆中,而定義object物件型別的變數obj則在記憶體棧中,並指向int型別的數值110,而該數值是付給變數i的數值副本。
所以執行結果是
i=220,obj=110
i=220,obj=330
記憶體格局通常分為四個區
全域性資料區:存放全域性變數,靜態資料,常量
程式碼區:存放所有的程式程式碼
棧區:存放為執行而分配的區域性變數,引數,返回資料,返回地址等,
堆區:即自由儲存區
值型別變數與引用型別變數的記憶體分配模型也不一樣。為了理解清楚這個問題,讀者首
先必須區分兩種不同型別的記憶體區域:執行緒堆疊(Thread Stack)和託管堆(Managed Heap)。
每個正在執行的程式都對應著一個程序(process),在一個程序內部,可以有一個或多
個執行緒(thread),每個執行緒都擁有一塊“自留地”,稱為“執行緒堆疊”,大小為1M,用於保
存自身的一些資料,比如函式中定義的區域性變數、函式呼叫時傳送的引數值等,這部分記憶體
區域的分配與回收不需要程式設計師干涉。
所有值型別的變數都是線上程堆疊中分配的。
另一塊記憶體區域稱為“堆(heap)”,在.NET 這種託管環境下,堆由CLR 進行管理,所
以又稱為“託管堆(managed heap)”。
用new 關鍵字建立的類的物件時,分配給物件的記憶體單元就位於託管堆中。
在程式中我們可以隨意地使用new 關鍵字建立多個物件,因此,託管堆中的記憶體資源
是可以動態申請並使用的,當然用完了必須歸還。
打個比方更易理解:託管堆相當於一個旅館,其中的房間相當於託管堆中所擁有的記憶體
單元。當程式設計師用new 方法建立物件時,相當於遊客向旅館預訂房間,旅館管理員會先看
一下有沒有合適的空房間,有的話,就可以將此房間提供給遊客住宿。當遊客旅途結束,要
辦理退房手續,房間又可以為其他旅客提供服務了。
從表 1 可以看到,引用型別共有四種:類型別、介面型別、陣列型別和委託型別。
所有引用型別變數所引用的物件,其記憶體都是在託管堆中分配的。
嚴格地說,我們常說的“物件變數”其實是類型別的引用變數。但在實際中人們經常將
引用型別的變數簡稱為“物件變數”,用它來指代所有四種類型的引用變數。在不致於引起
混淆的情況下,本書也採用了這種慣例。
在瞭解了物件記憶體模型之後,物件變數之間的相互賦值的含義也就清楚了。請看以下代
碼(示例專案ReferenceVariableForCS):
class A
02 {
03 public int i;
04 }
05 class Program
06 {
07 static void Main(string[] args)
08 {
09 A a ;
10 a= new A();
11 a.i = 100;
12 A b=null;
13 b = a; //物件變數的相互賦值
14 Console.WriteLine("b.i=" + b.i); //b.i=?
15 }
16 }
注意第12 和13 句。
程式的執行結果是:
b.i=100;
請讀者思索一下:兩個物件變數的相互賦值意味著什麼?
事實上,兩個物件變數的相互賦值意味著賦值後兩個物件變數所佔用的記憶體單元其內容
是相同的。
講得詳細一些:
第10 句建立物件以後,其首地址(假設為“1234 5678”)被放入到變數a 自身的4 個
位元組的記憶體單元中。
第12 句又定義了一個物件變數b,其值最初為null(即對應的4 個位元組記憶體單元中為
“0000 0000”)。
第13 句執行以後,a 變數的值被複制到b 的記憶體單元中,現在,b 記憶體單元中的值也為
“1234 5678”。
根據前面介紹的物件記憶體模型,我們知道現在變數a 和b 都指向同一個例項物件。
如果通過b.i 修改欄位i 的值,a.i 也會同步變化,因為a.i 與b.i 其實代表同一物件的同
一欄位。
整個過程可以用圖 9 來說明:
圖
圖 9 物件變數的相互賦值
由此得到一個重要結論:
物件變數的相互賦值不會導致物件自身被複制,其結果是兩個物件變數指向同一物件。
另外,由於物件變數本身是一個區域性變數,因此,物件變數本身是位於執行緒堆疊中的。
嚴格區分物件變數與物件變數所引用的物件,是面向物件程式設計的關鍵之一。
由於物件變數類似於一個物件指標,這就產生了“判斷兩個物件變數是否引用同一物件”
的問題。
C#使用“==”運算子比對兩個物件變數是否引用同一物件,“!=”比對兩個物件變數
22
是否引用不同的物件。參看以下程式碼:
//a1與a2引用不同的物件
A a1= new A();
A a2= new A();
Console.WriteLine(a1 == a2);//輸出:false
a2 = a1;//a1與a2引用相同的物件
Console.WriteLine(a1 == a2);//輸出:true
需要注意的是,如果“==”被用在值型別的變數之間,則比對的是變數的內容:
int i = 0;
int j = 100;
if (i == j)
{
Console.WriteLine("i與j的值相等");
}
理解值型別與引用型別的區別在面向物件程式設計中非常關鍵。
1、型別,物件,堆疊和託管堆
C#的型別和物件在應用計算機記憶體時,大體用到兩種記憶體,一個
叫堆疊,另一個叫託管堆,下面我們用直角長方形來代表堆疊,
用圓角長方形來代表託管堆。
首先討論一下方法內部變數的存放。
先舉個例子,有如下兩個方法,Method_1 和Add,分別如下:
public void Method_1()
{
int value1=10; //1
int value2=20; //2
int value3=Add(value,value); //3
}
public int Add(int n1,int n2)//4
{
rnt sum=n1+n2;//5
return sum;//6
}
這段程式碼的執行,用圖表示為:
上述的每個圖片,基本對應程式中的每個步驟。在開始執行Met
hod_1的時候,先把value1 壓入堆疊頂,然後是value2,接
下來的是呼叫方法Add,因為方法有兩個引數是n1 和n2,所以
把n1 和n2 分別壓入堆疊,因為此處是呼叫了一個方法,並且方
法有返回值,所以這裡需要儲存Add的返回地址,然後進入Ad
d方法內部,在Add內部,首先是給sum 賦值,所以把sum 壓
入棧項,然後用return 返回,此時,先前的返回地址就起到了
作用,return 會根據地址返回去的,在返回的過程中,把sum
推出棧頂,找到了返回地址,但在Method_1 方法中,我們希望
把Add的返回值賦給value3,此時的返回地址也被推出堆疊,
把value3 壓入堆疊。雖這個例子的結果在這裡沒有多大用途,
但這個例子很好的說明了在方法被執行時,變數與進出堆疊的情
況。這裡也能看出為什麼方法內部的局變數用過後,不能在其他
方法中訪問的原因。
其次來討論一下類和物件在託管堆和堆疊中的情況。
先看一下程式碼:
class Car
{
public void Run()
{
Console.WriteLine("一切正常");
}
public virtual double GetPrice()
{
return 0;
}
public static void Purpose()
{
Console.WriteLine("載人");
}
PDF 檔案使用 "pdfFactory Pro" 試用版本建立 fw w w . f i n e p rint.cn
}
class BMW : Car
{
public override double GetPrice()
{
return 800000;
}
}
上面是兩個類,一個Father一個Son,Son 繼承了Father,
因為你類中有一個virtual的BuyHouse 方法,所以Son類可以重
寫這個方法。
下面接著看呼叫程式碼。
public void Method_A()
{
double CarPrice;//1
Car car = new BMW();//2
CarPrice = car.GetPrice();//呼叫虛方法(其實呼叫的是重寫後
的方法)
car.Run();//呼叫例項化方法
Car.Purpose();//呼叫靜態方法
}
這個方法也比較簡單,就是定義一個變數用來獲得價格,同時
定義了一個父類的變數,用子類來例項化它。
接下來,我們分步來說明。
看一下執行時堆疊和託管堆的情部我
這裡需要說明的是,類是位於託管堆中的,每個類又分為四個
類部,類指標,用來關聯物件;同步索引,用來完成同步(比如線
程的同步)需建立的;靜態成員是屬於類的,所以在類中出現,還
有一個方法列表(這裡的方法列表項與具體的方法對應)。
當Method_A方法的第一步執行時:
這時的CarPrice 是沒有值的
當Method_A方法執行到第二步,其實第二步又可以分成
Car car;
car = new BMW();
先看Car car;
car在這裡是一個方法內部的變數,所以被壓到堆疊中。
再看 car = new BMW();
這是一個例項化過程,car變成了一個物件
這裡是用子類來例項化父型別。物件其實是子類的型別的,但
變數的型別是父類的。
接下來,在Method_A中的呼叫的中呼叫car.GetPrice(),
對於Car來說,這個方法是虛方法(並且子類重寫了它),虛方
法在呼叫是不會執行型別上的方法,即不會執行Car類中的虛方
法,而是執行物件對應類上的方法,即BMW中的GtPrice。
如果Method_A中執行方法Run(),因為Run是普通例項方
法,所以會執行Car類中的Run 方法。
如果呼叫了Method_A的Purpose 方法,即不用變數car調
用,也不用物件呼叫,而是用類名Car呼叫,因為靜態方法會在
類中分配記憶體的。如果用Car生成多個例項,靜態成員只有一份,
就是在類中,而不是在物件中。
33333333333333333333333333333333
在32位的Windows作業系統中,每個程序都可以使用4GB的記憶體,這得益於虛擬定址技術,在這4GB的記憶體中儲存著可執行程式碼、程式碼載入的DLL和程式執行的所有變數,在C#中,虛擬記憶體中有個兩個儲存變數的區域,一個稱為堆疊,一個稱為託管堆,託管堆的出現是.net不同於其他語言的地方,堆疊儲存值型別資料,而託管堆儲存引用型別如類、物件,並受垃圾收集器的控制和管理。在堆疊中,一旦變數超出使用範圍,其使用的記憶體空間會被其他變數重新使用,這時其空間中儲存的值將被其他變數覆蓋而不復存在,但有時候我們希望這些值仍然存在,這就需要託管堆來實現。我們用幾段程式碼來說明其工作原理,假設已經定義了一個類class1:
class1 object1;
object1=new class1();
第一句定義了一個class1的引用,實質上只是在堆疊中分配了一個4個位元組的空間,它將用來存府後來例項化物件在託管堆中的地址,在windows中這需要4個位元組來表示記憶體地址。第二句例項化object1物件,實際上是在託管堆中開僻了一個記憶體空間來儲存類class1的一個具體物件,假設這個物件需要36個位元組,那麼object1指向的實際上是在託管堆一個大小為36個位元組的連續記憶體空間開始的地址。由此也可以看出在C#編譯器中為什麼不允許使用未例項化的物件,因為這個物件在託管堆中還不存在。當物件不再使用時,這個被儲存在堆疊中的引用變數將被刪除,但是從上述機制可以看出,在託管堆中這個引用指向的物件仍然存在,其空間何時被釋放取決垃圾收集器而不是引用變數失去作用域時。
在使用電腦的過程中大家可能都有過這種經驗:電腦用久了以後程式執行會變得越來越慢,其中一個重要原因就是系統中存在大量記憶體碎片,就是因為程式反覆在堆疊中建立和釋入變數,久而久之可用變數在記憶體中將不再是連續的記憶體空間,為了定址這些變數也會增加系統開銷。在.net中這種情形將得到很大改善,這是因為有了垃圾收集器的工作,垃圾收集器將會壓縮託管堆的記憶體空間,保證可用變數在一個連續的記憶體空間內,同時將堆疊中引用變數中的地址改為新的地址,這將會帶來額外的系統開銷,但是,其帶來的好處將會抵消這種影響,而另外一個好處是,程式設計師將不再花上大量的心思在內在洩露問題上。
當然,以C#程式中不僅僅只有引用型別的變數,仍然也存在值型別和其他託管堆不能管理的物件,如果檔名柄、網路連線和資料庫連線,這些變數的釋放仍需要程式設計師通過解構函式或IDispose介面來做。
另一方面,在某些時候C#程式也需要追求速度,比如對一個含用大量成員的陣列的操作,如果仍使用傳統的類來操作,將不會得到很好的效能,因為陣列在C#中實際是System.Array的例項,會儲存在託管堆中,這將會對運算造成大量的額外的操作,因為除了垃圾收集器除了會壓縮託管堆、更新引用地址、還會維護託管堆的資訊列表。所幸的是C#中同樣能夠通過不安全程式碼使用C++程式設計師通常喜歡的方式來編碼,在標記為unsafe的程式碼塊使用指標,這和在C++中使用指標沒有什麼不同,變數也是存府在堆疊中,在這種情況下宣告一個數組可以使用stackalloc語法,比如宣告一個儲存有50個double型別的陣列:
double* pDouble=stackalloc double[50]
stackalloc會給pDouble陣列在堆疊中分配50個double型別大小的記憶體空間,可以使用pDouble[0]、*(pDouble+1)這種方式運算元組,與在C++中一樣,使用指標時必須知道自己在做什麼,確保訪問的正確的記憶體空間,否則將會出現無法預料的錯誤。
掌握託管堆、堆疊、垃圾收集器和不安全程式碼的工作原理和方式,將有助於你成為真正的優秀C#程式設計師。
程序中每個執行緒都有自己的堆疊,這是一段執行緒建立時保留下的地址區域。我們的“棧記憶體”即在此。至於“堆”記憶體,我個人認為在未用new定義時,堆應該就是未“保留”未“提交”的自由空間,new的功能是在這些自由空間中保留(並提交?)出一個地址範圍
棧(Stack)是作業系統在建立某個程序時或者執行緒(在支援多執行緒的作業系統中是執行緒)為這個執行緒建立的儲存區域,該區域具有FIFO的特性,在編譯的時候可以指定需要的Stack的大小。在程式設計中,例如C/C++中,所有的區域性變數都是從棧中分配記憶體空間,實際上也不是什麼分配,只是從棧頂向上用就行,在退出函式的時候,只是修改棧指標就可以把棧中的內容銷燬,所以速度最快。
堆(Heap)是應用程式在執行的時候請求作業系統分配給自己記憶體,一般是申請/給予的過程,C/C++分別用malloc/New請求分配Heap,用free/delete銷燬記憶體。由於從作業系統管理的記憶體分配所以在分配和銷燬時都要佔用時間,所以用堆的效率低的多!但是堆的好處是可以做的很大,C/C++對分配的Heap是不初始化的。
在Java中除了簡單型別(int,char等)都是在堆中分配記憶體,這也是程式慢的一個主要原因。但是跟C/C++不同,Java中分配Heap記憶體是自動初始化的。在Java中所有的物件(包括int的wrapper Integer)都是在堆中分配的,但是這個物件的引用卻是在Stack中分配。也就是說在建立一個物件時從兩個地方都分配記憶體,在Heap中分配的記憶體實際建立這個物件,而在Stack中分配的記憶體只是一個指向這個堆物件的指標(引用)而已。
在.NET的所有技術中,最具爭議的恐怕是垃圾收集(Garbage Collection,GC)了。作為.NET框架中一個重要的部分,託管堆和垃圾收集機制對我們中的大部分人來說是陌生的概念。在這篇文章中將要討論託管堆,和你將從中得到怎樣的好處。
為什麼要託管堆?
.NET框架包含一個託管堆,所有的.NET語言在分配引用型別物件時都要使用它。像值型別這樣的輕量級物件始終分配在棧中,但是所有的類例項和陣列都被生成在一個記憶體池中,這個記憶體池就是託管堆。
垃圾收集器的基本演算法很簡單:
● 將所有的託管記憶體標記為垃圾
● 尋找正被使用的記憶體塊,並將他們標記為有效
● 釋放所有沒有被使用的記憶體塊
● 整理堆以減少碎片
託管堆優化
看上去似乎很簡單,但是垃圾收集器實際採用的步驟和堆管理系統的其他部分並非微不足道,其中常常涉及為提高效能而作的優化設計。舉例來說,垃圾收集遍歷整個記憶體池具有很高的開銷。然而,研究表明大部分在託管堆上分配的物件只有很短的生存期,因此堆被分成三個段,稱作generations。新分配的物件被放在generation 0中。這個generation是最先被回收的——在這個generation中最有可能找到不再使用的記憶體,由於它的尺寸很小(小到足以放進處理器的L2 cache中),因此在它裡面的回收將是最快和最高效的。
託管堆的另外一種優化操作與locality of reference規則有關。該規則表明,一起分配的物件經常被一起使用。如果物件們在堆中位置很緊湊的話,快取記憶體的效能將會得到提高。由於託管堆的天性,物件們總是被分配在連續的地址上,託管堆總是保持緊湊,結果使得物件們始終彼此靠近,永遠不會分得很遠。這一點與標準堆提供的非託管程式碼形成了鮮明的對比,在標準堆中,堆很容易變成碎片,而且一起分配的物件經常分得很遠。
還有一種優化是與大物件有關的。通常,大物件具有很長的生存期。當一個大物件在.NET託管堆中產生時,它被分配在堆的一個特殊部分中,這部分堆永遠不會被整理。因為移動大物件所帶來的開銷超過了整理這部分堆所能提高的效能。
關於外部資源(External Resources)的問題
垃圾收集器能夠有效地管理從託管堆中釋放的資源,但是資源回收操作只有在記憶體緊張而觸發一個回收動作時才執行。那麼,類是怎樣來管理像資料庫連線或者視窗控制代碼這樣有限的資源的呢?等待,直到垃圾回收被觸發之後再清理資料庫連線或者檔案控制代碼並不是一個好方法,這會嚴重降低系統的效能。
所有擁有外部資源的類,在這些資源已經不再用到的時候,都應當執行Close或者Dispose方法。從Beta2(譯註:本文中所有的Beta2均是指.NET Framework Beta2,不再特別註明)開始,Dispose模式通過IDisposable介面來實現。這將在本文的後續部分討論。
需要清理外部資源的類還應當實現一個終止操作(finalizer)。在C#中,建立終止操作的首選方式是在解構函式中實現,而在Framework層,終止操作的實現則是通過過載System.Object.Finalize 方法。以下兩種實現終止操作的方法是等效的:
~OverdueBookLocator()
{
Dispose(false);
}
和:
public void Finalize()
{
base.Finalize();
Dispose(false);
}
在C#中,同時在Finalize方法和解構函式實現終止操作將會導致錯誤的產生。
除非你有足夠的理由,否則你不應該建立解構函式或者Finalize方法。終止操作會降低系統的效能,並且增加執行期的記憶體開銷。同時,由於終止操作被執行的方式,你並不能保證何時一個終止操作會被執行。
記憶體分配和垃圾回收的細節
對GC有了一個總體印象之後,讓我們來討論關於託管堆中的分配與回收工作的細節。託管堆看起來與我們已經熟悉的C++程式設計中的傳統的堆一點都不像。在傳統的堆中,資料結構習慣於使用大塊的空閒記憶體。在其中查詢特定大小的記憶體塊是一件很耗時的工作,尤其是當記憶體中充滿碎片的時候。與此不同,在託管堆中,記憶體被組製成連續的陣列,指標總是巡著已經被使用的記憶體和未被使用的記憶體之間的邊界移動。當記憶體被分配的時候,指標只是簡單地遞增——由此而來的一個好處是,分配操作的效率得到了很大的提升。
當物件被分配的時候,它們一開始被放在generation 0中。當generation 0的大小快要達到它的上限的時候,一個只在generation 0中執行的回收操作被觸發。由於generation 0的大小很小,因此這將是一個非常快的GC過程。這個GC過程的結果是將generation 0徹底的重新整理了一遍。不再使用的物件被釋放,確實正被使用的物件被整理並移入generation 1中。
當generation 1的大小隨著從generation 0中移入的物件數量的增加而接近它的上限的時候,一個回收動作被觸發來在generation 0和generation 1中執行GC過程。如同在generation 0中一樣,不再使用的物件被釋放,正在被使用的物件被整理並移入下一個generation中。大部分GC過程的主要目標是generation 0,因為在generation 0中最有可能存在大量的已不再使用的臨時物件。對generation 2的回收過程具有很高的開銷,並且此過程只有在generation
0和generation 1的GC過程不能釋放足夠的記憶體時才會被觸發。如果對generation 2的GC過程仍然不能釋放足夠的記憶體,那麼系統就會丟擲OutOfMemoryException異常
帶有終止操作的物件的垃圾收集過程要稍微複雜一些。當一個帶有終止操作的物件被標記為垃圾時,它並不會被立即釋放。相反,它會被放置在一個終止佇列(finalization queue)中,此佇列為這個物件建立一個引用,來避免這個物件被回收。後臺執行緒為佇列中的每個物件執行它們各自的終止操作,並且將已經執行過終止操作的物件從終止佇列中刪除。只有那些已經執行過終止操作的物件才會在下一次垃圾回收過程中被從記憶體中刪除。這樣做的一個後果是,等待被終止的物件有可能在它被清除之前,被移入更高一級的generation中,從而增加它被清除的延遲時間。
需要執行終止操作的物件應當實現IDisposable介面,以便客戶程式通過此介面快速執行終止動作。IDisposable介面包含一個方法——Dispose。這個被Beta2引入的介面,採用一種在Beta2之前就已經被廣泛使用的模式實現。從本質上講,一個需要終止操作的物件暴露出Dispose方法。這個方法被用來釋放外部資源並抑制終止操作,就象下面這個程式片斷所演示的那樣:
public class OverdueBookLocator: IDisposable
{
~OverdueBookLocator()
{
InternalDispose(false);
}
public void Dispose()
{
InternalDispose(true);
}
protected void InternalDispose(bool disposing)
{
if(disposing)
{
GC.SuppressFinalize(this);
// Dispose of managed objects if disposing.
}
// free external resources here
}
}
這些都是.NET中CLR的概念,和C#沒多大關係。
使用基於CLR的語言編譯器開發的程式碼稱為託管程式碼。
託管堆是CLR中自動記憶體管理的基礎。初始化新程序時,執行時會為程序保留一個連續的地址空間區域。這個保留的地址空間被稱為託管堆。託管堆維護著一個指標,用它指向將在堆中分配的下一個物件的地址。最初,該指標設定為指向託管堆的基址。
認真看MSDN Library,就會搞清楚這些概念。
以下程式碼說明的很形象:
//引用型別('class' 類型別)
class SomeRef { public int32 x;}
//值型別('struct')
struct SomeVal(pulic Int32 x;}
static void ValueTypeDemo()
{
SomeRef r1=new SomeRef();//分配在託管堆
SomeVal v1=new SomeVal();//堆疊上
r1.x=5;//解析指標
v1.x=5;//在堆疊上修改
SomeRef r2=r1;//僅拷貝引用(指標)
SomeVal v2=v1;//先在堆疊上分配,然後拷貝成員
r1.x=8;//改變了r1,r2的值
v1.x=9;//改變了v1,沒有改變v2
}
4444444444444444444444444444444444444444444444444444444
棧是記憶體中完全用於儲存區域性變數或成員欄位(值型別資料)的高效的區域,但其大小有限制。
託管堆所佔記憶體比棧大得多,當訪問速度較慢。託管堆只用於分配記憶體,一般由CLR(Common Language Runtime)來處理記憶體釋放問題。
當建立值型別資料時,在棧上分配記憶體;
當建立引用型資料時,在託管堆上分配記憶體並返回物件的引用。注意這個物件的引用,像其他區域性變數一樣也是儲存在棧中的。該引用指向的值則位於託管堆中。
如果建立了一個包含值型別的引用型別,比如陣列,其元素的值也是存放在託管堆中而非棧中的。當從陣列中檢索資料時,獲得本地使用的元素值的副本,而該副本這時候就是存放在棧中的了。所以,不能籠統的說“值型別儲存在棧中,引用型別儲存在託管堆中”。
值型別和引用型別的區別:引用型別儲存在託管堆的唯一位置中,其存在於託管堆中某個地方,由使用該實體的變數引用;而值型別儲存在使用它們的地方,有幾處在使用,就有幾個副本存在。
對於引用型別,如果在宣告變數的時候沒有使用new運算子,執行時就不會給它分配託管堆上的記憶體空間,而是在棧上給它分配一個包含null值的引用。對於值型別,執行時會給它分配棧上的空間,並呼叫預設的建構函式,來初始化物件的狀態。
55555555555555555555555555555555555555555555555555
一、棧和託管堆
通用型別系統(CTS)區分兩種基本型別:值型別和引用型別。它們之間的根本區別在於它們在記憶體中的儲存方式。.NET使用兩種不同的實體記憶體塊來儲存資料—棧和託管堆。如下圖所示:
二 型別層次結構
CTS定義了一種型別層次結構,該結構不僅僅描述了不同的預定義型別,還指出了使用者定義型別在層次結構種的