.NET面試題解析(06)-GC與內存管理

GC做爲.NET的重要核心基礎,是必需要了解的。本文主要側重於GC內存管理中的一些關鍵點,如要要全面深刻了解其精髓,最好仍是多看看書。html

  常見面試題目:

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. 什麼是垃圾?spring

4. GC是什麼,簡述一下GC的工做方式?編程

5. GC進行垃圾回收時的主要流程是?數組

6. GC在哪些狀況下回進行回收工做?性能優化

7. using() 語法是如何確保對象資源被釋放的?若是內部出現異常依然會釋放資源嗎?服務器

8. 解釋一下C#裏的析構函數?爲何有些編程建議裏不推薦使用析構函數呢?函數

9. Finalize() 和 Dispose() 之間的區別?性能

10. Dispose和Finalize方法在什麼時候被調用?

11. .NET中的託管堆中是否可能出現內存泄露的現象?

12. 在託管堆上建立新對象有哪幾種常見方式?

  深刻GC與內存管理

託管堆中存放引用類型對象,所以GC的內存管理的目標主要都是引用類型對象,本文中涉及的對象如無明確說明都指的是引用類型對象。

微笑 對象建立及生命週期

一個對象的生命週期簡單歸納就是:建立>使用>釋放,在.NET中一個對象的生命週期:

  • new建立對象並分配內存
  • 對象初始化
  • 對象操做、使用
  • 資源清理(非託管資源)
  • GC垃圾回收

那其中重要的一個環節,就是對象的建立,大部分的對象建立都是開始於關鍵字new。爲何說是大部分呢,由於有個別引用類型是由專門IL指令的,好比string有ldstr指令(參考前面的文章:.NET面試題解析(03)-string與字符串操做),0基數組好像也有一個專門指令。

引用對象都是分配在託管堆上的, 先來看看託管堆的基本結構,以下圖,託管堆中的對象是順序存放的,託管堆維護着一個指針NextObjPtr,它指向下一個對象在堆中的分配位置。

建立一個新對象的主要流程

image

以題目2中的代碼爲例,模擬一個對象的建立過程:

public class User
{
    public int Age { get; set; }
    public string Name { get; set; }

    public string _Name = "123" + "abc";
    public List<string> _Names;
}
  • 對象大小估算,共計40個字節
    • 屬性Age值類型Int,4字節;
    • 屬性Name,引用類型,初始爲NULL,4個字節,指向空地址;
    • 字段_Name初始賦值了,由前面的文章(.NET面試題解析(03)-string與字符串操做)可知,代碼會被編譯器優化爲_Name=」123abc」。一個字符兩個字節,字符串佔用2×6+8(附加成員:4字節TypeHandle地址,4字節同步索引塊)=20字節,總共內存大小=字符串對象20字節+_Name指向字符串的內存地址4字節=24字節;
    • 引用類型字段List<string> _Names初始默認爲NULL,4個字節;
    • User對象的初始附加成員(4字節TypeHandle地址,4字節同步索引塊)8個字節;
  • 內存申請: 申請44個字節的內存塊,從指針NextObjPtr開始驗證,空間是否足夠,若不夠則觸發垃圾回收。
  • 內存分配: 從指針NextObjPtr處開始劃分44個字節內存塊。
  • 對象初始化: 首先初始化對象附加成員,再調用User對象的構造函數,對成員初始化,值類型默認初始爲0,引用類型默認初始化爲NULL;
  • 託管堆指針後移: 指針NextObjPtr後移44個字節。
  • 返回內存地址: 返回對象的內存地址給引用變量。

吐舌笑臉 GC垃圾回收

GC是垃圾回收(Garbage Collect)的縮寫,是.NET核心機制的重要部分。她的基本工做原理就是遍歷託管堆中的對象,標記哪些被使用對象(那些沒人使用的就是所謂的垃圾),而後把可達對象轉移到一個連續的地址空間(也叫壓縮),其他的全部沒用的對象內存被回收掉。

首先,須要再次強調一下託管堆內存的結構,以下圖,很明確的代表了,只有GC堆纔是GC的管轄區域,關於加載堆在前面文中有提到過(.NET面試題解析(04)-類型、方法與繼承)。GC堆裏面爲了提升內存管理效率等因素,有分紅多個部分,其中 兩個主要部分:

  • 0/1/2代:代齡(Generation)在後面有專門說到;
  • 大對象堆(Large Object Heap),大於85000字節的大對象會分配到這個區域,這個區域的主要特色就是:不會輕易被回收;就是回收了也不會被壓縮(由於對象太大,移動複製的成本過高了);

image

圖3(Figure-3)

什麼是垃圾?簡單理解就是沒有被引用的對象。

垃圾回收的基本流程包含如下三個關鍵步驟:

① 標記

先假設全部對象都是垃圾,根據應用程序根指針Root遍歷堆上的每個引用對象,生成可達對象圖,對於還在使用的對象(可達對象)進行標記(其實就是在對象同步索引塊中開啓一個標示位)。

其中Root根指針保存了當前全部須要使用的對象引用,他其實只是一個統稱,意思就是這些對象當前還在使用,主要包含:靜態對象/靜態字段的引用;線程棧引用(局部變量、方法參數、棧幀);任何引用對象的CPU寄存器;根引用對象中引用的對象;GC Handle table;Freachable隊列等。

② 清除

針對全部不可達對象進行清除操做,針對普通對象直接回收內存,而對於實現了終結器的對象(實現了析構函數的對象)須要單獨回收處理。清除以後,內存就會變得不連續了,就是步驟3的工做了。

③ 壓縮

把剩下的對象轉移到一個連續的內存,由於這些對象地址變了,還須要把那些Root跟指針的地址修改成移動後的新地址。

垃圾回收的過程示意圖以下:

alt

 

垃圾回收的過程是否是還挺辛苦的,所以建議不要隨意手動調用垃圾回收GC.Collect(),GC會選擇合適的時機、合適的方式進行內存回收的。

吐舌笑臉 關於代齡(Generation)

固然,實際的垃圾回收過程可能比上面的要複雜,若是沒次都掃描託管堆內的全部對象實例,這樣作太耗費時間並且沒有必要。分代(Generation)算法是CLR垃圾回收器採用的一種機制,它惟一的目的就是提高應用程序的性能。分代回收,速度顯然快於回收整個堆。分代(Generation)算法的假設前提條件:

一、大量新建立的對象生命週期都比較短,而較老的對象生命週期會更長
二、對部份內存進行回收比基於所有內存的回收操做要快
三、新建立的對象之間關聯程度一般較強。heap分配的對象是連續的,關聯度較強有利於提升CPU cache的命中率

如圖3,.NET將託管堆分紅3個代齡區域: Gen 0、Gen 一、Gen 2:

  • 第0代,最新分配在堆上的對象,歷來沒有被垃圾收集過。任何一個新對象,當它第一次被分配在託管堆上時,就是第0代(大於85000的大對象除外)。 
  • 第1代,0代滿了會觸發0代的垃圾回收,0代垃圾回收後,剩下的對象會搬到1代。 
  • 第2代,當0代、1代滿了,會觸發0代、1代的垃圾回收,第0代升爲第1代,第1代升爲第2代。

alt

大部分狀況,GC只須要回收0代便可,這樣能夠顯著提升GC的效率,並且GC使用啓發式內存優化算法,自動優化內存負載,自動調整各代的內存大小。

吐舌笑臉 非託管資源回收

.NET中提供釋放非託管資源的方式主要是:Finalize() 和 Dispose()。

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的結構。

image

 

Finalize() :終結器(析構函數)

首先了解下Finalize方法的來源,她是來自System.Object中受保護的虛方法Finalize,沒法被子類顯示重寫,也沒法顯示調用,是否是有點怪?。她的做用就是用來釋放非託管資源,由GC來執行回收,所以能夠保證非託管資源能夠被釋放。

  • 沒法被子類顯示重寫:.NET提供相似C++析構函數的形式來實現重寫,所以也有稱之爲析構函數,但其實她只是外表和C++裏的析構函數像而已。
  • 沒法顯示調用:由GC來管理和執行釋放,不須要手動執行了,不再用擔憂猿們忘了調用Dispose了。

全部實現了終結器(析構函數)的對象,會被GC特殊照顧,GC的終止化隊列跟蹤全部實現了Finalize方法(析構函數)的對象。

  • 當CLR在託管堆上分配對象時,GC檢查該對象是否實現了自定義的Finalize方法(析構函數)。若是是,對象會被標記爲可終結的,同時這個對象的指針被保存在名爲終結隊列的內部隊列中。終結隊列是一個由垃圾回收器維護的表,它指向每個在從堆上刪除以前必須被終結的對象。
  • 當GC執行而且檢測到一個不被使用的對象時,須要進一步檢查「終結隊列」來查詢該對象類型是否含有Finalize方法,若是沒有則將該對象視爲垃圾,若是存在則將該對象的引用移動到另一張Freachable列表,此時對象會被複活一次
  • CLR將有一個單獨的高優先級線程負責處理Freachable列表,就是依次調用其中每一個對象的Finalize方法,而後刪除引用,這時對象實例就被視爲再也不被使用,對象再次變成垃圾
  • 下一個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。

選擇合適的垃圾回收機制:工做站模式、服務器模式

  題目答案解析:

1. 簡述一下一個引用對象的生命週期?

  • new建立對象並分配內存
  • 對象初始化
  • 對象操做、使用
  • 資源清理(非託管資源)
  • GC垃圾回收

2. 建立下面對象實例,須要申請多少內存空間?

public class User
{
    public int Age { get; set; }
    public string Name { get; set; }

    public string _Name = "123" + "abc";
    public List<string> _Names;
}

40字節內存空間,詳細分析文章中給出了。

3. 什麼是垃圾?

一個變量若是在其生存期內的某一時刻已經再也不被引用,那麼,這個對象就有可能成爲垃圾

4. GC是什麼,簡述一下GC的工做方式?

GC是垃圾回收(Garbage Collect)的縮寫,是.NET核心機制的重要部分。她的基本工做原理就是遍歷託管堆中的對象,標記哪些被使用對象(哪些沒人使用的就是所謂的垃圾),而後把可達對象轉移到一個連續的地址空間(也叫壓縮),其他的全部沒用的對象內存被回收掉。

5. GC進行垃圾回收時的主要流程是?

① 標記:先假設全部對象都是垃圾,根據應用程序根Root遍歷堆上的每個引用對象,生成可達對象圖,對於還在使用的對象(可達對象)進行標記(其實就是在對象同步索引塊中開啓一個標示位)。

② 清除:針對全部不可達對象進行清除操做,針對普通對象直接回收內存,而對於實現了終結器的對象(實現了析構函數的對象)須要單獨回收處理。清除以後,內存就會變得不連續了,就是步驟3的工做了。

③ 壓縮:把剩下的對象轉移到一個連續的內存,由於這些對象地址變了,還須要把那些Root跟指針的地址修改成移動後的新地址。

6. GC在哪些狀況下回進行回收工做?

  • 內存不足溢出時(0代對象充滿時)
  • Windwos報告內存不足時,CLR會強制執行垃圾回收
  • CLR卸載AppDomian,GC回收全部
  • 調用GC.Collect
  • 其餘狀況,如主機拒絕分配內存,物理內存不足,超出短時間存活代的存段門限

7. using() 語法是如何確保對象資源被釋放的?若是內部出現異常依然會釋放資源嗎?

using() 只是一種語法形式,其本質仍是try…finally的結構,能夠保證Dispose始終會被執行。

8. 解釋一下C#裏的析構函數?爲何有些編程建議裏不推薦使用析構函數呢?

C#裏的析構函數其實就是終結器Finalize,由於長得像C++裏的析構函數而已。

有些編程建議裏不推薦使用析構函數要緣由在於:第一是Finalize自己性能並很差;其次不少人搞不清楚Finalize的原理,可能會濫用,致使內存泄露,所以就乾脆別用了

9. Finalize() 和 Dispose() 之間的區別?

Finalize() 和 Dispose()都是.NET中提供釋放非託管資源的方式,他們的主要區別在於執行者和執行時間不一樣:

  • finalize由垃圾回收器調用;dispose由對象調用。
  • finalize無需擔憂由於沒有調用finalize而使非託管資源得不到釋放,而dispose必須手動調用。
  • finalize不能保證當即釋放非託管資源,Finalizer被執行的時間是在對象再也不被引用後的某個不肯定的時間;而dispose一調用便釋放非託管資源。
  • 只有class類型才能重寫finalize,而結構不能;類和結構都能實現IDispose。

另一個重點區別就是終結器會致使對象復活一次,也就說會被GC回收兩次才最終完成回收工做,這也是有些人不建議開發人員使用終結器的主要緣由。

10. Dispose和Finalize方法在什麼時候被調用?

  • Dispose一調用便釋放非託管資源;
  • Finalize不能保證當即釋放非託管資源,Finalizer被執行的時間是在對象再也不被引用後的某個不肯定的時間;

11. .NET中的託管堆中是否可能出現內存泄露的現象?

是的,可能會。好比:

  • 不正確的使用靜態字段,致使大量數據沒法被GC釋放;
  • 沒有正確執行Dispose(),非託管資源沒有獲得釋放;
  • 不正確的使用終結器Finalize(),致使沒法正常釋放資源;
  • 其餘不正確的引用,致使大量託管對象沒法被GC釋放;

12. 在託管堆上建立新對象有哪幾種常見方式?

  • new一個對象;
  • 字符串賦值,如string s1=」abc」;
  • 值類型裝箱;

版權全部,文章來源:http://www.cnblogs.com/anding

我的能力有限,本文內容僅供學習、探討,歡迎指正、交流。

.NET面試題解析(00)-開篇來談談面試 & 系列文章索引

  參考資料:

書籍:CLR via C#

書籍:你必須知道的.NET

.NET基礎拾遺(1)類型語法基礎和內存管理基礎

一個近乎完美的Finalize配合Dispose的設計模板

步步爲營 C# 技術漫談 4、垃圾回收機制(GC)

相關文章
相關標籤/搜索