漫談C#開發(fā)中的垃圾回收機(jī)制
GC的前世與今生
雖然本文是以.NET作為目標(biāo)來講述GC,但是GC的概念并非才誕生不久。早在1958年,由鼎鼎大名的圖林獎(jiǎng)得主John McCarthy所實(shí)現(xiàn)的Lisp語言就已經(jīng)提供了GC的功能,這是GC的***次出現(xiàn)。Lisp的程序員認(rèn)為內(nèi)存管理太重要了,所以不能由程序員自己來管理。但后來的日子里L(fēng)isp卻沒有成氣候,采用內(nèi)存手動(dòng)管理的語言占據(jù)了上風(fēng),以C為代表。出于同樣的理由,不同的人卻又不同的看法,C程序員認(rèn)為內(nèi)存管理太重要了,所以不能由系統(tǒng)來管理,并且譏笑Lisp程序慢如烏龜?shù)倪\(yùn)行速度。的確,在那個(gè)對每一個(gè)Byte都要精心計(jì)算的年代GC的速度和對系統(tǒng)資源的大量占用使很多人的無法接受。而后,1984年由Dave Ungar開發(fā)的Small talk語言***次采用了Generational garbage collection的技術(shù)(這個(gè)技術(shù)在下文中會(huì)談到),但是Small talk也沒有得到十分廣泛的應(yīng)用。
直到20世紀(jì)90年代中期GC才以主角的身份登上了歷史的舞臺(tái),這不得不歸功于Java的進(jìn)步,今日的GC已非吳下阿蒙。Java采用VM(Virtual Machine)機(jī)制,由VM來管理程序的運(yùn)行當(dāng)然也包括對GC管理。90年代末期.NET出現(xiàn)了,.NET采用了和Java類似的方法由CLR(Common Language Runtime)來管理。這兩大陣營的出現(xiàn)將人們引入了以虛擬平臺(tái)為基礎(chǔ)的開發(fā)時(shí)代,GC也在這個(gè)時(shí)候越來越得到大眾的關(guān)注。
為什么要使用GC呢?也可以說是為什么要使用內(nèi)存自動(dòng)管理?有下面的幾個(gè)原因:
1、提高了軟件開發(fā)的抽象度;
2、程序員可以將精力集中在實(shí)際的問題上而不用分心來管理內(nèi)存的問題;
3、可以使模塊的接口更加的清晰,減小模塊間的偶合;
4、大大減少了內(nèi)存人為管理不當(dāng)所帶來的Bug;
5、使內(nèi)存管理更加高效。
總的說來就是GC可以使程序員可以從復(fù)雜的內(nèi)存問題中擺脫出來,從而提高了軟件開發(fā)的速度、質(zhì)量和安全性。
什么是GC
GC如其名,就是垃圾收集,當(dāng)然這里僅就內(nèi)存而言。Garbage Collector(垃圾收集器,在不至于混淆的情況下也成為GC)以應(yīng)用程序的root為基礎(chǔ),遍歷應(yīng)用程序在Heap上動(dòng)態(tài)分配的所有對象[2],通過識(shí)別它們是否被引用來確定哪些對象是已經(jīng)死亡的哪些仍需要被使用。已經(jīng)不再被應(yīng)用程序的root或者別的對象所引用的對象就是已經(jīng)死亡的對象,即所謂的垃圾,需要被回收。這就是GC工作的原理。為了實(shí)現(xiàn)這個(gè)原理,GC有多種算法。比較常見的算法有Reference Counting,Mark Sweep,Copy Collection等等。目前主流的虛擬系統(tǒng).net CLR,Java VM和Rotor都是采用的Mark Sweep算法。
一、Mark-Compact 標(biāo)記壓縮算法 簡單把.NET的GC算法看作Mark-Compact算法
階段1: Mark-Sweep 標(biāo)記清除階段
先假設(shè)heap中所有對象都可以回收,然后找出不能回收的對象,給這些對象打上標(biāo)記,***heap中沒有打標(biāo)記的對象都是可以被回收的
階段2: Compact 壓縮階段
對象回收之后heap內(nèi)存空間變得不連續(xù),在heap中移動(dòng)這些對象,使他們重新從heap基地址開始連續(xù)排列,類似于磁盤空間的碎片整理
Heap內(nèi)存經(jīng)過回收、壓縮之后,可以繼續(xù)采用前面的heap內(nèi)存分配方法,即僅用一個(gè)指針記錄heap分配的起始地址就可以
主要處理步驟:將線程掛起=>確定roots=>創(chuàng)建reachable objectsgraph=>對象回收=>heap壓縮=>指針修復(fù)
可以這樣理解roots:heap中對象的引用關(guān)系錯(cuò)綜復(fù)雜(交叉引用、循環(huán)引用),形成復(fù)雜的graph,roots是CLR在heap之外可以找到的各種入口點(diǎn)。GC搜索roots的地方包括全局對象、靜態(tài)變量、局部對象、函數(shù)調(diào)用參數(shù)、當(dāng)前CPU寄存器中的對象指針(還有finalizationqueue)等。主要可以歸為2種類型:已經(jīng)初始化了的靜態(tài)變量、線程仍在使用的對象(stack+CPU register)
Reachable objects:指根據(jù)對象引用關(guān)系,從roots出發(fā)可以到達(dá)的對象。例如當(dāng)前執(zhí)行函數(shù)的局部變量對象A是一個(gè)rootobject,他的成員變量引用了對象B,則B是一個(gè)reachable object。從roots出發(fā)可以創(chuàng)建reachable objectsgraph,剩余對象即為unreachable,可以被回收
指針修復(fù)是因?yàn)閏ompact過程移動(dòng)了heap對象,對象地址發(fā)生變化,需要修復(fù)所有引用指針,包括stack、CPUregister中的指針以及heap中其他對象的引用指針
Debug和release執(zhí)行模式之間稍有區(qū)別,release模式下后續(xù)代碼沒有引用的對象是unreachable的,而debug模式下需要等到當(dāng)前函數(shù)執(zhí)行完畢,這些對象才會(huì)成為unreachable,目的是為了調(diào)試時(shí)跟蹤局部對象的內(nèi)容
傳給了COM+的托管對象也會(huì)成為root,并且具有一個(gè)引用計(jì)數(shù)器以兼容COM+的內(nèi)存管理機(jī)制,引用計(jì)數(shù)器為0時(shí)這些對象才可能成為被回收對象
Pinnedobjects指分配之后不能移動(dòng)位置的對象,例如傳遞給非托管代碼的對象(或者使用了fixed關(guān)鍵字),GC在指針修復(fù)時(shí)無法修改非托管代碼中的引用指針,因此將這些對象移動(dòng)將發(fā)生異常。pinnedobjects會(huì)導(dǎo)致heap出現(xiàn)碎片,但大部分情況來說傳給非托管代碼的對象應(yīng)當(dāng)在GC時(shí)能夠被回收掉
二、 Generational 分代算法 程序可能使用幾百M(fèi)、幾G的內(nèi)存,對這樣的內(nèi)存區(qū)域進(jìn)行GC操作成本很高,分代算法具備一定統(tǒng)計(jì)學(xué)基礎(chǔ),對GC的性能改善效果比較明顯
將對象按照生命周期分成新的、老的,根據(jù)統(tǒng)計(jì)分布規(guī)律所反映的結(jié)果,可以對新、老區(qū)域采用不同的回收策略和算法,加強(qiáng)對新區(qū)域的回收處理力度,爭取在較短時(shí)間間隔、較小的內(nèi)存區(qū)域內(nèi),以較低成本將執(zhí)行路徑上大量新近拋棄不再使用的局部對象及時(shí)回收掉
分代算法的假設(shè)前提條件:
1、大量新創(chuàng)建的對象生命周期都比較短,而較老的對象生命周期會(huì)更長
2、對部分內(nèi)存進(jìn)行回收比基于全部內(nèi)存的回收操作要快
3、新創(chuàng)建的對象之間關(guān)聯(lián)程度通常較強(qiáng)。heap分配的對象是連續(xù)的,關(guān)聯(lián)度較強(qiáng)有利于提高CPU cache的命中率
.NET將heap分成3個(gè)代齡區(qū)域: Gen 0、Gen 1、Gen 2
Heap分為3個(gè)代齡區(qū)域,相應(yīng)的GC有3種方式: # Gen 0 collections, # Gen 1 collections, #Gen 2 collections。如果Gen 0 heap內(nèi)存達(dá)到閥值,則觸發(fā)0代GC,0代GC后Gen 0中幸存的對象進(jìn)入Gen1。如果Gen 1的內(nèi)存達(dá)到閥值,則進(jìn)行1代GC,1代GC將Gen 0 heap和Gen 1 heap一起進(jìn)行回收,幸存的對象進(jìn)入Gen2。2代GC將Gen 0 heap、Gen 1 heap和Gen 2 heap一起回收
Gen 0和Gen 1比較小,這兩個(gè)代齡加起來總是保持在16M左右;Gen2的大小由應(yīng)用程序確定,可能達(dá)到幾G,因此0代和1代GC的成本非常低,2代GC稱為fullGC,通常成本很高。粗略的計(jì)算0代和1代GC應(yīng)當(dāng)能在幾毫秒到幾十毫秒之間完成,Gen 2 heap比較大時(shí)fullGC可能需要花費(fèi)幾秒時(shí)間。大致上來講.NET應(yīng)用運(yùn)行期間2代、1代和0代GC的頻率應(yīng)當(dāng)大致為1:10:100。
三、Finalization Queue和Freachable Queue
這兩個(gè)隊(duì)列和.net對象所提供的Finalize方法有關(guān)。這兩個(gè)隊(duì)列并不用于存儲(chǔ)真正的對象,而是存儲(chǔ)一組指向?qū)ο蟮闹羔?。?dāng)程序中使用了new操作符在Managed Heap上分配空間時(shí),GC會(huì)對其進(jìn)行分析,如果該對象含有Finalize方法則在Finalization Queue中添加一個(gè)指向該對象的指針。在GC被啟動(dòng)以后,經(jīng)過Mark階段分辨出哪些是垃圾。再在垃圾中搜索,如果發(fā)現(xiàn)垃圾中有被Finalization Queue中的指針?biāo)赶虻膶ο?,則將這個(gè)對象從垃圾中分離出來,并將指向它的指針移動(dòng)到Freachable Queue中。這個(gè)過程被稱為是對象的復(fù)生(Resurrection),本來死去的對象就這樣被救活了。為什么要救活它呢?因?yàn)檫@個(gè)對象的Finalize方法還沒有被執(zhí)行,所以不能讓它死去。Freachable Queue平時(shí)不做什么事,但是一旦里面被添加了指針之后,它就會(huì)去觸發(fā)所指對象的Finalize方法執(zhí)行,之后將這個(gè)指針從隊(duì)列中剔除,這是對象就可以安靜的死去了。.net framework的System.GC類提供了控制Finalize的兩個(gè)方法,ReRegisterForFinalize和SuppressFinalize。前者是請求系統(tǒng)完成對象的Finalize方法,后者是請求系統(tǒng)不要完成對象的Finalize方法。
ReRegisterForFinalize方法其實(shí)就是將指向?qū)ο蟮闹羔樦匦绿砑拥紽inalization Queue中。這就出現(xiàn)了一個(gè)很有趣的現(xiàn)象,因?yàn)樵贔inalization Queue中的對象可以復(fù)生,如果在對象的Finalize方法中調(diào)用ReRegisterForFinalize方法,這樣就形成了一個(gè)在堆上永遠(yuǎn)不會(huì)死去的對象,像鳳凰涅槃一樣每次死的時(shí)候都可以復(fù)生。
托管資源:
Net中的所有類型都是(直接或間接)從System.Object類型派生的。
CTS中的類型被分成兩大類——引用類型(reference type,又叫托管類型[managed type]),分配在內(nèi)存堆上,值類型(value type)。值類型分配在堆棧上。如圖
值類型在棧里,先進(jìn)后出,值類型變量的生命有先后順序,這個(gè)確保了值類型變量在推出作用域以前會(huì)釋放資源。比引用類型更簡單和高效。堆棧是從高地址往低地址分配內(nèi)存。
引用類型分配在托管堆(Managed Heap)上,聲明一個(gè)變量在棧上保存,當(dāng)使用new創(chuàng)建對象時(shí),會(huì)把對象的地址存儲(chǔ)在這個(gè)變量里。托管堆相反,從低地址往高地址分配內(nèi)存,如圖
.net中超過80%的資源都是托管資源。
非托管資源:
ApplicationContext,Brush,Component,ComponentDesigner,Container,Context,
Cursor,FileStream,Font,Icon,Image,Matrix,Object,OdbcDataReader,OleDBDataReader,
Pen,Regex,Socket,StreamWriter,Timer,Tooltip ,文件句柄,GDI資源,數(shù)據(jù)庫連接等等資源。可能在使用的時(shí)候很多都沒有注意到!
.NET的GC機(jī)制有這樣兩個(gè)問題:
首先,GC并不是能釋放所有的資源。它不能自動(dòng)釋放非托管資源。
第二,GC并不是實(shí)時(shí)性的,這將會(huì)造成系統(tǒng)性能上的瓶頸和不確定性。
GC并不是實(shí)時(shí)性的,這會(huì)造成系統(tǒng)性能上的瓶頸和不確定性。所以有了IDisposable接口,IDisposable接口定義了Dispose方法,這個(gè)方法用來供程序員顯式調(diào)用以釋放非托管資源。使用using 語句可以簡化資源管理。
示例
- /// <summary>
- /// 執(zhí)行SQL語句,返回影響的記錄數(shù)
- /// </summary>
- /// <param name="SQLString">SQL語句</param>
- /// <returns>影響的記錄數(shù)</returns>
- public static int ExecuteSql(string SQLString)
- {
- using (SqlConnection connection = new SqlConnection(connectionString))
- {
- using (SqlCommand cmd = new SqlCommand(SQLString, connection))
- {
- try
- {
- connection.Open();
- int rows = cmd.ExecuteNonQuery();
- return rows;
- }
- catch (System.Data.SqlClient.SqlException e)
- {
- connection.Close();
- throw e;
- }
- finally
- {
- cmd.Dispose();
- connection.Close();
- }
- }
- }
- }
當(dāng)你用Dispose方法釋放未托管對象的時(shí)候,應(yīng)該調(diào)用GC.SuppressFinalize。如果對象正在終結(jié)隊(duì)列(finalization queue),GC.SuppressFinalize會(huì)阻止GC調(diào)用Finalize方法。因?yàn)镕inalize方法的調(diào)用會(huì)犧牲部分性能。如果你的Dispose方法已經(jīng)對委托管資源作了清理,就沒必要讓GC再調(diào)用對象的Finalize方法(MSDN)。附上MSDN的代碼,大家可以參考.
GC.Collect() 方法
- public class BaseResource : IDisposable
- {
- // 指向外部非托管資源
- private IntPtr handle;
- // 此類使用的其它托管資源.
- private Component Components;
- // 跟蹤是否調(diào)用.Dispose方法,標(biāo)識(shí)位,控制垃圾收集器的行為
- private bool disposed = false;
- // 構(gòu)造函數(shù)
- public BaseResource()
- {
- // Insert appropriate constructor code here.
- }
- // 實(shí)現(xiàn)接口IDisposable.
- // 不能聲明為虛方法virtual.
- // 子類不能重寫這個(gè)方法.
- public void Dispose()
- {
- Dispose(true);
- // 離開終結(jié)隊(duì)列Finalization queue
- // 設(shè)置對象的阻止終結(jié)器代碼
- //
- GC.SuppressFinalize(this);
- }
- // Dispose(bool disposing) 執(zhí)行分兩種不同的情況.
- // 如果disposing 等于 true, 方法已經(jīng)被調(diào)用
- // 或者間接被用戶代碼調(diào)用. 托管和非托管的代碼都能被釋放
- // 如果disposing 等于false, 方法已經(jīng)被終結(jié)器 finalizer 從內(nèi)部調(diào)用過,
- //你就不能在引用其他對象,只有非托管資源可以被釋放。
- protected virtual void Dispose(bool disposing)
- {
- // 檢查Dispose 是否被調(diào)用過.
- if (!this.disposed)
- {
- // 如果等于true, 釋放所有托管和非托管資源
- if (disposing)
- {
- // 釋放托管資源.
- Components.Dispose();
- }
- // 釋放非托管資源,如果disposing為 false,
- // 只會(huì)執(zhí)行下面的代碼.
- CloseHandle(handle);
- handle = IntPtr.Zero;
- // 注意這里是非線程安全的.
- // 在托管資源釋放以后可以啟動(dòng)其它線程銷毀對象,
- // 但是在disposed標(biāo)記設(shè)置為true前
- // 如果線程安全是必須的,客戶端必須實(shí)現(xiàn)。
- }
- disposed = true;
- }
- // 使用interop 調(diào)用方法
- // 清除非托管資源.
- [System.Runtime.InteropServices.DllImport("Kernel32")]
- private extern static Boolean CloseHandle(IntPtr handle);
- // 使用C# 析構(gòu)函數(shù)來實(shí)現(xiàn)終結(jié)器代碼
- // 這個(gè)只在Dispose方法沒被調(diào)用的前提下,才能調(diào)用執(zhí)行。
- // 如果你給基類終結(jié)的機(jī)會(huì).
- // 不要給子類提供析構(gòu)函數(shù).
- ~BaseResource()
- {
- // 不要重復(fù)創(chuàng)建清理的代碼.
- // 基于可靠性和可維護(hù)性考慮,調(diào)用Dispose(false) 是***的方式
- Dispose(false);
- }
- // 允許你多次調(diào)用Dispose方法,
- // 但是會(huì)拋出異常如果對象已經(jīng)釋放。
- // 不論你什么時(shí)間處理對象都會(huì)核查對象的是否釋放,
- // check to see if it has been disposed.
- public void DoSomething()
- {
- if (this.disposed)
- {
- throw new ObjectDisposedException();
- }
- }
- // 不要設(shè)置方法為virtual.
- // 繼承類不允許重寫這個(gè)方法
- public void Close()
- {
- // 無參數(shù)調(diào)用Dispose參數(shù).
- Dispose();
- }
- public static void Main()
- {
- // Insert code here to create
- // and use a BaseResource object.
- }
- }
作用:強(qiáng)制進(jìn)行垃圾回收。
GC的方法:
名稱 |
說明 |
Collect() |
強(qiáng)制對所有代進(jìn)行即時(shí)垃圾回收。 |
Collect(Int32) |
強(qiáng)制對零代到指定代進(jìn)行即時(shí)垃圾回收。 |
Collect(Int32, GCCollectionMode) |
強(qiáng)制在 GCCollectionMode 值所指定的時(shí)間對零代到指定代進(jìn)行垃圾回收。 |
GC注意事項(xiàng):
1、只管理內(nèi)存,非托管資源,如文件句柄,GDI資源,數(shù)據(jù)庫連接等還需要用戶去管理
2、循環(huán)引用,網(wǎng)狀結(jié)構(gòu)等的實(shí)現(xiàn)會(huì)變得簡單。GC的標(biāo)志也壓縮算法能有效的檢測這些關(guān)系,并將不再被引用的網(wǎng)狀結(jié)構(gòu)整體刪除。
3、GC通過從程序的根對象開始遍歷來檢測一個(gè)對象是否可被其他對象訪問,而不是用類似于COM中的引用計(jì)數(shù)方法。
4、GC在一個(gè)獨(dú)立的線程中運(yùn)行來刪除不再被引用的內(nèi)存
5、GC每次運(yùn)行時(shí)會(huì)壓縮托管堆
6、你必須對非托管資源的釋放負(fù)責(zé)??梢酝ㄟ^在類型中定義Finalizer來保證資源得到釋放。
7、對象的Finalizer被執(zhí)行的時(shí)間是在對象不再被引用后的某個(gè)不確定的時(shí)間。注意并非和C++中一樣在對象超出聲明周期時(shí)立即執(zhí)行析構(gòu)函數(shù)
8、Finalizer的使用有性能上的代價(jià)。需要Finalization的對象不會(huì)立即被清除,而需要先執(zhí)行Finalizer.Finalizer不是在GC執(zhí)行的線程被調(diào)用。GC把每一個(gè)需要執(zhí)行Finalizer的對象放到一個(gè)隊(duì)列中去,然后啟動(dòng)另一個(gè)線程來執(zhí)行所有這些Finalizer.而GC線程繼續(xù)去刪除其他待回收的對象。在下一個(gè)GC周期,這些執(zhí)行完Finalizer的對象的內(nèi)存才會(huì)被回收。
9、.NET GC使用"代"(generations)的概念來優(yōu)化性能。代幫助GC更迅速的識(shí)別那些最可能成為垃圾的對象。在上次執(zhí)行完垃圾回收后新創(chuàng)建的對象為第0代對象。經(jīng)歷了一次GC周期的對象為第1代對象。經(jīng)歷了兩次或更多的GC周期的對象為第2代對象。代的作用是為了區(qū)分局部變量和需要在應(yīng)用程序生存周期中一直存活的對象。大部分第0代對象是局部變量。成員變量和全局變量很快變成第1代對象并最終成為第2代對象。
10、GC對不同代的對象執(zhí)行不同的檢查策略以優(yōu)化性能。每個(gè)GC周期都會(huì)檢查第0代對象。大約1/10的GC周期檢查第0代和第1代對象。大約1/100的GC周期檢查所有的對象。重新思考Finalization的代價(jià):需要Finalization的對象可能比不需要Finalization在內(nèi)存中停留額外9個(gè)GC周期。如果此時(shí)它還沒有被Finalize,就變成第2代對象,從而在內(nèi)存中停留更長時(shí)間。
原文鏈接:http://www.cnblogs.com/springyangwc/archive/2011/06/13/2080149.html
【編輯推薦】
- 漫談C#開發(fā)中的反射機(jī)制
- 漫談C#開發(fā)中的ASP.NET頁生命周期
- 淺析C#延遲加載的運(yùn)行機(jī)制和應(yīng)用場景
- C#多線程中l(wèi)ock的用法
- 用C#實(shí)現(xiàn)HTTP協(xié)議下的多線程文件傳輸