.net 垃圾回收机制
盡管在.NET framework下我們并不需要擔(dān)心內(nèi)存管理和垃圾回收(Garbage Collection),但是我們還是應(yīng)該了解它們,以優(yōu)化我們的應(yīng)用程序。同時,還需要具備一些基礎(chǔ)的內(nèi)存管理工作機制的知識,這樣能夠有助于解釋我們?nèi)粘3绦蚓帉懼械淖兞康男袨椤T诒疚闹形覀儗⑸钊肜斫饫厥掌?#xff0c;還有如何利用靜態(tài)類成員來使我們的應(yīng)用程序更高效。
* 更小的步伐 == 更高效的分配
為了更好地理解為什么更小的足跡會更高效,這需要我們對.NET的內(nèi)存分配和垃圾回收專研得更深一些。
* 圖解:
讓我們來仔細看看GC。如果我們需要負責(zé)"清除垃圾",那么我們需要擬定一個高效的方案。很顯然,我們需要決定哪些東西是垃圾而哪些不是。
為了決定哪些是需要保留的,我們首先假設(shè)所有的東西都不是垃圾(墻角里堆著的舊報紙,閣樓里貯藏的廢物,壁櫥里的所有東西,等等)。假設(shè)在我們的生活當(dāng)中有兩位朋友:Joseph Ivan Thomas(JIT)和Cindy Lorraine Richmond(CLR)。Joe和Cindy知道它們在使用什么,而且給了我們一張列表說明了我們需要需要些什么。我們將初始列表稱之為"根"列表,因為我們將它用作起始點。我們需要保存一張主列表來記錄出我們家中的必備物品。任何能夠使必備物品正常工作或使用的東西也將被添加到列表中來(如果我們要看電視,那么就不能扔掉遙控器,所以遙控器將被添加到列表。如果我們要使用電腦,那么鍵盤和顯示器就得添加到列表)。
這就是GC如何保存我們的物品的,它從即時編譯器(JIT)和通用語言運行時(CLR)中獲得"根"對象引用的列表,然后遞歸地搜索出其他對象引用來建立一張我們需要保存的物品的圖表。
根包括:
* 全局/靜態(tài)指針。為了使我們的對象不被垃圾回收掉的一種方式是將它們的引用保存在靜態(tài)變量中。
* 棧上的指針。我們不想丟掉應(yīng)用程序中需要執(zhí)行的線程里的東西。
* CPU寄存器指針。托管堆中哪些被CPU寄存器直接指向的內(nèi)存地址上的東西必須得保留。
在以上圖片中,托管堆中的對象1、3、5都被根所引用,其中1和5時直接被引用,而3時在遞歸查找時被發(fā)現(xiàn)的。像我們之前的假設(shè)一樣,對象1是我們的電視機,對象3是我們的遙控器。在所有對象被遞歸查找出來之后我們將進入下一步--壓縮。
* 壓縮
我們現(xiàn)在已經(jīng)繪制出哪些是我們需要保留的對象,那么我們就能夠通過移動"保留對象"來對托管堆進行整理。
幸運的是,在我們的房間里沒有必要為了放入別的東西而去清理空間。因為對象2已經(jīng)不再需要了,所以GC會將對象3移下來,同時修復(fù)它指向?qū)ο?的指針。
然后,GC將對象5也向下移,
現(xiàn)在所有的東西都被清理干凈了,我們只需要寫一張便簽貼到壓縮后的堆上,讓Claire(指CLR)知道在哪兒放入新的對象就行了。
理解GC的本質(zhì)會讓我們明白對象的移動是非常費力的。可以看出,假如我們能夠減少需要移動的物品大小是非常有意義的,通過更少的拷貝動作能夠使我們提升整個GC的處理性能。
* 托管堆之外是怎樣的情景呢?
作為負責(zé)垃圾回收的人員,有一個容易出現(xiàn)入的問題是在打掃房間時如何處理車里的東西,當(dāng)我們打掃衛(wèi)生時,我們需要將所有物品清理干凈。那家里的臺燈和車里的電池怎么辦?
在一些情況下,GC需要執(zhí)行代碼來清理非托管資源(如文件,數(shù)據(jù)庫連接,網(wǎng)絡(luò)連接等),一種可能的方式是通過finalizer來進行處理。
class Sample
在對象創(chuàng)建期間,所有帶有finalizer的對象都將被添加到一個finalizer隊列中。對象1、4、5都有finalizer,且都已在finalizer隊列當(dāng)中。讓我們來看看當(dāng)對象2和4在應(yīng)用程序中不再被引用,且系統(tǒng)正準(zhǔn)備進行垃圾回收時會發(fā)生些什么。
對象2會像通常情況下那樣被垃圾回收器回收,但是當(dāng)我們處理對象4時,GC發(fā)現(xiàn)它存在于finalizer隊列中,那么GC就不會回收對象4的內(nèi)存空間,而是將對象4的finalizer移到一個叫做"freachable"的特殊隊列中。
有一個專門的線程來執(zhí)行freachable隊列中的項,對象4的finalizer一旦被該線程所處理,就將從freachable隊列中被移除,然后對象4就等待被回收。
因此對象4將存活至下一輪的垃圾回收。
由于在類中添加一個finalizer會增加GC的工作量,這種工作是十分昂貴的,而且會影響垃圾回收的性能和我們的程序。最好只在你確認需要finalizer時才使用它。
在清理非托管資源時有一種更好的方法:在顯式地關(guān)閉連接時,使用IDisposalbe接口來代替finalizer進行清理工作會更好些。
* IDisposable
實現(xiàn)IDisposable接口的類需要執(zhí)行Dispose()方法來做清理工作(這個方法是IDisposable接口中唯一的簽名)。因此假如我們使用如下的帶有finalizer的ResourceUser類:
public class ResourceUser?
{
~ResourceUser() // THIS IS A FINALIZER
{
// DO CLEANUP HERE
????????? }
}
我們可以使用IDisposable來以更好的方式實現(xiàn)相同的功能:
public class ResourceUser : IDisposable
{
IDisposable Members
}
IDisposable被集成在了using塊當(dāng)中。在using()方法中聲明的對象在using塊的結(jié)尾處將調(diào)用Dispose()方法,using塊之外該對象將不再被引用,因為它已經(jīng)被認為是需要進行垃圾回收的對象了。
public static void DoSomething()
{
ResourceUser rec = new ResourceUser();
using (rec)
{
// DO SOMETHING?
} // DISPOSE CALLED HERE
// DON'T ACCESS rec HERE
}
我更喜歡將對象聲明放到using塊中,因為這樣可視化很強,而且rec對象在using塊的作用域之外將不再有效。這種模式的寫法更符合IDisposable接口的初衷,但這并不是必須的。
public static void DoSomething()
{
using (ResourceUser rec = new ResourceUser())
{
// DO SOMETHING
} // DISPOSE CALLED HERE
}
在類中使用using()塊來實現(xiàn)IDisposable接口,能夠使我們在清理垃圾對象時不需要寫額外的代碼來強制GC回收我們的對象。
* 靜態(tài)方法
靜態(tài)方法屬于一種類型,而不是對象的實例,它允許創(chuàng)建能夠被類所共享的方法,且能夠達到"減肥"的效果,因為只有靜態(tài)方法的指針(8 bytes)在內(nèi)存當(dāng)中移動。靜態(tài)方法實體僅在應(yīng)用程序生命周期的早期被一次性加載,而不是在我們的類實例中生成。當(dāng)然,方法越大那么將其作為靜態(tài)就越高效。假如我們的方法很小(小于8 bytes),那么將其作為靜態(tài)方法反而會影響性能,因為這時指針比它指向的方法所占的空間還大些。
接著來看看例子...
我們的類中有一個公共的方法SayHello():
class Dude
{
private string _Name = "Don";
public void SayHello()
{
??????????????????? Console.WriteLine(this._Name + " says Hello");
????????? }
}
在每一個Dude類實例中SayHello()方法都會占用內(nèi)存空間。
一種更高效的方式是采用靜態(tài)方法,這樣我們只需要在內(nèi)存中放置唯一的SayHello()方法,而不論存在多少個Dude類實例。因為靜態(tài)成員不是實例成員,我們不能使用this指針來進行方法的引用。
class Dude
{
private string _Name = "Don";
public static void SayHello(string pName)
{
??????????????????? Console.WriteLine(pName + " says Hello");
????????? }
}
請注意我們在傳遞變量時棧上發(fā)生了些什么(可以參看<第二部分>)。我們需要通過例子的看看是否需要使用靜態(tài)方法來提升性能。例如,一個靜態(tài)方法需要很多參數(shù)而且沒有什么復(fù)雜的邏輯,那么在使用靜態(tài)方法時我們可能會降低性能。
* 靜態(tài)變量:注意了!
對于靜態(tài)變量,有兩件事情我們需要注意。假如我們的類中有一個靜態(tài)方法用于返回一個唯一值,而下面的實現(xiàn)會造成bug:
class Counter
{
private static int s_Number = 0;
public static int GetNextNumber()
{
int newNumber = s_Number;
// DO SOME STUFF????????
??????????????????? s_Number = newNumber + 1;
return newNumber;
????????? }
}
假如有兩個線程同時調(diào)用GetNextNumber()方法,而且它們在s_Number的值增加前都為newNumber分配了相同的值,那么它們將返回同樣的結(jié)果!
我們需要顯示地為方法中的靜態(tài)變量鎖住讀/寫內(nèi)存的操作,以保證同一時刻只有一個線程能夠執(zhí)行它們。線程管理是一個非常大的主題,而且有很多途徑可以解決線程同步的問題。使用lock關(guān)鍵字能讓代碼塊在同一時刻僅能夠被一個線程訪問。一種好的習(xí)慣是,你應(yīng)該盡量鎖較短的代碼,因為在程序執(zhí)行l(wèi)ock代碼塊時所有線程都要進入等待隊列,這是非常低效的。
class Counter
{
private static int s_Number = 0;
public static int GetNextNumber()
{
lock (typeof(Counter))
{
int newNumber = s_Number;
// DO SOME STUFF
???????????????????????????? newNumber += 1;
???????????????????????????? s_Number = newNumber;
return newNumber;
??????????????????? }
????????? }
}
* 靜態(tài)變量:再次注意了!
靜態(tài)變量引用需要注意的另一件事情是:記住,被"root"引用的事物是不會被GC清理掉的。我遇到過的一個最煩人的例子:
class Olympics
{
public static Collection<Runner> TryoutRunners;
}
class Runner
{
private string _fileName;
private FileStream _fStream;
public void GetStats()
{
??????????????????? FileInfo fInfo = new FileInfo(_fileName);
??????????????????? _fStream = _fileName.OpenRead();
????????? }
}
由于Runner集合在Olympics類中是靜態(tài)的,不僅集合中的對象不會被GC釋放(它們都直接被根所引用),而且你可能注意到了,每次執(zhí)行GetStats()方法時都會為那個文件開放一個文件流,因為它沒有被關(guān)閉所以也不會被GC釋放,這個代碼將會給系統(tǒng)造成很大的災(zāi)難。假如我們有100000個運動員來參加奧林匹克,那么會由于太多不可回收的對象而難以釋放內(nèi)存。天啦,多差勁的性能呀!
* Singleton
有一種方法可以保證一個類的實例在內(nèi)存中始終保持唯一,我們可以采用Gof中的Singleton模式。(Gof:Gang of Four,一部非常具有代表性的設(shè)計模式書籍的作者別稱,歸納了23種常用的設(shè)計模式)
public class Earth
{
private static Earth _instance = new Earth();
private Earth() { }
public static Earth GetInstance() { return _instance; }
}
我們的Earth類有一個私有構(gòu)造器,所以Earth類能夠執(zhí)行它的構(gòu)造器來創(chuàng)建一個Earth實例。我們有一個Earth類的靜態(tài)實例,還有一個靜態(tài)方法來獲得這個實例。這種特殊的實現(xiàn)是線程安全的,因為CLR保證了靜態(tài)變量的創(chuàng)建是線程安全的。這是我認為在C#中實現(xiàn)singleton模式最為明智的方式。
* .NET Framework 2.0中的靜態(tài)類
在.NET 2.0 Framework中我們有一種靜態(tài)類,此類中的所有成員都是靜態(tài)的。這中特性對于工具類是非常有用的,而且能夠節(jié)省內(nèi)存空間,因為該類只存在于內(nèi)存中的某個地方,不能在任何情況下被實例化。
* 總結(jié)一下...
總的來說,我們能夠提升GC表現(xiàn)的方式有:
1. 清理工作。不要讓資源一直打開!盡可能地保證關(guān)閉所有打開的連接,清除所有非托管的資源。當(dāng)使用非托管對象時,初始化工作盡量完些,清理工作要盡量及時點。
2. 不要過度地引用。需要時才使用引用對象,記住,如果你的對象是活動著的,所有被它引用的對象都不會被垃圾回收。當(dāng)我們想清理一些類所引用的事物,可以通過將這些引用設(shè)置為null來移除它們。我喜歡采用的一種方式是將未使用的引用指向一個輕量級的NullObject來避免產(chǎn)生null引用的異常。在GC進行垃圾回收時,更少的引用將減少映射處理的壓力。
3. 少使用finalizer。Finalizer在垃圾回收時是非常昂貴的資源,我們應(yīng)該只在必要時使用。如果我們使用IDisposable來代替finalizer會更高效些,因為我們的對象能夠直接被GC回收而不是在第二次回收時進行。
4. 盡量保持對象和它們的子對象在一塊兒。GC在復(fù)制大塊內(nèi)存數(shù)據(jù)來放到一起時是很容易的,而復(fù)制堆中的碎片是很費勁的,所以當(dāng)我們聲明一個包含許多其他對象的對象時,我們應(yīng)該在初始化時盡量讓他們在一塊兒。
5. 最后,使用靜態(tài)方法來保持對象的輕便也是可行的。
轉(zhuǎn)載于:https://www.cnblogs.com/as_as/archive/2010/07/13/1776405.html
總結(jié)
以上是生活随笔為你收集整理的.net 垃圾回收机制的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 一年后,我又来了
- 下一篇: 敏捷个人:提供更多文档下载,并转载一篇敏