(轉(zhuǎn)).NET面試題系列[5] - 垃圾回收:概念與策略

面試出現(xiàn)頻率:經(jīng)常出現(xiàn),但通常不會問的十分深入但汞。通常來說宿刮,看完我這篇文章就足夠應(yīng)付面試了互站。面試時主要考察垃圾回收的基本概念私蕾,標(biāo)記-壓縮算法,以及對于微軟的垃圾回收模板的理解胡桃。知道什么時候需要繼承IDisposible接口踩叭,解構(gòu)函數(shù)是做什么用的,什么時候需要自己寫一個解構(gòu)函數(shù)翠胰。

重要程度:10/10

參考書籍:CLR via C#容贝,其對垃圾回收講解的十分詳細(xì),有些內(nèi)容甚至過于高深之景。熟悉垃圾回收可以使你的程序更加健壯斤富,性能更好。

4.1 托管堆的構(gòu)造

垃圾回收的主要操作對象是托管堆锻狗,托管堆包括GC堆和加載堆满力。

GC堆里面為了提高內(nèi)存管理效率等因素,分成多個部分轻纪,其中兩個主要部分為:

  • 0/1/2代:越大的代的堆空間越大油额。
  • 大對象堆(Large Object Heap),大于85000字節(jié)的大對象會分配到這個區(qū)域刻帚,這個區(qū)域的主要特點就是:不會輕易被回收潦嘶;就是回收了也不會被壓縮(因為對象太大,移動復(fù)制的成本太高)崇众。大對象堆是第二代GC堆的一部分掂僵。

加載堆不受GC管轄。加載堆上的主要對象有類型對象和它們的靜態(tài)字段顷歌,字符串駐留池等锰蓬。幾個非托管資源的例子:StreamWriter,數(shù)據(jù)庫連接對象等衙吩。

4.2 關(guān)于垃圾

  • 垃圾是不會再被用到的資源互妓。具體的情況則包括超出該變量的有效范圍(離開了對應(yīng)的大括號的區(qū)域變量),將變量指定為null,重新指向其他物件(而原先指向的物件已無法被取得)冯勉,重新初始化等澈蚌,這時原先變量占有的空間都會被CLR視為垃圾而等待回收。

  • 托管代碼/資源/物件是會被CLR管理的代碼(CLR會對它們進(jìn)行內(nèi)存管理灼狰,垃圾回收宛瞄,線程管理等),反之則是非托管代碼交胚。

  • C#的值類型(如果它屬于托管代碼)存儲在棧中份汗。使用完(離開其作用域)就立刻銷毀。

  • C#的引用類型(如果它屬于托管代碼)存儲在棧和堆中蝴簇。使用完(離開其作用域)棧上的資料立刻銷毀杯活,而堆上(棧上所引用的資料指向堆上的一塊空間)的資料不立刻銷毀。銷毀時間根據(jù)其世代而定熬词。

4.3 簡述GC的垃圾回收策略

  • GC將整個托管堆分成0代旁钧,1代和2代三個區(qū)域。更高的世代的區(qū)域更大互拾。所有的引用對象一開始都是在第0代分配地址歪今。進(jìn)行垃圾回收時,大部分情況都是只對某個特定代進(jìn)行操作颜矿。這樣分配基于下面幾個假設(shè):
    1.越老的對象生存期越長(即還可能繼續(xù)生存很長一段時間)
    2.回收堆的一部分快于回收整個堆

  • 當(dāng)程序調(diào)用new操作符創(chuàng)建對象時寄猩,會計算類型(及其所有基類型)的字段需要的字節(jié)數(shù)。如果托管堆已經(jīng)沒有足夠的空間來創(chuàng)建新對象了(第0代滿)骑疆,就觸發(fā)一次垃圾回收田篇。

  • 整個回收將會遍歷0,1,2三代區(qū)域,并先標(biāo)記封断,后壓縮斯辰,標(biāo)記了的所有0代垃圾被銷毀,幸存者移到第一代堆中坡疼。標(biāo)記了的所有1代垃圾被銷毀彬呻,幸存者被移到第2代堆中。所有第二代堆的垃圾將會被銷毀柄瑰。幸存者仍然在第2代堆中闸氮。

  • GC使用的垃圾回收算法是先標(biāo)記(垃圾),之后壓縮教沾,將垃圾清理法瑟,釋放腥沽,將幸存者升代挫剑,使得垃圾釋放空出來的位置變得連續(xù)。類似于磁盤空間的碎片整理孙咪。連續(xù)的空間便于管理和建立新的對象。

  • 具體一點說巡语,每個應(yīng)用程序都包含一組根翎蹈,每個根都是一個存儲位置,其中包含指向引用類型對象的一個指針男公。該指針要么引用堆中的一個對象荤堪,要么為null。

  • GC開始執(zhí)行時枢赔,假設(shè)堆上所有的對象都是垃圾澄阳。在標(biāo)記階段,GC沿著線程棧開始遍歷踏拜,檢查每個根是否為null碎赢。對于那些有引用對象的根,則不認(rèn)為它們是垃圾执隧。

  • 可以通過呼叫GC.Collect來主動觸發(fā)一次垃圾回收(甚至可以指定某代)揩抡,但通常這是沒必要的户侥。

