原文地址:http://blog.csdn.net/baoxuetianxia/archive/2008/11/04/3218913.aspx
首先堆棧和堆(托管堆)都在進(jìn)程的虛擬內(nèi)存中。(在32位處理器上每個進(jìn)程的虛擬內(nèi)存為4GB)
堆棧stack
堆棧中存儲值類型。
堆棧實際上是向下填充,即由高內(nèi)存地址指向低內(nèi)存地址填充。
堆棧的工作方式是先分配內(nèi)存的變量后釋放(先進(jìn)后出原則)。
堆棧中的變量是從下向上釋放,這樣就保證了堆棧中先進(jìn)后出的規(guī)則不與變量的生命周期起沖突!
堆棧的性能非常高,但是對于所有的變量來說還不太靈活,而且變量的生命周期必須嵌套。
通常我們希望使用一種方法分配內(nèi)存來存儲數(shù)據(jù),并且方法退出后很長一段時間內(nèi)數(shù)據(jù)仍然可以使用。此時就要用到堆(托管堆)!
堆(托管堆)heap
堆(托管堆)存儲引用類型。
此堆非彼堆,.NET中的堆由垃圾收集器自動管理。
與堆棧不同,堆是從下往上分配,所以自由的空間都在已用空間的上面。
比如創(chuàng)建一個對象:
Customer cus;
cus = new Customer();
申明一個Customer的引用cus,在堆棧上給這個引用分配存儲空間。這僅僅只是一個引用,不是實際的Customer對象!
cus占4個字節(jié)的空間,包含了存儲Customer的引用地址。
接著分配堆上的內(nèi)存以存儲Customer對象的實例,假定Customer對象的實例是32字節(jié),為了在堆上找到一個存儲Customer對象的存儲位置。
.NET運(yùn)行庫在堆中搜索第一個從未使用的,32字節(jié)的連續(xù)塊存儲Customer對象的實例!
然后把分配給Customer對象實例的地址賦給cus變量!
從這個例子中可以看出,建立對象引用的過程比建立值變量的過程復(fù)雜,且不能避免性能的降低!
實際上就是.NET運(yùn)行庫保存對的狀態(tài)信息,在堆中添加新數(shù)據(jù)時,堆棧中的引用變量也要更新。性能上損失很多!
有種機(jī)制在分配變量內(nèi)存的時候,不會受到堆棧的限制:把一個引用變量的值賦給一個相同類型的變量,那么這兩個變量就引用同一個堆中的對象。
當(dāng)一個應(yīng)用變量出作用域時,它會從堆棧中刪除。但引用對象的數(shù)據(jù)仍然保留在堆中,一直到程序結(jié)束 或者 該數(shù)據(jù)不被任何變量應(yīng)用時,垃圾收集器會刪除它。
裝箱轉(zhuǎn)化
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);
}
}
定義整數(shù)類型變量I的時候,這個變量占用的內(nèi)存是內(nèi)存棧中分配的,第二句是裝箱操作將變量 110存放到了內(nèi)存堆中,而定義object對象類型的變量obj則在內(nèi)存棧中,并指向int類型的數(shù)值110,而該數(shù)值是付給變量i的數(shù)值副本。
所以運(yùn)行結(jié)果是
i=220,obj=110
i=220,obj=330
內(nèi)存格局通常分為四個區(qū)
全局?jǐn)?shù)據(jù)區(qū):存放全局變量,靜態(tài)數(shù)據(jù),常量
代碼區(qū):存放所有的程序代碼
棧區(qū):存放為運(yùn)行而分配的局部變量,參數(shù),返回數(shù)據(jù),返回地址等,
堆區(qū):即自由存儲區(qū)
值類型變量與引用類型變量的內(nèi)存分配模型也不一樣。為了理解清楚這個問題,讀者首
先必須區(qū)分兩種不同類型的內(nèi)存區(qū)域:線程堆棧(Thread Stack)和托管堆(Managed Heap)。
每個正在運(yùn)行的程序都對應(yīng)著一個進(jìn)程(process),在一個進(jìn)程內(nèi)部,可以有一個或多
個線程(thread),每個線程都擁有一塊“自留地”,稱為“線程堆棧”,大小為1M,用于保
存自身的一些數(shù)據(jù),比如函數(shù)中定義的局部變量、函數(shù)調(diào)用時傳送的參數(shù)值等,這部分內(nèi)存
區(qū)域的分配與回收不需要程序員干涉。
所有值類型的變量都是在線程堆棧中分配的。
另一塊內(nèi)存區(qū)域稱為“堆(heap)”,在.NET 這種托管環(huán)境下,堆由CLR 進(jìn)行管理,所
以又稱為“托管堆(managed heap)”。
用new 關(guān)鍵字創(chuàng)建的類的對象時,分配給對象的內(nèi)存單元就位于托管堆中。
在程序中我們可以隨意地使用new 關(guān)鍵字創(chuàng)建多個對象,因此,托管堆中的內(nèi)存資源
是可以動態(tài)申請并使用的,當(dāng)然用完了必須歸還。
打個比方更易理解:托管堆相當(dāng)于一個旅館,其中的房間相當(dāng)于托管堆中所擁有的內(nèi)存
單元。當(dāng)程序員用new 方法創(chuàng)建對象時,相當(dāng)于游客向旅館預(yù)訂房間,旅館管理員會先看
一下有沒有合適的空房間,有的話,就可以將此房間提供給游客住宿。當(dāng)游客旅途結(jié)束,要
辦理退房手續(xù),房間又可以為其他旅客提供服務(wù)了。
從表 1 可以看到,引用類型共有四種:類類型、接口類型、數(shù)組類型和委托類型。
所有引用類型變量所引用的對象,其內(nèi)存都是在托管堆中分配的。
嚴(yán)格地說,我們常說的“對象變量”其實是類類型的引用變量。但在實際中人們經(jīng)常將
引用類型的變量簡稱為“對象變量”,用它來指代所有四種類型的引用變量。在不致于引起
混淆的情況下,本書也采用了這種慣例。
在了解了對象內(nèi)存模型之后,對象變量之間的相互賦值的含義也就清楚了。請看以下代
碼(示例項目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 句。
程序的運(yùn)行結(jié)果是:
b.i=100;
請讀者思索一下:兩個對象變量的相互賦值意味著什么?
事實上,兩個對象變量的相互賦值意味著賦值后兩個對象變量所占用的內(nèi)存單元其內(nèi)容
是相同的。
講得詳細(xì)一些:
第10 句創(chuàng)建對象以后,其首地址(假設(shè)為“1234 5678”)被放入到變量a 自身的4 個
字節(jié)的內(nèi)存單元中。
第12 句又定義了一個對象變量b,其值最初為null(即對應(yīng)的4 個字節(jié)內(nèi)存單元中為
“0000 0000”)。
第13 句執(zhí)行以后,a 變量的值被復(fù)制到b 的內(nèi)存單元中,現(xiàn)在,b 內(nèi)存單元中的值也為
“1234 5678”。
根據(jù)前面介紹的對象內(nèi)存模型,我們知道現(xiàn)在變量a 和b 都指向同一個實例對象。
如果通過b.i 修改字段i 的值,a.i 也會同步變化,因為a.i 與b.i 其實代表同一對象的同
一字段。
整個過程可以用圖 9 來說明:
圖 
圖 9 對象變量的相互賦值
由此得到一個重要結(jié)論:
對象變量的相互賦值不會導(dǎo)致對象自身被復(fù)制,其結(jié)果是兩個對象變量指向同一對象。
另外,由于對象變量本身是一個局部變量,因此,對象變量本身是位于線程堆棧中的。
嚴(yán)格區(qū)分對象變量與對象變量所引用的對象,是面向?qū)ο缶幊痰年P(guān)鍵之一。
由于對象變量類似于一個對象指針,這就產(chǎn)生了“判斷兩個對象變量是否引用同一對象”
的問題。
C#使用“==”運(yùn)算符比對兩個對象變量是否引用同一對象,“!=”比對兩個對象變量
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
需要注意的是,如果“==”被用在值類型的變量之間,則比對的是變量的內(nèi)容:
int i = 0;
int j = 100;
if (i == j)
{
Console.WriteLine("i與j的值相等");
}
理解值類型與引用類型的區(qū)別在面向?qū)ο缶幊讨蟹浅jP(guān)鍵。
1、類型,對象,堆棧和托管堆
C#的類型和對象在應(yīng)用計算機(jī)內(nèi)存時,大體用到兩種內(nèi)存,一個
叫堆棧,另一個叫托管堆,下面我們用直角長方形來代表堆棧,
用圓角長方形來代表托管堆。

