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


  .NET的垃圾回收機制是一個非常強大的功能,盡管我們很少主動使用,但它一直在默默的在后台運行,我們仍需要意識到它的存在,了解它,做出更高效的.NET應用程序;下面我分享一下我對於垃圾回收機制(GC)的學習心得。

GC的必要性

  我們知道程序會需要向內存堆使用new請求內存,然后將請求的內存初始化並使用,使用完畢之后,變清理資源和釋放內存,等待別的程序來請求使用;對內存資源的管理方式,現在存在這么幾種管理方式:

  1、手動管理:C、C++

  2、計數管理:COM

  3、自動管理:.NET、JAVA、PHP

  現在的高級語言基本上都實現了自動管理內存,這是因為手動管理內存會因為人為的原因產生以下問題:

  1、開發人員忘記釋放請求的內存,造成內存泄漏,若是內存泄露過多,則可能會造成內存溢出,導致程序無法運行;

  2、應用程序訪問已釋放的內存,造成數據讀取錯誤。

  由此可見,手動去管理堆里面的內存可靠程度,會因開發人員的不同而不同,在C++因指針而出現的問題可不少;而且易出現Bug等亂七八糟的問題,影響系統穩定性,所以自動化管理內存是必要的。

GC的工作原理

 通用概念

  回收時機

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

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

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

  應用程序根

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

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

  代

  垃圾回收器將托管堆(heap)里面的對象划分為3個代(一般為3代),可以使用GC.MaxGeneration()方法來進行查詢當前系統所支持的最大代數:

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

  2、G1:在GC中幸存下來的G0對象

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

  當一個對象被new的時候,它的代為0,經過一次回收之后,若該對象沒有被回收,則代上升,變為1,若每次回收都幸存下來,則代都會上升,最大代為操作系統所支持的最大代。

  因為將對象以代划分,並且可以單獨回收某一個世代,避免回收整個托管堆,提升性能。一個基於代的垃圾回收器有一下特點:

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

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

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

 工作過程

  標記對象

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

  弱引用對象則不在考慮范圍之內,所以一定會被回收掉的。

  銷毀對象,釋放內存

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

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

  壓縮堆內存

  經過第二步的銷毀對象和釋放內存之后,幸存下來的對象在堆中的排列可能是不連續的,這時在堆中存在非常多的內存碎片,程序在new對象的時候都是請求一段連續的內存,則內存碎片可能就無法再次利用(雖然沒有被使用),造成內存資源的浪費,所以垃圾回收的最后一步就是壓縮內存:將垃圾回收后幸存的對象移動到一起,並且將各個對象的引用更新到對象新的位置上,保證對象引用的正確性。

  注:從這里看得出,在壓縮堆內存的時候,所有相關線程必須暫停,因為壓縮時不能保證對象引用的正確性,所以在垃圾回收的時候,GC會劫持所有相關線程,在回收完畢之后,被劫持的線程才會正常工作,所以垃圾回收勢必會影響一定的性能,所以慎用System.GC.Collect()。

 Finalize()與Dispose()

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

  1、調用者:

    Finalize只能由GC調用

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

  2、調用時機:

    Finalize由於是GC調用的,所以調用時機是垃圾回收的時候調用,時機不確定

    Dispose由於是顯示調用,所以調用時機是確定的,在調用方法的時候就調用了

  3、目的:

    這里的目的主要說是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有一些積極的影響。

  1、盡量不要new很大的對象,大對象(>=85000Byte)直接歸為G2代,GC回收算法從來不對大對象堆(LOH)進行內存壓縮整理,移動大對象將會消耗更多的CPU時間,也更容易造成內存碎片。這里也可以將大對象或者生命周期長的對象進行池化。

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

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

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

  當然注意的地方還有不少,最后貼一篇博客,這里介紹了如何編寫高性能的.NET代碼,其中的GC介紹非常詳細:

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

   


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM