.NET的GC理解誤區
最近面試了一些人,發現對.NET的GC(記憶體回收)的理解都存在錯誤。GC其實是相當複雜的系統,雖然95%的情況下我們並不需要考慮它,但仍有5%的情況我們不得不接觸GC體系來解決問題。比如這個問題:
void Func()
{
A a = new A();
B b = new B();
a.RefToB = b;
b.RefToA = a;
}
那麼a和b會不會被GC回收?好幾個人都答錯。如果你按照COM的模式去思考GC,那就完全錯誤了。每次我問“什麼情況下,對象會被GC回收?”,他們都能回答上來“當程式裡沒有對對象的引用時”。但是錯了,為什嗎?如果你還沒明白,就再看看上面這段代碼。
GC管理對象不是用的COM的引用計數模式。事實上最初微軟確實想用引用計數方式實現GC,這樣的一個優勢就是對象的析構時機是確定的,當引用計數為0時,對象會被析構,之後也不會再有任何代碼能夠再訪問該對象,這是很理想的情況。但經過反覆實驗,這種方法被拋棄了。一個原因就是如上的例子,會導致對象無法釋放。另一個重要原因就是應用計數的額外開銷對高效能程式不可接受。尤其是在多線程情況下,因為.NET使用自由執行緒模式,多個線程可能同時訪問一個對象,每一個引用計數的增減操作都不得不做線程同步。
.NET採用的GC模式是分代GC(Generational GC),堆空間按對象的生存期長短分成3代。新分配的對象在第0代,按地址順序分配,當第0代的空間(約幾百K)用光時,將程式裡能引用到的對象移動到第1代,那麼剩下的就是垃圾,第0代空間便可以重新用於分配。同理,第1代也按同樣的邏輯運行,那麼第2代裡的對象將都是生存期很長的對象。由此可以推出如下幾點:
1)對象的分配時間開銷遠小於C++的堆分配,但回收時間開銷大於分配時間開銷。
2)不會出現C++裡的堆片段過多的問題,有利於程式長時間運行。
3)循環參考的對象能夠被正確回收。
那麼再回答開始的問題,“什麼情況下,對象會被GC回收?”正確答案是“當程式裡沒有對對象的活引用(Alive reference)時。”
當然,實際情況比如上所述複雜得多,有興趣可以考慮如下問題:
1)Finalizer在哪裡、什麼時候執行?如果拋了異常會有什麼後果?
2)大於第0代空間的大數組是怎麼分配的?
3)為什麼要求Dispose()可以被調用多次?