首先討論一下方法內(nèi)部變量的存放。
先舉個例子,有如下兩個方法,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
}
這段代碼的執(zhí)行,用圖表示為:


上述的每個圖片,基本對應(yīng)程序中的每個步驟。在開始執(zhí)行Met
hod_1的時候,先把value1 壓入堆棧頂,然后是value2,接
下來的是調(diào)用方法Add,因為方法有兩個參數(shù)是n1 和n2,所以
把n1 和n2 分別壓入堆棧,因為此處是調(diào)用了一個方法,并且方
法有返回值,所以這里需要保存Add的返回地址,然后進(jìn)入Ad
d方法內(nèi)部,在Add內(nèi)部,首先是給sum 賦值,所以把sum 壓
入棧項,然后用return 返回,此時,先前的返回地址就起到了
作用,return 會根據(jù)地址返回去的,在返回的過程中,把sum
推出棧頂,找到了返回地址,但在Method_1 方法中,我們希望
把Add的返回值賦給value3,此時的返回地址也被推出堆棧,
把value3 壓入堆棧。雖這個例子的結(jié)果在這里沒有多大用途,
但這個例子很好的說明了在方法被執(zhí)行時,變量與進(jìn)出堆棧的情
況。這里也能看出為什么方法內(nèi)部的局變量用過后,不能在其他
方法中訪問的原因。
其次來討論一下類和對象在托管堆和堆棧中的情況。
先看一下代碼:
class Car
{
public void Run()
{
Console.WriteLine("一切正常");
}
public virtual double GetPrice()
{
return 0;
}
public static void Purpose()
{
Console.WriteLine("載人");
}
PDF 文件使用 "pdfFactory Pro" 試用版本創(chuàng)建 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類可以重
寫這個方法。
下面接著看調(diào)用代碼。
public void Method_A()
{
double CarPrice;//1
Car car = new BMW();//2
CarPrice = car.GetPrice();//調(diào)用虛方法(其實調(diào)用的是重寫后
的方法)
car.Run();//調(diào)用實例化方法
Car.Purpose();//調(diào)用靜態(tài)方法
}
這個方法也比較簡單,就是定義一個變量用來獲得價格,同時
定義了一個父類的變量,用子類來實例化它。
接下來,我們分步來說明。
看一下運(yùn)行時堆棧和托管堆的情部我

