首先堆棧和堆(託管堆)都在進程的虛擬內存中。(在32位處理器上每一個進程的虛擬內存爲4GB)html
堆棧stack程序員
堆棧中存儲值類型。算法
堆棧其實是向下填充,即由高內存地址指向低內存地址填充。數據庫
堆棧的工做方式是先分配內存的變量後釋放(先進後出原則)。編程
堆棧中的變量是從下向上釋放,這樣就保證了堆棧中先進後出的規則不與變量的生命週期起衝突!windows
堆棧的性能很是高,可是對於全部的變量來講還不太靈活,並且變量的生命週期必須嵌套。數組
一般咱們但願使用一種方法分配內存來存儲數據,而且方法退出後很長一段時間內數據仍然可使用。此時就要用到堆(託管堆)!緩存
堆(託管堆)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的值相等");
}
理解值類型與引用類型的區別在面向對象編程中很是關鍵。
一、類型,對象,堆棧和託管堆
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的全部技術中,最具爭議的恐怕是垃圾收集(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
1、棧和託管堆
通用類型系統(CTS)區分兩種基本類型:值類型和引用類型。它們之間的根本區別在於它們在內存中的存儲方式。.NET使用兩種不一樣的物理內存塊來存儲數據—棧和託管堆。以下圖所示:
二 類型層次結構
CTS定義了一種類型層次結構,該結構不只僅描述了不一樣的預約義類型,還指出了用戶定義類型在層次結構種的
文章來自:http://www.cnblogs.com/shenfengok/archive/2011/09/06/2169306.html