前言
.net運行庫通過記憶體回收行程自動處理回收託管資源,非託管的資源需要手動編碼處理。理解記憶體管理的工作原理,有助於提高應用程式的速度和效能。廢話少說,切入正題。主要闡述的概念見:
概念
記憶體:又稱為虛擬記憶體,或虛擬位址空間,windows使用虛擬定址系統,在後台自動將可用的記憶體位址映射到硬體記憶體中的實際地址上,其結果便是32位處理器上的每個進程都可以使用4GB的記憶體,用來存放程式的所有部分,包括可執行代碼(exe檔案),代碼載入的所有DLL,程式運行時使用的所有變數的內容。
記憶體棧
在進程的虛擬記憶體中,存在的一個變數的生存期必須嵌套的地區。
記憶體堆
在進程的虛擬記憶體中,在方法退出後的很長一段時間內資料仍是可用的地區。
託管資源
記憶體回收行程在後台能自動處理的資源
非託管資源
需要手動編碼,通過解構函式,Finalize,IDisposable,Using等機制或方法處理的資源。
記憶體棧
實值型別資料存放區在記憶體棧中,參考型別的執行個體地址值也放在記憶體棧中(見記憶體堆的討論),記憶體棧的工作原理,透過下面一段代碼理解:
{ //block1開始 int a; //solve something {//block2開始 int b; // solve something else }//block2結束}//block1結束
以上代碼注意2點:
1)C#中變數的範圍,遵循先聲明的後超出範圍,後聲明的先超出範圍,即b先釋放,a後釋放,釋放順序總是與它們分配記憶體的順序相反。
2)b在一個單獨的塊範圍(block2)中,而a所在的塊名稱為block1,其內嵌套著block2。
請看下面:
棧記憶體管理中,始終都維護著一個棧指標,它始終指向站地區中下一個可用的地址,名字為sp,,假定它指向編號為1000的地址。
變數a 首先入棧,假定機子是32位的,int型佔4個位元組,即997~1000,入棧後,sp指向996,可見記憶體棧的增長方向為從高地址向低地址方向。
然後b入棧,佔據993~996,sp指向992。當超越塊block2 時,變數b立即釋放在記憶體棧上的儲存,sp增加4個位元組,指向996。
向外走,超越塊block1 時,變數a 立即釋放,此時sp再增加4個位元組,指向原來的初始地址1000,後面再入棧時,這些地址再被佔用,然後再被釋放,迴圈往複。
記憶體堆
儘管棧有非常高的效能,但對於所有的變數它還是不太靈活,因為位於記憶體棧上的變數的生存期必須嵌套。許多情況下,這種要求過於苛刻,因為我們希望有些資料在方法退出後的很長一段時間內還是可用的。
只要是用new運算子來請求的堆儲存空間,就滿足資料聲明期延時性,例如所有的參考型別。在.net中使用託管堆來管理記憶體堆上的資料。
.net中的託管堆和C++使用的堆不同,它在記憶體回收行程的控制下工作,而C++的堆是低級的。
既然參考型別的資料存放區在託管堆上,那麼它們是如何儲存的呢?請看下面代碼
void Shout(){ Monkey xingxing; //猴子類 xingxing = new Monkey();}
在這段代碼中,假定兩個類Monkey和AIMonkey,其中AIMonkey類擴充了Monkey對象。
在這裡,我們稱Monkey為一個對象,稱xingxing為它的一個執行個體。
首先,聲明了一個Monkey引用xingxing,在棧上給這個引用分配儲存空間,記住這僅是一個引用,而不是實際的Monkey對象。記住這一點很重要!!!
然後看下第2行代碼:
xingxing = new Monkey();
它完成的操作:首先,它分配堆上的記憶體,以儲存Monkey對象,注意了!!!這是一個真正的對象,它不是一個佔用4個位元組的地址!!! 假定Monkey對象佔用64個位元組,這64個位元組包含了Monkey執行個體的欄位,和.NET中用於識別和管理Monkey類執行個體的一些資訊。這64個位元組實在記憶體堆上分配的,假定記憶體堆上的地址1937~2000。new操作符返回一個記憶體位址,假定為997~1000,並賦值給xingxing。如下所示:
記住一點:
與記憶體棧不同的是,堆上的記憶體是向上分配的,由低地址到高地址。
從上面的例子中,可以看出建立引用執行個體的過程要比建立值變數的過程更複雜,系統開銷更大。那麼既然開銷這麼大,它到底優勢何在呢?引用資料類型強大到底在哪裡???
請看下面代碼:
{//block1 Monkey xingxing; //猴子類 xingxing = new Monkey(); {//block2 Monkey jingjing = xingxing; //jingjing也引用了Monkey對象 //do something } //jinjing超出範圍,它從棧中刪除 //現在只有xingxing還在引用Monkey}//xingxing超出範圍,它從棧中刪除//現在沒有在引用Monkey的了
把一個引用執行個體的值xingxing賦值予另一個相同類型的執行個體jingjing,這樣的結果便是有兩個引用記憶體中的同一個對象Monkey了。當一個執行個體超出範圍時,它會從棧中刪除,但引用對象的資料還是保留在堆中,一直到程式終止,或記憶體回收行程回收它位置,而只有該資料不再有任何執行個體引用它時,它才會被刪除!
隨便舉一個實際應用引用的簡單例子:
//從介面抓取資料放到list中List<Person> persons = getPersonsFromUI();//retrieve these persons from DBList<person> personsFromDB = retrievePersonsFromDB();//do something to personsFromDBgetSomethingToPersonsFromDB();
請問對personsFromDB的改變,能在介面上及時相應出來嗎?
不能!
請看下面修改代碼:
//從介面抓取資料放到list中List<Person> persons = getPersonsFromUI();//retrieve these persons from DBList<Person> personsFromDB = retrievePersonsFromDB();int cnt = persons.Count;for(int i=0;i<cnt;i++){ persons[i]= personsFromDB [i] ;} //do something to personsFromDBgetSomethingToPersonsFromDB();
修改後,資料能立即響應在介面上。因為persons與UI綁定,所有修改在persons上,自然可以立即響應。
這就是引用資料類型的強大之處,在C#.NET中廣泛使用了這個特性。這表明,我們可以對資料的生存期進行非常強大的控制,因為只要保持對資料的引用,該資料就肯定位於堆上!!!
這也表明了基於棧的執行個體與基於堆的對象的生存期不匹配!
記憶體回收行程 GC
記憶體堆上會有片段形成,.NET記憶體回收行程會壓縮記憶體堆,移動對象和修改對象的所有引用的地址,這是託管的堆與非託管的堆的區別之一。
.NET的託管堆只需要讀取堆積指標的值即可,但是非託管的舊堆需要遍曆地址鏈表,找出一個地方來放置新資料,所以在.NET下執行個體化對象要快得多。
堆的第一部分稱為第0代,這部分駐留了最新的對象。在第0代記憶體回收過程中遺留下來的舊對象放在第1代對應的部分上,依次遞迴下去。。。
承上啟下
以上部分便是對託管資源的記憶體管理部分,這些都是在後台由.NET自動執行的。下面看下非託管資源的記憶體管理,比如這些資源可能是UI控制代碼,network串連,檔案控制代碼,Image對象等。.NET主要通過三種機制來做這件事。分別為解構函式、IDisposable介面,和兩者的結合處理方法,以此實現最好的處理結果。下面分別看一下。
解構函式
C#編譯器在編譯解構函式時,它會隱式地把解構函式的代碼編譯為等價於Finalize()方法的代碼,並確定執行父類的Finalize()方法。看下面的代碼:
public class Person{ ~Person() { //析構實現 }}
~Person()解構函式產生的IL的C#代碼:
protected override void Finalize(){ try { //析構實現 } finally { base.Finalize(); }}
放在finally塊中確保父類的Finalize()一定調用。
C#解構函式要比C++解構函式的使用少很多,因為它的問題是不確定性。在銷毀C++對象時,其解構函式會立即執行。但由於C#使用記憶體回收行程,無法確定C#對象的解構函式何時執行。如果對象佔用了 寶貴的資源,而需要儘快釋放資源,此時就不能等待記憶體回收行程來釋放了。
第一次調用解構函式時,有解構函式的對象需要第二次調用解構函式,才會真正刪除對象。如果頻繁使用析構,對效能的影響非常大。
IDisposable介面
在C#中,推薦使用IDisposable介面替代解構函式,該模式為釋放非託管資源提供了確定的機制,而不像析構那樣何時執行不確定。
假定Person對象依賴於某些外部資源,且實現IDisposable介面,如果要釋放它,可以這樣:
class Person:IDisposable{ public void Dispose() { //implementation }}Person xingxing = new Person();//dom somethingxingxing .Dispose();
上面代碼如果在處理過程中出現異常,這段代碼就沒有釋放xingxing,所以修改為:
Person xingxing = null;try{ xingxing = new Person(); //do something}finally{ if(xingxing !=null) { xingxing.Dispose(); }}
C#提供了一種文法糖,叫做using,來簡化以上操作。
using(Person xingxing = new Person()){ // do something}
using在此處的語義不同於普通的引用類庫作用。using用在此處的功能,僅僅是簡化了代碼,這種文法糖可以少用!!!
總之,實現IDisposable的對象,在釋放非託管資源時,必須手動調用Dispose()方法。因此一旦忘記,就會造成資源泄漏。如下所示:
Image backImage = this.BackgroundImage; if (backImage != null) { backImage.Dispose(); SessionToImage.DeleteImage(_imageFilePath, _imageFileName); this.BackgroundImage = null; }
在上面那個例子中,backImage已經確定不再用了,並且backImage又是通過Image.FromFile(fullPathWay)從物理磁碟上讀取的,是非託管的資源,所以需要Dispose()一下,這樣讀取Image的這個進程就被關閉了。如果忘記寫backImage.Dispose();就會造成資源泄漏!
結合 解構函式和IDisposable這2種機制
一般情況下,最好的方法是實現兩種機制,獲得這兩種機制的優點。因為正確調用Dispose()方法,同時把實現解構函式作為一種安全機制,以防沒有調用Dispose()方法。請參考一種結合兩種方法釋放託管和非託管資源的機制:
public class Person:IDisposable{ private bool isDisposed = false; //實現IDisposable介面 public void Dispose() { //為true表示清理託管和非託管資源 Dispose(true); //告訴記憶體回收行程不要調用解構函式了 GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { //isDisposed: 是否對象已經被清理掉了 if(!isDisposed) { if(disposing) { //清理託管資源 } //清理非託管資源 } isDisposed = true; } ~Person() { //false:調用後只清理非託管資源 //託管資源會被記憶體回收行程的一個單獨線程Finalize() Dispose(false); }}
當這個對象的使用者,直接調用了Dispose()方法,比如
Person xingxing = new Person();//do somethingperson.Dispose();
此時調用IDisposable.Dispose()方法,指定應清理所有與該對象相關的資源,包括託管和非託管資源。
如果未調用Dispose()方法,則是由解構函式處理掉託管和非託管資源。