.NET 垃圾回收與內存泄漏

> 前言
相信你們必定聽過,看過甚至遇到過內存泄漏。在 .NET 平臺也必定知道有垃圾回收器,它可讓開發人員沒必要擔憂內存的釋放問題,由於它會自定管理內存。可是在 .NET 平臺下進行編程,絕對不會發生內存泄漏的問題嗎?答案是否認的,就算有了自動內存管理的垃圾回收器,也會發生內存泄漏。本文就討論下 .NET 平臺的垃圾回收器是如何工做的,進而當咱們在編寫 .NET 程序時避免發生內存泄漏的問題。算法


> 垃圾回收的基本概念
「垃圾」指的是事先分配過但後來再也不被使用的內存。
垃圾回收背後的一個基本觀念是:「無限訪問的內存」,可是歷來沒有無限的內存,當機器須要分配內存但不夠的時候,就須要把以前再也不使用的內存——「垃圾」回收再利用。
.NET 的垃圾回收器正是這樣作的:
.NET Framework 的垃圾回收器管理應用程序的內存分配和釋放。每當您建立新對象時,公共語言運行時都會從託管堆爲該對象分配內存。只要託管堆中有地址空間可用,運行時就會繼續爲新對象分配空間。 可是,內存不是無限大的。最終,垃圾回收器必須執行回收以釋放一些內存。(引用 MSDN 垃圾回收編程


> 垃圾回收器的工做場景
每當咱們建立一個對象的時候,系統會爲新對象分配一塊內存,若是有足夠的可用內存則會直接分配;可是當內存不足的時候,此時垃圾回收器會進行一次回收操做,把再也不使用的對象釋放,轉化爲可用的內存供新對象使用。網絡

看似很簡單的工做步驟,可是垃圾回收器怎麼知道確保再也不使用的對象的呢?框架


> 垃圾回收算法
當進行一次垃圾回收操做時,會分三個步驟進行:
1. 先假設全部對象都是垃圾;
2. 標記出正在使用的對象;
  標記依據:
  a. 被變量引用的對象,仍然在做用域中。
    好比某個類中的某個方法,方法執行了一半,若是此時發生垃圾回收,那麼方法塊中的變量都在做用域中,那麼它們都會被標記爲正在使用。
  b. 被另外一個對象引用的對象,仍在使用中。
3. 壓縮:釋放第二步中未標記的對象(再也不使用,即「垃圾」)並將使用中的對象轉移到連續的內存塊中。
  只要垃圾回收器釋放了能釋放的對象,它就會壓縮剩餘的對象,把它們都移回堆的端部,再一次造成一個連續的塊。
函數

備註:
垃圾回收器爲了提高性能,使用了代機制,新建的對象是新一代,較早建立的對象是老一代,最近建立的對象是第0代。爲了描述垃圾回收器的基本原理,本文不深刻討論代機制。性能

總之,有了垃圾回收器,咱們沒必要本身實現代碼來管理應用程序所用的對象的生存期。this

既然有了自動內存管理功能的垃圾回收器,爲何還會發生內存泄漏呢?spa


> 託管與非託管
由公共語言運行庫環境(而不是直接由操做系統)執行的代碼稱做託管代碼,運行在 .NET 框架下,受 .NET 框架管理的應用或組件稱做託管資源。.NET 中超過80%的資源都是託管資源,如 int, string, float, DateTime。
非託管資源是 .NET 框架以外的,最多見的一類非託管資源就是包裝操做系統資源的對象,例如文件,窗口或網絡鏈接,對於這類資源雖然垃圾回收器能夠跟蹤封裝非託管資源的對象的生存期,但它不瞭解具體如何清理這些資源。因此,對於非託管資源,在應用程序中使用完以後,必須顯示的釋放它們。
因此,大部份內存泄漏都是非託管資源內存泄漏:沒有顯示的釋放它們。操作系統


> 非託管資源內存泄漏
一個會致使內存泄漏的類:3d

public class Foo
{
    Timer _timer;

    public Foo()
    {
        _timer = new Timer(1000);
        _timer.Elapsed += _timer_Elapsed;
        _timer.Start();
    }

    void _timer_Elapsed(object sender, ElapsedEventArgs e)
    {
        Console.WriteLine("Tick");
    }
}

調用 Foo 類:

static void Main(string[] args)
{
    Foo foo = new Foo();
    foo = null;
    
    Thread.Sleep(int.MaxValue);
}

foo 雖然設置爲 null,可是 foo 中的字段 _timer 依然存活,Elapsed 事件繼續執行:

此類中,_timer 對象就是非託管對象,因爲 _timer 的 Elapsed 事件,.NET Framework 會保持 _timer 永遠存活,進而 _timer 對象會保持 Foo 實例永遠存活,直到程序關閉。

爲了解決這個問題,咱們要顯示的釋放 _timer 對象:Foo 類繼承 IDisposable 接口,修改後的類:

public class Foo : IDisposable
{
    Timer _timer;

    public Foo()
    {
        _timer = new Timer(1000);
        _timer.Elapsed += _timer_Elapsed;
        _timer.Start();
    }

    public void Dispose()
    {
        Console.WriteLine("Dispose");
        _timer.Dispose();
    }

    void _timer_Elapsed(object sender, ElapsedEventArgs e)
    {
        Console.WriteLine("Tick");
    }
}

 

再次調用 Foo 類,並顯示調用 Dispose 方法: 

static void Main(string[] args)
{
    Foo foo = new Foo();
    foo.Dispose();
    foo = null;
    
    Thread.Sleep(int.MaxValue);
}

foo 設置爲 null,_timer 對象也同時被回收,Elapsed 事件中止:


> 非託管資源的垃圾回收
1. 析構函數。
2. 實現 IDisposable 接口。
  在咱們編寫代碼時,一個簡單的方法就是查看類中定義的字段是否有繼承 IDisposable 接口的,若是有,那麼當前的類也應繼承 IDisposable 接口。在使用完非託管資源時,要及時調用 Dispose 方法釋放資源:

Label label = new Label();  
this.Controls.Add(label);  
this.Controls.Remove(label);  
label.Dispose();

更好的方式是使用 using,using 會在編譯代碼的時候自動建立 try/finally 語句塊,在 finally 語句塊中自動調用 Dispose 方法。

using (Label label = new Label())
{
    this.Controls.Add(label);
    this.Controls.Remove(label);
}

 

> 避免內存泄漏的幾點建議除了剛剛提到的非託管資源,還有幾點須要注意:1. 訂閱事件,再也不使用時要記得取消訂閱。2. 不要大量使用靜態字段,靜態字段會永遠存活,一個靜態的集合很容易引發內存溢出。

相關文章
相關標籤/搜索