4.4 何時需要繼承IDisposible接口镀琉?

你可以繼承IDisposible接口,然后在Dispose方法中銷毀任何資源蕊唐,包括非托管資源屋摔。但如果你忘記了調(diào)用它,那么你的非托管資源將沒有任何機會得到釋放替梨。只有當(dāng)你的類型含有非托管資源钓试,或者實現(xiàn)了IDisposible的托管資源時,你才需要繼承IDisposible接口副瀑,實現(xiàn)一個Dispose弓熏。 如果你只面對一堆托管資源,并且它們都沒有實現(xiàn)IDisposible時糠睡,你不需要做任何事挽鞠。

4.5 什么是Finalize方法?

  • 只要對象繼承自O(shè)bject狈孔,它就擁有Finalize方法信认。在創(chuàng)建這個對象時,會在Finalization Queue(終結(jié)列表均抽,由垃圾回收器控制的一個內(nèi)部數(shù)據(jù)結(jié)構(gòu))為其加入一個指針嫁赏。擁有Finalize方法的對象被稱為可終結(jié)的。

  • Finalize方法又被稱為終結(jié)器油挥。復(fù)寫Finalize方法稱為實現(xiàn)終結(jié)器潦蝇。只有你需要釋放非托管資源時才需要這么做款熬。

  • 復(fù)寫Finalize方法的唯一方法是實現(xiàn)一個解構(gòu)函數(shù)。解構(gòu)函數(shù)的實現(xiàn)只有一個意義攘乒,就是保證非托管資源得到回收华烟,作為Dispose這道關(guān)口后面的最終總閘,因為解構(gòu)函數(shù)是肯定會被執(zhí)行到的持灰。

  • 垃圾清理時盔夜,會標(biāo)記所有的垃圾,并探查終結(jié)列表堤魁,并將其中為垃圾的對象移除出終結(jié)列表喂链,加到Freachable Queue之中(這無形當(dāng)中會給對象續(xù)命一輪GC,因為此時對象被Freachable Queue引用妥泉,不再是沒有被任何其他對象引用的垃圾)椭微。

  • 一個特殊的高優(yōu)先級的線程專門負(fù)責(zé)調(diào)用Finalize方法。這可以避免潛在的線程同步問題盲链。Freachable隊列為空時蝇率,該線程睡眠。一旦Freachable隊列有記錄出現(xiàn)刽沾,該線程就會被喚醒本慕,將每一項都從Freachable隊列中移出,并調(diào)用每一項對象的Finalize方法侧漓,該方法會銷毀對象锅尘。

  • 當(dāng)GC隱式的處理垃圾回收時,第一輪GC會將所有的擁有Finalize方法的垃圾移動到Freachable Queue之中布蔗,并不調(diào)用Finalize方法(所以對象還活著)藤违。下一輪GC才遍歷上面那輪GC中,放到Freachable Queue的對象纵揍,并使用Finalize方法銷毀那些引用類型對象顿乒。所以如果對象擁有Finalize方法,它的壽命會無形之中延長一輪GC(稱為對象的復(fù)生)泽谨,并且它的Finalize方法調(diào)用的時間是不可知的璧榄。在必要的時候,你可以實現(xiàn)IDisposible接口隔盛,利用Dispose來主動銷毀資源犹菱,并在Dispose()成功地執(zhí)行之后呼叫GC.SuppressFinalize(this); 這可以告訴GC不需再去呼叫這個物件的Finalize方法(因為Dispose執(zhí)行過了之后Finalize不需要執(zhí)行了),這樣GC就不會把對象從終結(jié)列表移動到freachable隊列吮炕,可以回避系統(tǒng)的續(xù)命行為腊脱。

  • 因為終結(jié)器會導(dǎo)致續(xù)命,所以請留心龙亲,記得呼叫Dispose陕凹,并呼叫GC.SuppressFinalize(this)悍抑,這可以讓終結(jié)器沒有機會上場,對象就被銷毀了杜耙。