這里需要說明的是,類是位于托管堆中的,每個類又分為四個
類部,類指針,用來關(guān)聯(lián)對象;同步索引,用來完成同步(比如線
程的同步)需建立的;靜態(tài)成員是屬于類的,所以在類中出現(xiàn),還
有一個方法列表(這里的方法列表項與具體的方法對應(yīng))。
當(dāng)Method_A方法的第一步執(zhí)行時:

這時的CarPrice 是沒有值的
當(dāng)Method_A方法執(zhí)行到第二步,其實第二步又可以分成
Car car;
car = new BMW();
先看Car car;

car在這里是一個方法內(nèi)部的變量,所以被壓到堆棧中。
再看 car = new BMW();
這是一個實例化過程,car變成了一個對象

這里是用子類來實例化父類型。對象其實是子類的類型的,但
變量的類型是父類的。
接下來,在Method_A中的調(diào)用的中調(diào)用car.GetPrice(),
對于Car來說,這個方法是虛方法(并且子類重寫了它),虛方
法在調(diào)用是不會執(zhí)行類型上的方法,即不會執(zhí)行Car類中的虛方
法,而是執(zhí)行對象對應(yīng)類上的方法,即BMW中的GtPrice。
如果Method_A中執(zhí)行方法Run(),因為Run是普通實例方
法,所以會執(zhí)行Car類中的Run 方法。
如果調(diào)用了Method_A的Purpose 方法,即不用變量car調(diào)
用,也不用對象調(diào)用,而是用類名Car調(diào)用,因為靜態(tài)方法會在
類中分配內(nèi)存的。如果用Car生成多個實例,靜態(tài)成員只有一份,
就是在類中,而不是在對象中。
33333333333333333333333333333333
在32位的Windows操作系統(tǒng)中,每個進(jìn)程都可以使用4GB的內(nèi)存,這得益于虛擬尋址技術(shù),在這4GB的內(nèi)存中存儲著可執(zhí)行代碼、代碼加載的DLL和程序運(yùn)行的所有變量,在C#中,虛擬內(nèi)存中有個兩個存儲變量的區(qū)域,一個稱為堆棧,一個稱為托管堆,托管堆的出現(xiàn)是.net不同于其他語言的地方,堆棧存儲值類型數(shù)據(jù),而托管堆存儲引用類型如類、對象,并受垃圾收集器的控制和管理。在堆棧中,一旦變量超出使用范圍,其使用的內(nèi)存空間會被其他變量重新使用,這時其空間中存儲的值將被其他變量覆蓋而不復(fù)存在,但有時候我們希望這些值仍然存在,這就需要托管堆來實現(xiàn)。我們用幾段代碼來說明其工作原理,假設(shè)已經(jīng)定義了一個類class1:
class1 object1;
object1=new class1();
第一句定義了一個class1的引用,實質(zhì)上只是在堆棧中分配了一個4個字節(jié)的空間,它將用來存府后來實例化對象在托管堆中的地址,在windows中這需要4個字節(jié)來表示內(nèi)存地址。第二句實例化object1對象,實際上是在托管堆中開僻了一個內(nèi)存空間來存儲類class1的一個具體對象,假設(shè)這個對象需要36個字節(jié),那么object1指向的實際上是在托管堆一個大小為36個字節(jié)的連續(xù)內(nèi)存空間開始的地址。由此也可以看出在C#編譯器中為什么不允許使用未實例化的對象,因為這個對象在托管堆中還不存在。當(dāng)對象不再使用時,這個被存儲在堆棧中的引用變量將被刪除,但是從上述機(jī)制可以看出,在托管堆中這個引用指向的對象仍然存在,其空間何時被釋放取決垃圾收集器而不是引用變量失去作用域時。
在使用電腦的過程中大家可能都有過這種經(jīng)驗:電腦用久了以后程序運(yùn)行會變得越來越慢,其中一個重要原因就是系統(tǒng)中存在大量內(nèi)存碎片,就是因為程序反復(fù)在堆棧中創(chuàng)建和釋入變量,久而久之可用變量在內(nèi)存中將不再是連續(xù)的內(nèi)存空間,為了尋址這些變量也會增加系統(tǒng)開銷。在.net中這種情形將得到很大改善,這是因為有了垃圾收集器的工作,垃圾收集器將會壓縮托管堆的內(nèi)存空間,保證可用變量在一個連續(xù)的內(nèi)存空間內(nèi),同時將堆棧中引用變量中的地址改為新的地址,這將會帶來額外的系統(tǒng)開銷,但是,其帶來的好處將會抵消這種影響,而另外一個好處是,程序員將不再花上大量的心思在內(nèi)在泄露問題上。
當(dāng)然,以C#程序中不僅僅只有引用類型的變量,仍然也存在值類型和其他托管堆不能管理的對象,如果文件名柄、網(wǎng)絡(luò)連接和數(shù)據(jù)庫連接,這些變量的釋放仍需要程序員通過析構(gòu)函數(shù)或IDispose接口來做。
另一方面,在某些時候C#程序也需要追求速度,比如對一個含用大量成員的數(shù)組的操作,如果仍使用傳統(tǒng)的類來操作,將不會得到很好的性能,因為數(shù)組在C#中實際是System.Array的實例,會存儲在托管堆中,這將會對運(yùn)算造成大量的額外的操作,因為除了垃圾收集器除了會壓縮托管堆、更新引用地址、還會維護(hù)托管堆的信息列表。所幸的是C#中同樣能夠通過不安全代碼使用C++程序員通常喜歡的方式來編碼,在標(biāo)記為unsafe的代碼塊使用指針,這和在C++中使用指針沒有什么不同,變量也是存府在堆棧中,在這種情況下聲明一個數(shù)組可以使用stackalloc語法,比如聲明一個存儲有50個double類型的數(shù)組:
double* pDouble=stackalloc double[50]
stackalloc會給pDouble數(shù)組在堆棧中分配50個double類型大小的內(nèi)存空間,可以使用pDouble[0]、*(pDouble+1)這種方式操作數(shù)組,與在C++中一樣,使用指針時必須知道自己在做什么,確保訪問的正確的內(nèi)存空間,否則將會出現(xiàn)無法預(yù)料的錯誤。
掌握托管堆、堆棧、垃圾收集器和不安全代碼的工作原理和方式,將有助于你成為真正的優(yōu)秀C#程序員。
進(jìn)程中每個線程都有自己的堆棧,這是一段線程創(chuàng)建時保留下的地址區(qū)域。我們的“棧內(nèi)存”即在此。至于“堆”內(nèi)存,我個人認(rèn)為在未用new定義時,堆應(yīng)該就是未“保留”未“提交”的自由空間,new的功能是在這些自由空間中保留(并提交?)出一個地址范圍
棧(Stack)是操作系統(tǒng)在建立某個進(jìn)程時或者線程(在支持多線程的操作系統(tǒng)中是線程)為這個線程建立的存儲區(qū)域,該區(qū)域具有FIFO的特性,在編譯的時候可以指定需要的Stack的大小。在編程中,例如C/C++中,所有的局部變量都是從棧中分配內(nèi)存空間,實際上也不是什么分配,只是從棧頂向上用就行,在退出函數(shù)的時候,只是修改棧指針就可以把棧中的內(nèi)容銷毀,所以速度最快。
堆(Heap)是應(yīng)用程序在運(yùn)行的時候請求操作系統(tǒng)分配給自己內(nèi)存,一般是申請/給予的過程,C/C++分別用malloc/New請求分配Heap,用free/delete銷毀內(nèi)存。由于從操作系統(tǒng)管理的內(nèi)存分配所以在分配和銷毀時都要占用時間,所以用堆的效率低的多!但是堆的好處是可以做的很大,C/C++對分配的Heap是不初始化的。
在Java中除了簡單類型(int,char等)都是在堆中分配內(nèi)存,這也是程序慢的一個主要原因。但是跟C/C++不同,Java中分配Heap內(nèi)存是自動初始化的。在Java中所有的對象(包括int的wrapper Integer)都是在堆中分配的,但是這個對象的引用卻是在Stack中分配。也就是說在建立一個對象時從兩個地方都分配內(nèi)存,在Heap中分配的內(nèi)存實際建立這個對象,而在Stack中分配的內(nèi)存只是一個指向這個堆對象的指針(引用)而已。
在.NET的所有技術(shù)中,最具爭議的恐怕是垃圾收集(Garbage Collection,GC)了。作為.NET框架中一個重要的部分,托管堆和垃圾收集機(jī)制對我們中的大部分人來說是陌生的概念。在這篇文章中將要討論托管堆,和你將從中得到怎樣的好處。
為什么要托管堆?
.NET框架包含一個托管堆,所有的.NET語言在分配引用類型對象時都要使用它。像值類型這樣的輕量級對象始終分配在棧中,但是所有的類實例和數(shù)組都被生成在一個內(nèi)存池中,這個內(nèi)存池就是托管堆。
垃圾收集器的基本算法很簡單:
● 將所有的托管內(nèi)存標(biāo)記為垃圾
● 尋找正被使用的內(nèi)存塊,并將他們標(biāo)記為有效
● 釋放所有沒有被使用的內(nèi)存塊
● 整理堆以減少碎片
托管堆優(yōu)化
看上去似乎很簡單,但是垃圾收集器實際采用的步驟和堆管理系統(tǒng)的其他部分并非微不足道,其中常常涉及為提高性能而作的優(yōu)化設(shè)計。舉例來說,垃圾收集遍歷整個內(nèi)存池具有很高的開銷。然而,研究表明大部分在托管堆上分配的對象只有很短的生存期,因此堆被分成三個段,稱作generations。新分配的對象被放在generation 0中。這個generation是最先被回收的——在這個generation中最有可能找到不再使用的內(nèi)存,由于它的尺寸很?。ㄐ〉阶阋苑胚M(jìn)處理器的L2 cache中),因此在它里面的回收將是最快和最高效的。
托管堆的另外一種優(yōu)化操作與locality of reference規(guī)則有關(guān)。該規(guī)則表明,一起分配的對象經(jīng)常被一起使用。如果對象們在堆中位置很緊湊的話,高速緩存的性能將會得到提高。由于托管堆的天性,對象們總是被分配在連續(xù)的地址上,托管堆總是保持緊湊,結(jié)果使得對象們始終彼此靠近,永遠(yuǎn)不會分得很遠(yuǎn)。這一點與標(biāo)準(zhǔn)堆提供的非托管代碼形成了鮮明的對比,在標(biāo)準(zhǔn)堆中,堆很容易變成碎片,而且一起分配的對象經(jīng)常分得很遠(yuǎn)。
還有一種優(yōu)化是與大對象有關(guān)的。通常,大對象具有很長的生存期。當(dāng)一個大對象在.NET托管堆中產(chǎn)生時,它被分配在堆的一個特殊部分中,這部分堆永遠(yuǎn)不會被整理。因為移動大對象所帶來的開銷超過了整理這部分堆所能提高的性能。
關(guān)于外部資源(External Resources)的問題
垃圾收集器能夠有效地管理從托管堆中釋放的資源,但是資源回收操作只有在內(nèi)存緊張而觸發(fā)一個回收動作時才執(zhí)行。那么,類是怎樣來管理像數(shù)據(jù)庫連接或者窗口句柄這樣有限的資源的呢?等待,直到垃圾回收被觸發(fā)之后再清理數(shù)據(jù)庫連接或者文件句柄并不是一個好方法,這會嚴(yán)重降低系統(tǒng)的性能。
所有擁有外部資源的類,在這些資源已經(jīng)不再用到的時候,都應(yīng)當(dāng)執(zhí)行Close或者Dispose方法。從Beta2(譯注:本文中所有的Beta2均是指.NET Framework Beta2,不再特別注明)開始,Dispose模式通過IDisposable接口來實現(xiàn)。這將在本文的后續(xù)部分討論。
需要清理外部資源的類還應(yīng)當(dāng)實現(xiàn)一個終止操作(finalizer)。在C#中,創(chuàng)建終止操作的首選方式是在析構(gòu)函數(shù)中實現(xiàn),而在Framework層,終止操作的實現(xiàn)則是通過重載System.Object.Finalize 方法。以下兩種實現(xiàn)終止操作的方法是等效的:
~OverdueBookLocator()
{
Dispose(false);
}
和:
public void Finalize()
{
base.Finalize();
Dispose(false);
}
在C#中,同時在Finalize方法和析構(gòu)函數(shù)實現(xiàn)終止操作將會導(dǎo)致錯誤的產(chǎn)生。
除非你有足夠的理由,否則你不應(yīng)該創(chuàng)建析構(gòu)函數(shù)或者Finalize方法。終止操作會降低系統(tǒng)的性能,并且增加執(zhí)行期的內(nèi)存開銷。同時,由于終止操作被執(zhí)行的方式,你并不能保證何時一個終止操作會被執(zhí)行。
內(nèi)存分配和垃圾回收的細(xì)節(jié)
對GC有了一個總體印象之后,讓我們來討論關(guān)于托管堆中的分配與回收工作的細(xì)節(jié)。托管堆看起來與我們已經(jīng)熟悉的C++編程中的傳統(tǒng)的堆一點都不像。在傳統(tǒng)的堆中,數(shù)據(jù)結(jié)構(gòu)習(xí)慣于使用大塊的空閑內(nèi)存。在其中查找特定大小的內(nèi)存塊是一件很耗時的工作,尤其是當(dāng)內(nèi)存中充滿碎片的時候。與此不同,在托管堆中,內(nèi)存被組制成連續(xù)的數(shù)組,指針總是巡著已經(jīng)被使用的內(nèi)存和未被使用的內(nèi)存之間的邊界移動。當(dāng)內(nèi)存被分配的時候,指針只是簡單地遞增——由此而來的一個好處是,分配操作的效率得到了很大的提升。
當(dāng)對象被分配的時候,它們一開始被放在generation 0中。當(dāng)generation 0的大小快要達(dá)到它的上限的時候,一個只在generation 0中執(zhí)行的回收操作被觸發(fā)。由于generation 0的大小很小,因此這將是一個非??斓腉C過程。這個GC過程的結(jié)果是將generation 0徹底的刷新了一遍。不再使用的對象被釋放,確實正被使用的對象被整理并移入generation 1中。
當(dāng)generation 1的大小隨著從generation 0中移入的對象數(shù)量的增加而接近它的上限的時候,一個回收動作被觸發(fā)來在generation 0和generation 1中執(zhí)行GC過程。如同在generation 0中一樣,不再使用的對象被釋放,正在被使用的對象被整理并移入下一個generation中。大部分GC過程的主要目標(biāo)是generation 0,因為在generation 0中最有可能存在大量的已不再使用的臨時對象。對generation 2的回收過程具有很高的開銷,并且此過程只有在generation 0和generation 1的GC過程不能釋放足夠的內(nèi)存時才會被觸發(fā)。如果對generation 2的GC過程仍然不能釋放足夠的內(nèi)存,那么系統(tǒng)就會拋出OutOfMemoryException異常
帶有終止操作的對象的垃圾收集過程要稍微復(fù)雜一些。當(dāng)一個帶有終止操作的對象被標(biāo)記為垃圾時,它并不會被立即釋放。相反,它會被放置在一個終止隊列(finalization queue)中,此隊列為這個對象建立一個引用,來避免這個對象被回收。后臺線程為隊列中的每個對象執(zhí)行它們各自的終止操作,并且將已經(jīng)執(zhí)行過終止操作的對象從終止隊列中刪除。只有那些已經(jīng)執(zhí)行過終止操作的對象才會在下一次垃圾回收過程中被從內(nèi)存中刪除。這樣做的一個后果是,等待被終止的對象有可能在它被清除之前,被移入更高一級的generation中,從而增加它被清除的延遲時間。
需要執(zhí)行終止操作的對象應(yīng)當(dāng)實現(xiàn)IDisposable接口,以便客戶程序通過此接口快速執(zhí)行終止動作。IDisposable接口包含一個方法——Dispose。這個被Beta2引入的接口,采用一種在Beta2之前就已經(jīng)被廣泛使用的模式實現(xiàn)。從本質(zhì)上講,一個需要終止操作的對象暴露出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#沒多大關(guān)系。
使用基于CLR的語言編譯器開發(fā)的代碼稱為托管代碼。
托管堆是CLR中自動內(nèi)存管理的基礎(chǔ)。初始化新進(jìn)程時,運(yùn)行時會為進(jìn)程保留一個連續(xù)的地址空間區(qū)域。這個保留的地址空間被稱為托管堆。托管堆維護(hù)著一個指針,用它指向?qū)⒃诙阎蟹峙涞南乱粋€對象的地址。最初,該指針設(shè)置為指向托管堆的基址。
認(rèn)真看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
棧是內(nèi)存中完全用于存儲局部變量或成員字段(值類型數(shù)據(jù))的高效的區(qū)域,但其大小有限制。
托管堆所占內(nèi)存比棧大得多,當(dāng)訪問速度較慢。托管堆只用于分配內(nèi)存,一般由CLR(Common Language Runtime)來處理內(nèi)存釋放問題。
當(dāng)創(chuàng)建值類型數(shù)據(jù)時,在棧上分配內(nèi)存;
當(dāng)創(chuàng)建引用型數(shù)據(jù)時,在托管堆上分配內(nèi)存并返回對象的引用。注意這個對象的引用,像其他局部變量一樣也是保存在棧中的。該引用指向的值則位于托管堆中。
如果創(chuàng)建了一個包含值類型的引用類型,比如數(shù)組,其元素的值也是存放在托管堆中而非棧中的。當(dāng)從數(shù)組中檢索數(shù)據(jù)時,獲得本地使用的元素值的副本,而該副本這時候就是存放在棧中的了。所以,不能籠統(tǒng)的說“值類型保存在棧中,引用類型保存在托管堆中”。
值類型和引用類型的區(qū)別:引用類型存儲在托管堆的唯一位置中,其存在于托管堆中某個地方,由使用該實體的變量引用;而值類型存儲在使用它們的地方,有幾處在使用,就有幾個副本存在。
對于引用類型,如果在聲明變量的時候沒有使用new運(yùn)算符,運(yùn)行時就不會給它分配托管堆上的內(nèi)存空間,而是在棧上給它分配一個包含null值的引用。對于值類型,運(yùn)行時會給它分配棧上的空間,并調(diào)用默認(rèn)的構(gòu)造函數(shù),來初始化對象的狀態(tài)。
55555555555555555555555555555555555555555555555555
一、棧和托管堆
通用類型系統(tǒng)(CTS)區(qū)分兩種基本類型:值類型和引用類型。它們之間的根本區(qū)別在于它們在內(nèi)存中的存儲方式。.NET使用兩種不同的物理內(nèi)存塊來存儲數(shù)據(jù)—棧和托管堆。如下圖所示:


二 類型層次結(jié)構(gòu)
CTS定義了一種類型層次結(jié)構(gòu),該結(jié)構(gòu)不僅僅描述了不同的預(yù)定義類型,還指出了用戶定義類型在層次結(jié)構(gòu)種的




posted on 2011-04-08 15:00
luis 閱讀(6565)
評論(0) 編輯 收藏 引用