GC作為.NET的重要核心基礎,是必須要了解的。本文主要側重於GC內存管理中的一些關鍵點,如要要全面深入了解其精髓,最好還是多看看書。
1. 簡述一下一個引用對象的生命周期?
2. 創建下面對象實例,需要申請多少內存空間?
public class User { public int Age { get; set; } public string Name { get; set; } public string _Name = "123" + "abc"; public List<string> _Names; }
3. 什麼是垃圾?
4. GC是什麼,簡述一下GC的工作方式?
5. GC進行垃圾回收時的主要流程是?
6. GC在哪些情況下回進行回收工作?
7. using() 語法是如何確保對象資源被釋放的?如果內部出現異常依然會釋放資源嗎?
8. 解釋一下C#裡的析構函數?為什麼有些編程建議裡不推薦使用析構函數呢?
9. Finalize() 和 Dispose() 之間的區別?
10. Dispose和Finalize方法在何時被調用?
11. .NET中的托管堆中是否可能出現內存洩露的現象?
12. 在托管堆上創建新對象有哪幾種常見方式?
托管堆中存放引用類型對象,因此GC的內存管理的目標主要都是引用類型對象,本文中涉及的對象如無明確說明都指的是引用類型對象。
一個對象的生命周期簡單概括就是:創建>使用>釋放,在.NET中一個對象的生命周期:
那其中重要的一個環節,就是對象的創建,大部分的對象創建都是開始於關鍵字new。為什麼說是大部分呢,因為有個別引用類型是由專門IL指令的,比如string有ldstr指令(參考前面的文章:.NET面試題解析(03)-string與字符串操作),0基數組好像也有一個專門指令。
引用對象都是分配在托管堆上的, 先來看看托管堆的基本結構,如下圖,托管堆中的對象是順序存放的,托管堆維護著一個指針NextObjPtr,它指向下一個對象在堆中的分配位置。
創建一個新對象的主要流程:
以題目2中的代碼為例,模擬一個對象的創建過程:
public class User { public int Age { get; set; } public string Name { get; set; } public string _Name = "123" + "abc"; public List<string> _Names; }
GC是垃圾回收(Garbage Collect)的縮寫,是.NET核心機制的重要部分。她的基本工作原理就是遍歷托管堆中的對象,標記哪些被使用對象(那些沒人使用的就是所謂的垃圾),然後把可達對象轉移到一個連續的地址空間(也叫壓縮),其余的所有沒用的對象內存被回收掉。
首先,需要再次強調一下托管堆內存的結構,如下圖,很明確的表明了,只有GC堆才是GC的管轄區域,關於加載堆在前面文中有提到過(.NET面試題解析(04)-類型、方法與繼承)。GC堆裡面為了提高內存管理效率等因素,有分成多個部分,其中 兩個主要部分:
圖3(Figure-3)
什麼是垃圾?簡單理解就是沒有被引用的對象。
先假設所有對象都是垃圾,根據應用程序根指針Root遍歷堆上的每一個引用對象,生成可達對象圖,對於還在使用的對象(可達對象)進行標記(其實就是在對象同步索引塊中開啟一個標示位)。
其中Root根指針保存了當前所有需要使用的對象引用,他其實只是一個統稱,意思就是這些對象當前還在使用,主要包含:靜態對象/靜態字段的引用;線程棧引用(局部變量、方法參數、棧幀);任何引用對象的CPU寄存器;根引用對象中引用的對象;GC Handle table;Freachable隊列等。
針對所有不可達對象進行清除操作,針對普通對象直接回收內存,而對於實現了終結器的對象(實現了析構函數的對象)需要單獨回收處理。清除之後,內存就會變得不連續了,就是步驟3的工作了。
把剩下的對象轉移到一個連續的內存,因為這些對象地址變了,還需要把那些Root跟指針的地址修改為移動後的新地址。
垃圾回收的過程示意圖如下:
垃圾回收的過程是不是還挺辛苦的,因此建議不要隨意手動調用垃圾回收GC.Collect(),GC會選擇合適的時機、合適的方式進行內存回收的。
當然,實際的垃圾回收過程可能比上面的要復雜,如果沒次都掃描托管堆內的所有對象實例,這樣做太耗費時間而且沒有必要。分代(Generation)算法是CLR垃圾回收器采用的一種機制,它唯一的目的就是提升應用程序的性能。分代回收,速度顯然快於回收整個堆。分代(Generation)算法的假設前提條件:
1、大量新創建的對象生命周期都比較短,而較老的對象生命周期會更長
2、對部分內存進行回收比基於全部內存的回收操作要快
3、新創建的對象之間關聯程度通常較強。heap分配的對象是連續的,關聯度較強有利於提高CPU cache的命中率
如圖3,.NET將托管堆分成3個代齡區域: Gen 0、Gen 1、Gen 2:
大部分情況,GC只需要回收0代即可,這樣可以顯著提高GC的效率,而且GC使用啟發式內存優化算法,自動優化內存負載,自動調整各代的內存大小。
.NET中提供釋放非托管資源的方式主要是:Finalize() 和 Dispose()。
常用的大多是Dispose模式,主要實現方式就是實現IDisposable接口,下面是一個簡單的IDisposable接口實現方式。
public class SomeType : IDisposable { public MemoryStream _MemoryStream; public void Dispose() { if (_MemoryStream != null) _MemoryStream.Dispose(); } }
Dispose需要手動調用,在.NET中有兩中調用方式:
//方式1:顯示接口調用 SomeType st1=new SomeType(); //do sth st1.Dispose(); //方式2:using()語法調用,自動執行Dispose接口 using (var st2 = new SomeType()) { //do sth }
第一種方式,顯示調用,缺點顯而易見,如果程序猿忘了調用接口,則會造成資源得不到釋放。或者調用前出現異常,當然這一點可以使用try…finally避免。
一般都建議使用第二種實現方式,他可以保證無論如何Dispose接口都可以得到調用,原理其實很簡單,using()的IL代碼如下圖,因為using只是一種語法形式,本質上還是try…finally的結構。
首先了解下Finalize方法的來源,她是來自System.Object中受保護的虛方法Finalize,無法被子類顯示重寫,也無法顯示調用,是不是有點怪?。她的作用就是用來釋放非托管資源,由GC來執行回收,因此可以保證非托管資源可以被釋放。
所有實現了終結器(析構函數)的對象,會被GC特殊照顧,GC的終止化隊列跟蹤所有實現了Finalize方法(析構函數)的對象。
上面的過程是不是很復雜!是就對了,如果想徹底搞清楚,沒有捷徑,不要偷懶,還是去看書吧!
簡單總結一下:Finalize()可以確保非托管資源會被釋放,但需要很多額外的工作(比如終結對象特殊管理),而且GC需要執行兩次才會真正釋放資源。聽上去好像缺點很多,她唯一的優點就是不需要顯示調用。
有些編程意見或程序猿不建議大家使用Finalize,盡量使用Dispose代替,我覺得可能主要原因在於:第一是Finalize本身性能並不好;其次很多人搞不清楚Finalize的原理,可能會濫用,導致內存洩露。因此就干脆別用了,其實微軟是推薦大家使用的,不過是和Dispose一起使用,同時實現IDisposable接口和Finalize(析構函數),其實FCL中很多類庫都是這樣實現的,這樣可以兼具兩者的優點:
- 如果調用了Dispose,則可以忽略對象的終結器,對象一次就回收了;
- 如果程序猿忘了調用Dispose,則還有一層保障,GC會負責對象資源的釋放;
盡量不要手動執行垃圾回收的方法:GC.Collect()
垃圾回收的運行成本較高(涉及到了對象塊的移動、遍歷找到不再被使用的對象、很多狀態變量的設置以及Finalize方法的調用等等),對性能影響也較大,因此我們在編寫程序時,應該避免不必要的內存分配,也盡量減少或避免使用GC.Collect()來執行垃圾回收,一般GC會在最適合的時間進行垃圾回收。
而且還需要注意的一點,在執行垃圾回收的時候,所有線程都是要被掛起的(如果回收的時候,代碼還在執行,那對象狀態就不穩定了,也沒辦法回收了)。
推薦Dispose代替Finalize
如果你了解GC內存管理以及Finalize的原理,可以同時使用Dispose和Finalize雙保險,否則盡量使用Dispose。
選擇合適的垃圾回收機制:工作站模式、服務器模式
public class User { public int Age { get; set; } public string Name { get; set; } public string _Name = "123" + "abc"; public List<string> _Names; }
40字節內存空間,詳細分析文章中給出了。
一個變量如果在其生存期內的某一時刻已經不再被引用,那麼,這個對象就有可能成為垃圾
GC是垃圾回收(Garbage Collect)的縮寫,是.NET核心機制的重要部分。她的基本工作原理就是遍歷托管堆中的對象,標記哪些被使用對象(哪些沒人使用的就是所謂的垃圾),然後把可達對象轉移到一個連續的地址空間(也叫壓縮),其余的所有沒用的對象內存被回收掉。
① 標記:先假設所有對象都是垃圾,根據應用程序根Root遍歷堆上的每一個引用對象,生成可達對象圖,對於還在使用的對象(可達對象)進行標記(其實就是在對象同步索引塊中開啟一個標示位)。
② 清除:針對所有不可達對象進行清除操作,針對普通對象直接回收內存,而對於實現了終結器的對象(實現了析構函數的對象)需要單獨回收處理。清除之後,內存就會變得不連續了,就是步驟3的工作了。
③ 壓縮:把剩下的對象轉移到一個連續的內存,因為這些對象地址變了,還需要把那些Root跟指針的地址修改為移動後的新地址。
using() 只是一種語法形式,其本質還是try…finally的結構,可以保證Dispose始終會被執行。
C#裡的析構函數其實就是終結器Finalize,因為長得像C++裡的析構函數而已。
有些編程建議裡不推薦使用析構函數要原因在於:第一是Finalize本身性能並不好;其次很多人搞不清楚Finalize的原理,可能會濫用,導致內存洩露,因此就干脆別用了
Finalize() 和 Dispose()都是.NET中提供釋放非托管資源的方式,他們的主要區別在於執行者和執行時間不同:
另外一個重點區別就是終結器會導致對象復活一次,也就說會被GC回收兩次才最終完成回收工作,這也是有些人不建議開發人員使用終結器的主要原因。
是的,可能會。比如:
版權所有,文章來源:http://www.cnblogs.com/anding
個人能力有限,本文內容僅供學習、探討,歡迎指正、交流。
.NET面試題解析(00)-開篇來談談面試 & 系列文章索引
書籍:CLR via C#
書籍:你必須知道的.NET
.NET基礎拾遺(1)類型語法基礎和內存管理基礎
一個近乎完美的Finalize配合Dispose的設計模板
步步為營 C# 技術漫談 四、垃圾回收機制(GC)