菜鳥之旅——.NET垃圾回收機制

  .NET的垃圾回收機制是一個很是強大的功能,儘管咱們不多主動使用,但它一直在默默的在後臺運行,咱們仍須要意識到它的存在,瞭解它,作出更高效的.NET應用程序;下面我分享一下我對於垃圾回收機制(GC)的學習心得。html

GC的必要性

  咱們知道程序會須要向內存堆使用new請求內存,而後將請求的內存初始化並使用,使用完畢以後,變清理資源和釋放內存,等待別的程序來請求使用;對內存資源的管理方式,如今存在這麼幾種管理方式:算法

  一、手動管理:C、C++windows

  二、計數管理:COMapp

  三、自動管理:.NET、JAVA、PHPide

  如今的高級語言基本上都實現了自動管理內存,這是由於手動管理內存會由於人爲的緣由產生如下問題:函數

  一、開發人員忘記釋放請求的內存,形成內存泄漏,如果內存泄露過多,則可能會形成內存溢出,致使程序沒法運行;post

  二、應用程序訪問已釋放的內存,形成數據讀取錯誤。性能

  因而可知,手動去管理堆裏面的內存可靠程度,會因開發人員的不一樣而不一樣,在C++因指針而出現的問題可很多;並且易出現Bug等亂七八糟的問題,影響系統穩定性,因此自動化管理內存是必要的。學習

GC的工做原理

 通用概念

  回收時機

  當應用程序分配新的對象,GC的代的預算大小已經達到閾值,好比GC的第0代已滿;優化

  代碼主動顯式調用System.GC.Collect();

  其餘特殊狀況,好比,windows報告內存不足、CLR卸載AppDomain、CLR關閉,甚至某些極端狀況下系統參數設置改變也可能致使GC回收。

  應用程序根

  應用程序根(application root):根(root)就是一個存儲位置其中保存着對託管堆上一個對象的引用,根能夠屬性下面任何一個類別

  • 全局對象和靜態對象的引用
  • 應用程序代碼庫中局部對象的引用
  • 傳遞進一個方法的對象參數的引用
  • 等待被終結(finalize,後面介紹)對象的引用
  • 任何引用對象的CPU寄存器

  代

  垃圾回收器將託管堆(heap)裏面的對象劃分爲3個代(通常爲3代),可使用GC.MaxGeneration()方法來進行查詢當前系統所支持的最大代數:

  一、G0 小對象(Size<85000Byte):新分配的小於85000字節的對象

  二、G1:在GC中倖存下來的G0對象

  三、G2:大對象(Size>=85000Byte);在GC中倖存下來的G1對象

  當一個對象被new的時候,它的代爲0,通過一次回收以後,若該對象沒有被回收,則代上升,變爲1,若每次回收都倖存下來,則代都會上升,最大代爲操做系統所支持的最大代。

  由於將對象以代劃分,而且能夠單獨回收某一個世代,避免回收整個託管堆,提高性能。一個基於代的垃圾回收器有一下特色:

  一、對象越新,生存期越短;

  二、對象越老,生存期越長;

  三、回收堆的一部分,速度快於回收整個堆。

 工做過程

  標記對象

  在垃圾回收的第一步就是標記對象:垃圾回收器會認爲託管堆中的全部對象都是垃圾,而後垃圾回收器會去檢查全部的應用程序根,遍歷每一個根所引用到的對象,將其標記爲活動的(live ),全部的根對象都檢查完以後,有標記的對象就是可達對象,未標記的對象就是不可達對象,不可達對象就是回收的目標。

  弱引用對象則不在考慮範圍以內,因此必定會被回收掉的。

  銷燬對象,釋放內存

  在通過第一步的對象篩選以後,回收沒有被引用的對象,就是不可達對象,GC調用對象默認的終結器Finalize(),銷燬對象以後,將內存也釋放掉。

  同時,還存在引用的對象,就是可達對象的世代變爲下一個世代。

  壓縮堆內存

  通過第二步的銷燬對象和釋放內存以後,倖存下來的對象在堆中的排列多是不連續的,這時在堆中存在很是多的內存碎片,程序在new對象的時候都是請求一段連續的內存,則內存碎片可能就沒法再次利用(雖然沒有被使用),形成內存資源的浪費,因此垃圾回收的最後一步就是壓縮內存:將垃圾回收後倖存的對象移動到一塊兒,而且將各個對象的引用更新到對象新的位置上,保證對象引用的正確性。

  注:從這裏看得出,在壓縮堆內存的時候,全部相關線程必須暫停,由於壓縮時不能保證對象引用的正確性,因此在垃圾回收的時候,GC會劫持全部相關線程,在回收完畢以後,被劫持的線程纔會正常工做,因此垃圾回收勢必會影響必定的性能,因此慎用System.GC.Collect()。

 Finalize()與Dispose()

  上面說到,GC在回收對象的時候是調用對象的終結器Finalize()來實現的,那麼,就簡單的總結一下Finalize()與Dispose()吧:

  一、調用者:

    Finalize只能由GC調用

    Dispose由開發人員顯示調用,也可使用use區塊,在程序離開區塊使自動調用Dispose方法

  二、調用時機:

    Finalize因爲是GC調用的,因此調用時機是垃圾回收的時候調用,時機不肯定

    Dispose因爲是顯示調用,因此調用時機是肯定的,在調用方法的時候就調用了

  三、目的:

    這裏的目的主要說是Dispose出現的目的;

    首先是.NET存在託管資源和非託管資源,通常來講,非託管資源數量有限,比較珍貴,在使用完畢以後,但願可以釋放掉,那麼將釋放非託管資源的方法寫到終結器Finalize裏面也是能夠的,可是因爲Finalize的調用時機不肯定,致使釋放資源不及時,那麼有限的非託管資源很快就被佔用完畢,因此,爲了可以及時的釋放掉這類資源,咱們須要可以顯示調用的方法,這就是Dispose。

    Finalize主要是爲了GC釋放託管資源和銷燬對象,釋放內存

    Dispose主要是爲了釋放託管和非託管資源和銷燬對象,釋放內存

  注:沒必要擔憂資源的重複釋放問題,就算是重複釋放,.NET也作好了相應措施來處理,不會拋出異常。

    下面貼一個MSDN推薦的標準的Dispose實現方式

    class Class : IDisposable
    {
        // 標識:是否釋放託管資源
        private bool disposed = false;

        // 顯示調用的方法
        public void Dispose()
        {
            Dispose(true);
            // 將對象從垃圾回收器鏈表中移除,
            // 從而在垃圾回收器工做時,只釋放託管資源,而不執行此對象的析構函數
            GC.SuppressFinalize(this);
        }

        // 受保護的釋放資源方法
        protected virtual void Dispose(bool disposing)
        {
            if (!this.disposed)
            {
                if (disposing)
                {
                    // 此處寫釋放託管資源的方法
                }
                disposed = true;

                // 此處寫釋放非託管資源的方法
            }
        }
        ~Class()
        {
            // 這裏是防止忘記顯示調用Dispose(),在GC進行垃圾回收的時候進行釋放非託管資源
            Dispose(false);
        }
    }