4.6 什么是解構(gòu)函數(shù)搜骡?何時需要寫一個解構(gòu)函數(shù)?

  • 解構(gòu)函數(shù)是Finalize方法的override佑女。它將會被隱式的轉(zhuǎn)換為一個帶有try-finally的Finalize方法记靡,覆蓋它的父對象的Finalize,并在finally中呼叫base.Finalize团驱。(此處的base指System.Object)

  • 解構(gòu)函數(shù)不能有參數(shù)和方法修飾符摸吠。除非你主動觸發(fā)垃圾回收,它的執(zhí)行時間是不可知的嚎花。

  • 雖然僅由托管資源組成的類型也可能會因為用戶忘了呼叫Dispose而暫時存留在堆中寸痢,這并不會造成太大的問題,因為GC最終會回收它紊选。而如果類型中有非托管資源啼止,你需要實現(xiàn)解構(gòu)函數(shù)。如果你沒有實現(xiàn)解構(gòu)函數(shù)兵罢,又忘了呼叫Dispose献烦,則當(dāng)GC回收這個類型時(通過Finalize),將只會回收托管資源(非托管資源沒有Finalize方法)趣些,非托管資源將會一直存留在堆中仿荆。

4.7 如何回收托管資源?

如果類型沒有非托管資源坏平,此時,因為所有托管資源肯定都有Finalize方法锦亦,我們不需要實現(xiàn)解構(gòu)函數(shù)舶替。特別的,對于實現(xiàn)了IDisposible的類型杠园,我們只需要簡單的調(diào)用Dispose來釋放資源即可(這會調(diào)用那個類型的Dispose方法顾瞪,如果類型是屬于微軟的,則微軟已經(jīng)給你實現(xiàn)好了)抛蚁。有些類型的Dispose方法的名稱為Close陈醒。

如果你的托管資源包含了一些實現(xiàn)了IDisposible接口的成員時,你要繼承IDisposible接口瞧甩,并在Dispose方法中將這些成員回收钉跷。或者肚逸,你在使用成員時爷辙,使用using關(guān)鍵字彬坏。using關(guān)鍵字本質(zhì)上是一個try - finally塊,所以即使你在using塊中發(fā)生了異常膝晾,也不用擔(dān)心栓始,對象仍然會在finally塊中被dispose。(曾經(jīng)有面試官問過我這個問題)

4.8 如何回收非托管資源血当?

如果你只是臨時使用非托管資源幻赚,那么將其包含在using中就可以了,例如使用StreamWriter臊旭。

假設(shè)你的類型中含有非托管資源屬性/字段坯屿,此時,你要繼承IDisposible接口巍扛,實現(xiàn)Dispose方法领跛,并寫一個解構(gòu)函數(shù)。你可以follow微軟的垃圾回收模板撤奸,步驟如下:

    1.寫一個私有的方法吠昭,在私有的方法中,釋放托管資源(如果該資源擁有Dispose方法則可以通過呼叫它的Dispose方法完成)和非托管資源胧瓜。
    2.實現(xiàn)Dispose方法矢棚,呼叫私有方法,之后呼叫SuppressFinalize府喳。
    3.實現(xiàn)一個解構(gòu)函數(shù)(這會覆蓋原有的Finalize方法)在其中呼叫私有方法蒲肋。這是為了防止用戶忘了呼叫Dispose方法而最終沒有回收這個非托管資源。原有的Finalize方法并不會理會非托管資源钝满。在解構(gòu)函數(shù)中你不需要呼叫SuppressFinalize因為這已經(jīng)是Finalize方法了兜粘,續(xù)命已經(jīng)發(fā)生了。
public sealed class WindowStationHandle : IDisposable
    {
        // 非托管資源      
        public IntPtr Handle { get; set; }

        public WindowStationHandle(IntPtr handle)
        {
            this.Handle = handle;
        }

        public WindowStationHandle()
            : this(IntPtr.Zero)
        {
        }

        public bool IsInvalid
        {
            get { return (this.Handle == IntPtr.Zero); }
        }

        // 私有方法
        private void CloseHandle()
        {
            if (this.IsInvalid)
            {
                return;
            }

            if (!NativeMethods.CloseWindowStation(this.Handle))
            {
                Trace.WriteLine("CloseWindowStation: " + new Win32Exception().Message);
            }

            // 釋放非托管資源
            this.Handle = IntPtr.Zero;        
        }

        public void Dispose()
        {
            //實現(xiàn)Dispose方法弯蚜,呼叫私有方法孔轴,之后呼叫SuppressFinalize
            this.CloseHandle();
            GC.SuppressFinalize(this);
        }

        ~WindowStationHandle()
        {
            //實現(xiàn)一個解構(gòu)函數(shù)(這會覆蓋原有的Finalize方法)在其中呼叫私有方法。
            //這是為了防止用戶忘了呼叫Dispose方法而最終沒有回收這個非托管資源碎捺。
            //原有的Finalize方法并不會理會非托管資源路鹰。
            this.CloseHandle();
        }
    }

