> 前言
相信大家一定聽過,看過甚至遇到過內存泄漏。在 .NET 平台也一定知道有垃圾回收器,它可以讓開發人員不必擔心內存的釋放問題,因為它會自定管理內存。但是在 .NET 平台下進行編程,絕對不會發生內存泄漏的問題嗎?答案是否定的,就算有了自動內存管理的垃圾回收器,也會發生內存泄漏。本文就討論下 .NET 平台的垃圾回收器是如何工作的,進而當我們在編寫 .NET 程序時避免發生內存泄漏的問題。
> 垃圾回收的基本概念
“垃圾”指的是事先分配過但后來不再被使用的內存。
垃圾回收背后的一個基本觀念是:“無限訪問的內存”,但是從來沒有無限的內存,當機器需要分配內存但不夠的時候,就需要把之前不再使用的內存——“垃圾”回收再利用。
.NET 的垃圾回收器正是這樣做的:
.NET Framework 的垃圾回收器管理應用程序的內存分配和釋放。每當您創建新對象時,公共語言運行時都會從托管堆為該對象分配內存。只要托管堆中有地址空間可用,運行時就會繼續為新對象分配空間。 但是,內存不是無限大的。最終,垃圾回收器必須執行回收以釋放一些內存。(引用 MSDN 垃圾回收)
> 垃圾回收器的工作場景
每當我們創建一個對象的時候,系統會為新對象分配一塊內存,如果有足夠的可用內存則會直接分配;但是當內存不足的時候,此時垃圾回收器會進行一次回收操作,把不再使用的對象釋放,轉化為可用的內存供新對象使用。
看似很簡單的工作步驟,但是垃圾回收器怎么知道確保不再使用的對象的呢?
> 垃圾回收算法
當進行一次垃圾回收操作時,會分三個步驟進行:
1. 先假設所有對象都是垃圾;
2. 標記出正在使用的對象;
標記依據:
a. 被變量引用的對象,仍然在作用域中。
比如某個類中的某個方法,方法執行了一半,如果此時發生垃圾回收,那么方法塊中的變量都在作用域中,那么它們都會被標記為正在使用。
b. 被另一個對象引用的對象,仍在使用中。
3. 壓縮:釋放第二步中未標記的對象(不再使用,即“垃圾”)並將使用中的對象轉移到連續的內存塊中。
只要垃圾回收器釋放了能釋放的對象,它就會壓縮剩余的對象,把它們都移回堆的端部,再一次形成一個連續的塊。
備注:
垃圾回收器為了提升性能,使用了代機制,新建的對象是新一代,較早創建的對象是老一代,最近創建的對象是第0代。為了描述垃圾回收器的基本原理,本文不深入討論代機制。
總之,有了垃圾回收器,我們不必自己實現代碼來管理應用程序所用的對象的生存期。
既然有了自動內存管理功能的垃圾回收器,為什么還會發生內存泄漏呢?
> 托管與非托管
由公共語言運行庫環境(而不是直接由操作系統)執行的代碼稱作托管代碼,運行在 .NET 框架下,受 .NET 框架管理的應用或組件稱作托管資源。.NET 中超過80%的資源都是托管資源,如 int, string, float, DateTime。
非托管資源是 .NET 框架之外的,最常見的一類非托管資源就是包裝操作系統資源的對象,例如文件,窗口或網絡連接,對於這類資源雖然垃圾回收器可以跟蹤封裝非托管資源的對象的生存期,但它不了解具體如何清理這些資源。所以,對於非托管資源,在應用程序中使用完之后,必須顯示的釋放它們。
所以,大部分內存泄漏都是非托管資源內存泄漏:沒有顯示的釋放它們。
> 非托管資源內存泄漏
一個會導致內存泄漏的類:
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. 不要大量使用靜態字段,靜態字段會永遠存活,一個靜態的集合很容易引起內存溢出。