View Code

 

總結

  GC所帶來的便利是不言而喻的,可是這是付出必定的系統性能來實現的:在垃圾回收的時候GC會劫持全部相關的線程,而且會有必定的時空開銷,因此在平時開發過程當中注意一些良好的開發習慣可能會對GC有一些積極的影響。

  一、儘可能不要new很大的對象,大對象(>=85000Byte)直接歸爲G2代,GC回收算法歷來不對大對象堆(LOH)進行內存壓縮整理,移動大對象將會消耗更多的CPU時間,也更容易形成內存碎片。這裏也能夠將大對象或者生命週期長的對象進行池化。

  二、不要頻繁的new生命週期短的小對象,這可能會致使頻繁的垃圾回收,這裏能夠考慮使用結構體放在棧中來代替,或者也可使用對象池化來優化。

  三、不推薦使用對象池化的解決方案,它比較笨重和容易出錯,設計一個高性能穩定的對象池並不容易。

  四、下降對象之間的縱向深度,GC在回收過程當中,會先順着根來進行對象遍歷和標記,減小深度能夠加快遍歷速度;若系統中各個類之間的關係錯綜複雜,那麼考慮一下設計方案是否合理。

  固然注意的地方還有很多,最後貼一篇博客,這裏介紹了如何編寫高性能的.NET代碼,其中的GC介紹很是詳細:

  [翻譯]【目錄】編寫高性能 .NET 代碼

相關文章
相關標籤/搜索