4.9 垃圾回收策略

結(jié)合上面4.4,4.8收厨,就構(gòu)成了常規(guī)的垃圾回收策略:

  • 類中沒有非托管資源晋柱,且沒有對象實現(xiàn)IDisposible: 什么也不用做。

  • 類中沒有非托管資源诵叁,且有對象實現(xiàn)IDisposible: 特別注意這些對象雁竞,確保調(diào)用了它們的Dispose方法(顯式或者隱式的)。你可以實現(xiàn)IDisposible黎休,然后實現(xiàn)Dispose方法浓领,在其中釋放資源玉凯。

  • 類中有非托管資源: 跟從微軟模板,實現(xiàn)一個私有函數(shù)釋放托管和非托管資源联贩,實現(xiàn)IDisposible漫仆,然后實現(xiàn)Dispose方法,并在其中調(diào)用私有函數(shù)泪幌,然后呼叫GC.SuppressFinalize(第一道閘)盲厌。實現(xiàn)一個解構(gòu)函數(shù),并在其中調(diào)用私有函數(shù)(第二道閘)祸泪。如果你的第一道閘完美無缺吗浩,第二道閘是沒有機會上場的。

4.10 擴(kuò)展閱讀:

大對象堆陷阱:http://www.cnblogs.com/brucebi/archive/2013/04/16/3024136.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末没隘,一起剝皮案震驚了整個濱河市懂扼,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌右蒲,老刑警劉巖阀湿,帶你破解...
    沈念sama閱讀 217,734評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異瑰妄,居然都是意外死亡陷嘴,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,931評論 3 394
  • 文/潘曉璐 我一進(jìn)店門间坐,熙熙樓的掌柜王于貴愁眉苦臉地迎上來灾挨,“玉大人,你說我怎么就攤上這事竹宋±统危” “怎么了?”我有些...
    開封第一講書人閱讀 164,133評論 0 354
  • 文/不壞的土叔 我叫張陵逝撬,是天一觀的道長浴骂。 經(jīng)常有香客問我,道長宪潮,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,532評論 1 293
  • 正文 為了忘掉前任趣苏,我火速辦了婚禮狡相,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘食磕。我一直安慰自己尽棕,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,585評論 6 392
  • 文/花漫 我一把揭開白布彬伦。 她就那樣靜靜地躺著滔悉,像睡著了一般伊诵。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上回官,一...
    開封第一講書人閱讀 51,462評論 1 302
  • 那天曹宴,我揣著相機與錄音,去河邊找鬼歉提。 笑死笛坦,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的苔巨。 我是一名探鬼主播版扩,決...
    沈念sama閱讀 40,262評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼侄泽!你這毒婦竟也來了礁芦?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,153評論 0 276
  • 序言:老撾萬榮一對情侶失蹤悼尾,失蹤者是張志新(化名)和其女友劉穎柿扣,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體诀豁,經(jīng)...
    沈念sama閱讀 45,587評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡窄刘,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,792評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了舷胜。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片娩践。...
    茶點故事閱讀 39,919評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖烹骨,靈堂內(nèi)的尸體忽然破棺而出翻伺,到底是詐尸還是另有隱情,我是刑警寧澤沮焕,帶...
    沈念sama閱讀 35,635評論 5 345
  • 正文 年R本政府宣布吨岭,位于F島的核電站,受9級特大地震影響峦树,放射性物質(zhì)發(fā)生泄漏辣辫。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,237評論 3 329
  • 文/蒙蒙 一魁巩、第九天 我趴在偏房一處隱蔽的房頂上張望急灭。 院中可真熱鬧,春花似錦谷遂、人聲如沸葬馋。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,855評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽畴嘶。三九已至蛋逾,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間窗悯,已是汗流浹背区匣。 一陣腳步聲響...
    開封第一講書人閱讀 32,983評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留蟀瞧,地道東北人沉颂。 一個月前我還...
    沈念sama閱讀 48,048評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像悦污,于是被迫代替她去往敵國和親铸屉。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,864評論 2 354

推薦閱讀更多精彩內(nèi)容