天天看點

.NET 垃圾回收與記憶體洩漏

> 前言

相信大家一定聽過,看過甚至遇到過記憶體洩漏。在 .NET 平台也一定知道有垃圾回收器,它可以讓開發人員不必擔心記憶體的釋放問題,因為它會自定管理記憶體。但是在 .NET 平台下進行程式設計,絕對不會發生記憶體洩漏的問題嗎?答案是否定的,就算有了自動記憶體管理的垃圾回收器,也會發生記憶體洩漏。本文就讨論下 .NET 平台的垃圾回收器是如何工作的,進而當我們在編寫 .NET 程式時避免發生記憶體洩漏的問題。

> 垃圾回收的基本概念

“垃圾”指的是事先配置設定過但後來不再被使用的記憶體。

垃圾回收背後的一個基本觀念是:“無限通路的記憶體”,但是從來沒有無限的記憶體,當機器需要配置設定記憶體但不夠的時候,就需要把之前不再使用的記憶體——“垃圾”回收再利用。

.NET 的垃圾回收器正是這樣做的:

> 垃圾回收器的工作場景

每當我們建立一個對象的時候,系統會為新對象配置設定一塊記憶體,如果有足夠的可用記憶體則會直接配置設定;但是當記憶體不足的時候,此時垃圾回收器會進行一次回收操作,把不再使用的對象釋放,轉化為可用的記憶體供新對象使用。

看似很簡單的工作步驟,但是垃圾回收器怎麼知道確定不再使用的對象的呢?

> 垃圾回收算法

當進行一次垃圾回收操作時,會分三個步驟進行:

1. 先假設所有對象都是垃圾;

2. 标記出正在使用的對象;

  标記依據:

  a. 被變量引用的對象,仍然在作用域中。

    比如某個類中的某個方法,方法執行了一半,如果此時發生垃圾回收,那麼方法塊中的變量都在作用域中,那麼它們都會被标記為正在使用。

  b. 被另一個對象引用的對象,仍在使用中。

3. 壓縮:釋放第二步中未标記的對象(不再使用,即“垃圾”)并将使用中的對象轉移到連續的記憶體塊中。

  隻要垃圾回收器釋放了能釋放的對象,它就會壓縮剩餘的對象,把它們都移回堆的端部,再一次形成一個連續的塊。

.NET 垃圾回收與記憶體洩漏

備注:

垃圾回收器為了提升性能,使用了代機制,建立的對象是新一代,較早建立的對象是老一代,最近建立的對象是第0代。為了描述垃圾回收器的基本原理,本文不深入讨論代機制。

總之,有了垃圾回收器,我們不必自己實作代碼來管理應用程式所用的對象的生存期。

既然有了自動記憶體管理功能的垃圾回收器,為什麼還會發生記憶體洩漏呢?

> 托管與非托管

由公共語言運作庫環境(而不是直接由作業系統)執行的代碼稱作托管代碼,運作在 .NET 架構下,受 .NET 架構管理的應用或元件稱作托管資源。.NET 中超過80%的資源都是托管資源,如 int, string, float, DateTime。

非托管資源是 .NET 架構之外的,最常見的一類非托管資源就是包裝作業系統資源的對象,例如檔案,視窗或網絡連接配接,對于這類資源雖然垃圾回收器可以跟蹤封裝非托管資源的對象的生存期,但它不了解具體如何清理這些資源。是以,對于非托管資源,在應用程式中使用完之後,必須顯示的釋放它們。

是以,大部分記憶體洩漏都是非托管資源記憶體洩漏:沒有顯示的釋放它們。

> 非托管資源記憶體洩漏

一個會導緻記憶體洩漏的類:

調用 Foo 類:

foo 雖然設定為 null,但是 foo 中的字段 _timer 依然存活,Elapsed 事件繼續執行:

.NET 垃圾回收與記憶體洩漏

此類中,_timer 對象就是非托管對象,由于 _timer 的 Elapsed 事件,.NET Framework 會保持 _timer 永遠存活,進而 _timer 對象會保持 Foo 執行個體永遠存活,直到程式關閉。

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

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

foo 設定為 null,_timer 對象也同時被回收,Elapsed 事件停止:

.NET 垃圾回收與記憶體洩漏

> 非托管資源的垃圾回收

1. 析構函數。

2. 實作 IDisposable 接口。

  在我們編寫代碼時,一個簡單的方法就是檢視類中定義的字段是否有繼承 IDisposable 接口的,如果有,那麼目前的類也應繼承 IDisposable 接口。在使用完非托管資源時,要及時調用 Dispose 方法釋放資源:

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

> 避免記憶體洩漏的幾點建議

除了剛剛提到的非托管資源,還有幾點需要注意:

1. 訂閱事件,不再使用時要記得取消訂閱。

2. 不要大量使用靜态字段,靜态字段會永遠存活,一個靜态的集合很容易引起記憶體溢出。