【转】.net框架读书笔记---CLR内存管理\垃圾收集(一)
一、垃圾收集平臺基本原理解析
在C#中程序訪問一個資源需要以下步驟:
- 調(diào)用中間語言(IL)中的newobj指令,為表示某個特定資源的類型實例分配一定的內(nèi)存空間。
- 初始化上一步所得的內(nèi)存,設(shè)置資源的初始狀態(tài),從而使其可以為程序所用。一個類型的實例構(gòu)造器負(fù)責(zé)這樣的初始化工作。
- 通過訪問類型成員來使用資源。
- 銷毀資源狀態(tài),執(zhí)行清理工作。
- 釋放托管堆上面的內(nèi)存,該步驟由垃圾收集器全權(quán)負(fù)責(zé),值類型實例所占的內(nèi)存位于當(dāng)前運行線程的堆棧上,垃圾收集器并不負(fù)責(zé)這些資源的回收,當(dāng)值類型實例變量所在的方法執(zhí)行結(jié)束時,他們的內(nèi)存將隨著堆棧空間的消亡而自動消亡,無所謂回收。對于一些表示非托管的類型,在其對象被銷毀時,就必須執(zhí)行一些清理代碼。
當(dāng)應(yīng)用程序完成初始化后,CLR將保留(reserve)一塊連續(xù)的地址空間,這段空間最初并不對應(yīng)任何的物理內(nèi)存(backing storage)(該地址是一段虛擬地址空間,所以要真正使用它,它必須為其“提交”物理內(nèi)存),該地址空間即為托管堆。托管堆上維護(hù)著一個指針,暫且稱之為NextObjPtr。該指針標(biāo)識著下一個新建對象分配時在托管堆中所處的位置。剛開始的時候,NextObjPtr被設(shè)為CLR保留地址空間的基地址。
中間語言指令newObj負(fù)責(zé)創(chuàng)建新的對象。在代碼運行時,newobj指令將導(dǎo)致CLR執(zhí)行以下操作:
- 計算類型的所有實例字段(以及其基類型所有的字段)所需要的字節(jié)總數(shù)。
- 在前面所的字節(jié)總數(shù)的基礎(chǔ)上面再加上對象額為的附加成員所需的字節(jié)數(shù):一個方法指針和一個SyncBlockIndex。
- CLR檢查保留區(qū)域中的空間是否滿足分配新對象所需的字節(jié)數(shù)-----如果需要則提交物理內(nèi)存。如果滿足,對象將被分配在NextObjPtr指針?biāo)傅牡胤健=又愋偷膶嵗龢?gòu)造器被調(diào)用(NextObjPtr會被傳遞給this參數(shù)),IL指令newobj(或者說new操作符)返回其分配內(nèi)存地址。就在newobj指令返回新對象的地址之前,NextObjPtr指針會越過新對象所處的內(nèi)存區(qū)域,并指示出下一個新建對象在托管堆中的地址。
下圖演示了包含A,B,C三個對象的托管堆,如果再分配對象將會被放在NextObjPtr指針?biāo)菔镜奈恢?#xff08;緊跟C之后)
在C語言中堆分配內(nèi)存時,首先需要遍歷一個鏈表數(shù)據(jù)結(jié)構(gòu),一旦找到一個足夠大的內(nèi)存塊,該內(nèi)存塊就會被拆開來,同時鏈表相應(yīng)節(jié)點上的指針會得到適當(dāng)?shù)恼{(diào)整。但是對于托管堆來說,分配內(nèi)存僅僅意味著在指針上增加一個數(shù)值---顯然要比操作鏈表的做法快許多,C語言都是在找到自由空間為其對象分配內(nèi)存,因此連續(xù)創(chuàng)建幾個對象,他們將很有可能被分散在地址空間的各個角落。但是在托管堆中,連續(xù)分配的對象可以保證它們在內(nèi)存中也是連續(xù)的。
就目前來看托管堆在實現(xiàn)的簡單性和速度方面要遠(yuǎn)優(yōu)于C語言的運行時中的堆。之所以這樣是因為CLR做了大膽的假設(shè)---那就是應(yīng)用程序的地址空間和存儲空間是無限的,顯然這是不可能的。托管堆必須應(yīng)用某種機(jī)制來允許這種假設(shè)。這種機(jī)制就是垃圾回收器。
當(dāng)應(yīng)用程序調(diào)用new創(chuàng)建對象時,托管堆可能沒有足夠的地址空間來分配該對象。托管堆通過將對象所需要的字節(jié)總數(shù)添加到NextObjPtr指針表示的地址上來檢測這種情況。如果得到的結(jié)果超出了托管堆的地址空間范圍,那么托管堆將被認(rèn)為已滿,這時就需要垃圾收集器。,其實這種描述是過于簡單的,垃圾回收與對象的代齡有著密切的關(guān)系,還需繼續(xù)學(xué)習(xí)垃圾收集。
二、垃圾收集算法
垃圾收集器通過檢查托管堆中是否有應(yīng)用程序不再使用的對象來回收內(nèi)存。如果有這樣的對象,它們的內(nèi)存將被回收。那么垃圾收集器是這樣知道應(yīng)用程序是否正在使用一個對象呢??還得繼續(xù)學(xué)習(xí)。
每一個應(yīng)用程序都有一組根(root),一個根是一個存儲位置,其中包含著一個指向引用類型的內(nèi)存指針。該指針或者指向一個托管堆中的對象,或者被設(shè)置為null。例如所有的全局引用類型變量或靜態(tài)引用類型都被認(rèn)為是根。另外,一個線程堆棧上所有引用類型的本地變量或者參數(shù)變量也被認(rèn)為是一個根。最后,在一個方法內(nèi),指向引用類型對象的CPU寄存器也被認(rèn)為是一個根。
當(dāng)垃圾收集器開始執(zhí)行時,它首先假設(shè)托管堆中的所有對象都是可收集的垃圾。換句話,垃圾收集器假設(shè)應(yīng)用程序中沒有一個根引用著托管堆中的對象。然后垃圾收集器便利所有的根,構(gòu)造出一個包含所有可達(dá)對象的圖。例如,垃圾收集器可能會定位出一個引用托管對象的全局變量。下圖展示了分配有幾個對象的托管堆,其中對象A,C,D,F為應(yīng)用程序的根所直接引用。所有這些對象都是可達(dá)對象圖的一部分。當(dāng)對象D被添加到該圖中時,垃圾收集器注意到它還引用著對象H,于是對象H被添加到該圖,垃圾回收器就這樣子以遞歸的方式來遍歷應(yīng)用程序中所有的可達(dá)對象。
一旦該部分的可達(dá)對象完成后,垃圾回收器將檢查下一個根,并遍歷其引用的對象。當(dāng)垃圾回收器在對象之間進(jìn)行遍歷時,如果發(fā)現(xiàn)某對象已經(jīng)添加到可達(dá)對象圖中時(比如上圖中的H,在檢查D的時候已經(jīng)將其添加到了可達(dá)對象圖),它會停止沿著該對象標(biāo)識的路徑方向上遍歷的活動。兩個目的:
- 可以避免垃圾收集器對一些對象的多次遍歷,可高性能。
- 如果兩個對象之間出現(xiàn)了循環(huán)引用,可以避免遍歷陷入無限循環(huán)(比如上圖中D引用著H,而H又引用著D)。
垃圾收集器一旦檢查完所有的根,其得到的可達(dá)對象將包含所有從應(yīng)用程序的根可以訪問的對象。任何不在該圖中的對象將是應(yīng)用程序不可訪問的對象,不可達(dá)的對象,因此也是可以被執(zhí)行垃圾收集器的對象。垃圾收集器接著線性地遍歷托管堆以尋找包含可收集垃圾對象的連續(xù)區(qū)域。
PS:CLR的垃圾收集機(jī)制對我來說有點非主流,在此之前,我一直認(rèn)為是垃圾收集器直接去尋找不可達(dá)的對象,現(xiàn)在看來垃圾收集器使用了逆向思維,通過找到可達(dá)對象來找到不可達(dá)的對象(這個原因還得繼續(xù)思考)。
如果找到了較大的連續(xù)區(qū)域,垃圾收集器將會把內(nèi)存中的一些非垃圾對象搬移到這些連續(xù)區(qū)塊中以壓縮堆棧,顯然搬移內(nèi)存中的對象將使所有這些指向?qū)ο蟮闹羔樧兊臒o效。所以垃圾收集器必須修改應(yīng)用程序的根以使它們指向這些對象更新后的位置。另外,如果任何對象包含有指向這些對象的指針,那么垃圾收集器也會負(fù)責(zé)矯正它們。托管堆被壓縮以后,NextObjPtr指針將被設(shè)為指向最后一個非垃圾對象之后。下圖展示了對于上面圖執(zhí)行垃圾收集器后的托管堆。
可見垃圾回收器對于應(yīng)用程序的性能有不小的影響,CLR采用了代齡等措施來優(yōu)化了性能(以后學(xué)習(xí))。
因為任何不從應(yīng)用程序的根中訪問的對象都會在某個時刻被收集,所以應(yīng)用程序?qū)⒉豢赡馨l(fā)生內(nèi)存泄漏,另外應(yīng)用程序也不可能再訪問已經(jīng)被釋放的對象。因為如果對象可達(dá),它將不可能被釋放;而如果對象不可達(dá),應(yīng)用程序必將無法訪問到它。
下面代碼演示了垃圾收集器是這樣分配管理對象的:
class?Program
{
static?void?Main(?string?[] args)
{
//?在托管堆上ArrayList對象,a現(xiàn)在就是一個根
ArrayList a?=?new?ArrayList();
//?在托管堆上創(chuàng)建10000個對象
for?(?int?i?=?0?; i?<?10000?; i?++?)
{
a.Add(?new?Object());?//?對象被創(chuàng)建在托管堆上
}
//?現(xiàn)在a是一個根(位于線程堆棧上)。所以a是一個可達(dá)對象
//?,a引用的10000個對象也是可達(dá)對象
Console.WriteLine(a.Count);
//?在a.Count返回后,a便不再被Main中的代碼所引用,
//?因此也就不再是一個根。如果另外一個線程在a.Count的結(jié)果被
//?傳遞給WirteLine之前啟動了垃圾收集,那么a以及它所引用的10000個對象將會被回收。
//?上面for里面的變量i雖然在后面的代碼中不再被引用,但由于它是一個值類型,并不存在于
//?托管堆中,所以它不受垃圾收集器的管理,它在Main方法執(zhí)行完畢后會隨著堆棧的消失而自動
//?被系統(tǒng)回收
Console.WriteLine(?"?End of method?"?);
}
}
?
CLR之所以能夠使用垃圾回收機(jī)制,有一個原因是因為托管堆總是能知道一個對象的實際類型,從而使用其元數(shù)據(jù)信息來判斷一個對象的那些成員引用著其他對象。
?
------------------------------------------------------------------------------------------------------------
關(guān)于根的問題,好多朋友都討論了,其實書上已有更詳細(xì)的東西呢,只是那些000的東西我比較反感,非常抱歉,現(xiàn)在我貼出來,一起再學(xué)習(xí)一下:
當(dāng)JIT編譯器編譯一個方法的IL代碼時,除了產(chǎn)生本地CPU代碼外,JIT編譯器還會創(chuàng)建一個內(nèi)部邏輯表。從邏輯上來看,該表中的每一個條目都標(biāo)識著一個方法的本地CPU指令的字節(jié)偏移范圍,以及該范圍中一組包含根的內(nèi)存地址(或者CPU寄存器),下表描述了該內(nèi)存表:
| 起始字節(jié)偏移??????????????????????????????????????????? ? | ?結(jié)尾字節(jié)偏移????????????????????????????? ? | 根?????????????????????????????????????????????????????????????? |
| 0x00000000 | 0x00000020 | this,arg1,arg2,ECX,EDX |
| 0x00000021 | 0x00000122 | this,arg2,fs,EBX |
| 0x00000123 | 0x00000145 | fs |
如果在0x00000021和0x00000122之間的代碼執(zhí)行時開始執(zhí)行垃圾收集,那么垃圾收集器將知道參數(shù)this參數(shù)arg2,本地變量fs以及寄存器EBX都是根,他們引用的托管堆中的對象將不會被認(rèn)為是可收集的垃圾對象。除此之外,垃圾收集器還可以遍歷線程的調(diào)用堆棧,通過檢測其中每一個方法內(nèi)部表來確定所有調(diào)用方法中的根,最后,垃圾收集器使用其他一些手段獲得存儲在全局引用類型變量和靜態(tài)引用類型變量中保持的根。
在上表中方法的arg1參數(shù)在偏移為0x00000020處的指令執(zhí)行完畢后就不再被引用了,這意味著arg1引用的對象在該指令執(zhí)行后的任何時刻都可以被垃圾收集器收集(假設(shè)應(yīng)用程序中沒有其他的根再引用該對象),換句話說,只要一個對象不再可達(dá),它就是垃圾收集器的候選對象,CLR并不保證對象在一個方法的整個生存期內(nèi)都一直存活。
另外請大家關(guān)注11樓的答復(fù)。
一本書不要指望一次就能看懂啊
總結(jié)
以上是生活随笔為你收集整理的【转】.net框架读书笔记---CLR内存管理\垃圾收集(一)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 信用卡刷卡次数少会影响征信吗
- 下一篇: 【转】Postman系列五:Postma