一、前言
這個問題涉及了垃圾回收的內部機制,在通常情況下程序員並不需要去關心和干涉GC的內部執行,但是理解其算法,可以幫助程序員理解哪些代碼是高效的,而哪些代碼是需要避免的。
二、什么是代
GC在執行垃圾回收時,並不是每次都掃描托管堆內的所有對象實例,這樣做太耗費時間而且也沒有必要。簡單來說,GC會把所有托管堆內的對象按照其已經不再被使用的可能性分成三類,並且從最有可能不被使用的類別開始掃描,.NET對這樣的分類類別有一個稱呼:代。GC會把所有的托管堆內對象分為3代:0代、1代和2代,其基本機制如下:
- 並不是每次垃圾回收都會同時回收3個代的所有對象,越小的代擁有着越多被釋放的機會。CLR的基本算法是:每執行N次0代的回收,才會執行一次1代的回收,而每執行N次1代的回收,才會執行一次2代的回收。
- 當某個對象實例在GC執行時被發現仍然在使用,它將被移動到下一個代上。新分配的對象實例屬於0代。
- 根據.NET的垃圾回收機制,0代、1代和2代的初始分配空間分別為256KB,2MB和10MB。
垃圾回收中代的設計,是參考了這樣一個事實:一個對象實例存活的時間越長,那它就具有更大的幾率去存活更長的時間。而反過來理解,最有可能馬上就不被使用的對象實例,往往是那些剛剛才被分配的對象實例,而且新分配的對象實例通常都會被馬上大量地使用。這就是為什么0代對象擁有最多被釋放的機會,並且.NET只為0代分配了一塊相當小的邏輯內存,以使得0代對象的處理有機會被全部放入處理器的緩存中取,這樣做的結果是使用頻率最高並且最有可能馬上可以釋放的對象實例擁有了最高的使用頻率和最快的釋放速度。
相對於0代的快速釋放,1代和2代的對象將具有較少被釋放的機會。需要注意的是,當一次GC回收發現扔在被使用的對象實例時,會把它移到更高的代上。這就需要程序員避免保留已經不再被使用的對象引用,把對象的引用設置為null是告訴.NET該對象不需要再使用的最直接的方法。
我們現在再回頭分析一個Finalize方法,就可以知道它為何會大幅度地影響性能了。在前面已經講述了需要執行Finalize方法的對象被回收時的具體步驟:它會被暫時視為正在被使用而駐留在堆中,且至少要等一個GC循環才能被釋放,這取決於執行Finalize方法的線程的執行速度。很顯然,需要執行Finalize方法的那些對象實例,被真正釋放時最樂觀的情況下也已經處在1代的位置上了,而如果它們是在1代上才開始釋放或者執行Finalize方法的線程運行得慢了一點,那該對象就將在2代上才被釋放,相對於0代,這樣的對象實例在堆中存留的時間將長得多。
三、總結
垃圾回收機制按照對象不被使用的可能性把托管堆內的對象分為3代:0代、1代和2代。越小的代有越多被釋放的機會,而每一次GC中扔存活的對象實例將被移到下一代上。