程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> .NET深入學習筆記(3):垃圾回收與內存管理

.NET深入學習筆記(3):垃圾回收與內存管理

編輯:關於.NET

今天抽空來討論一下.Net的垃圾回收與內存管理機制,也算是完成上個《WCF分布式開發必備知識》系列後的一次休息吧。以前被別人面試的時候問過我GC工作原理的問題,我現在面試新人的時候偶爾也會問相關的問題。那麼你是否也遇到這樣的問題呢?比如你清楚.Net的垃圾回收機制嗎?你能簡述一下GC的工作原理嗎?怎麼樣才能有效的管理內存呢?Using語句體內實例化的對象有什麼作用?等等相關問題。下面我們就來詳細討論一下。相信你看完以後也可以面試別人。

本節的組織如下,1..Net的類型和內存分配2.GC垃圾收集器的工作原理3.什麼是非托管資源4.如何有效釋放對象資源。總結.現在開始我們本節的學習。

1..Net的類型和內存分配

Net中的所有類型都是(直接或間接)從System.Object類型派生的。

CTS中的類型被分成兩大類——引用類型(reference type,又叫托管類型[managed type]),分配在內存堆上,值類型(value type)。值類型分配在堆棧上。如圖

值類型在棧裡,先進後出,值類型變量的生命有先後順序,這個確保了值類型變量在推出作用域以前會釋放資源。比引用類型更簡單和高效。堆棧是從高地址往低地址分配內存。

引用類型分配在托管堆(Managed Heap)上,聲明一個變量在棧上保存,當使用new創建對象時,會把對象的地址存儲在這個變量裡。托管堆相反,從低地址往高地址分配內存,如圖

2.GC垃圾收集器的工作原理

上圖中,當dataSet使用過期以後,我們不顯示銷毀對象,堆上的對象還繼續存在,等待GC的 回收。

垃圾收集器通過分代支持對象的年齡化是推薦的但不是必需的。一代在內存裡是一個具有相對年齡的對象的單位。對象的

代號或年齡標識對象屬於那個分代。在應用程序的生命周期裡,越近創建的對象屬於越新的代,並且比早創建的對象具有

較低的分代號。最近分代裡的對象代號是0.

在new對象時,要先搜索空閒鏈表,找到最適合內存塊,分配,調整內存塊鏈表,合並碎片。new操作幾乎可以在O(1)的時間完成,把堆頂指針加1。工作原理是: 當托管堆上剩余空間不足,或者Generator 0 的空間已滿的時候GC運行,開始回收內存。垃圾回收的開始,GC對堆內存的壓縮調整,對象集中到頂部。GC在掃描垃圾的時候會占用一定的CPU時間片的,最初的GC算法真的是掃描整個堆,效率低。現在的GC把堆中的對象分成3代,最近進入堆的是第0代(generation 0), 其次是generation 1, generation2. 第一次GC只掃描第0代。如果回收的空間足夠當前使用就不必掃描其它generation的對象。所以,GC創建對象的效率比C++高效,不需要掃描全部堆空間。它通過掃描策略,再加上內存管理策略帶來的性能提升,足以補償GC所占用的CPU時間。

3.什麼是非托管資源

常見的非托管資源就是包裝操作系統資源的對象,例如文件,窗口或網絡連接,對於這類資源雖然垃圾回收器可以跟蹤封裝非托管資源的對象的生存期,但它知道如何清理這些資源。好在.net Framework提供的Finalize()方法,它允許在垃圾回收器回收該類資源前,適當的清理非托管資源。這裡列舉幾種常見的非托管資源:畫筆、流對象、組件對象等等資源(Object,OdbcDataReader,OleDBDataReader,Pen,Regex,Socket,StreamWriter,ApplicationContext,Brush,

Component,ComponentDesigner,Container,Context,Cursor,FileStream,

Font,Icon,Image,Matrix,Timer,Tooltip)。(參考MSDN)

4.如何有效釋放非托管資源。

GC無法管理非托管資源,那麼如何釋放非托管資源呢?.Net提供了兩種方式:

(1)析構函數:垃圾收集器回收非托管對象的資源時,會調用對象的終結方法Finalize(),進行資源的清理工作,但是由於GC工作規則的限制,GC調用對象的Finalize方法,第一次不會釋放資源,第二次調用之後才刪除對象。

(2)繼承IDisposable接口,實現Dispose()方法,IDisposable接口定義了一個模式(具有語言級的支持),為釋放未托管的資源提供了確定的機制,並避免產生析構函數固有的與垃圾收集器相關的問題。

為了更好的理解垃圾回收機制,我特地寫了部分代碼,裡面添加了詳細的注釋。定義單個類FrankClassWithDispose(繼承接口IDisposable)、FrankClassNoFinalize(沒終結器)、FrankClassWithDestructor(定義了析構函數)。

具體代碼如下:

1using System;
2using System.Collections.Generic;
3using System.Text;
4using System.Data;
5using System.Data.Odbc;
6using System.Drawing;
7//Coded By Frank Xu Lei 18/2/2009
8//Study the .NET Memory Management
9//Garbage Collector 垃圾收集器。可以根據策略在需要的時候回收托管資源,
10//但是GC不知道如何管理非托管資源。如網絡連接、數據庫連接、畫筆、組件等
11//兩個機制來解決非托管資源的釋放問題。析構函數、IDispose接口
12//COM引用計數
13//C++手動管理,New Delete
14//VB自動管理
15namespace MemoryManagement
16{
17    //繼承接口IDisposable,實現Dispose方法,可以釋放FrankClassDispose的實例資源
18    public class FrankClassWithDispose : IDisposable
19    {
20        private OdbcConnection _odbcConnection = null;
21
22        //構造函數
23        public FrankClassWithDispose()
24        {
25            if (_odbcConnection == null)
26                _odbcConnection = new OdbcConnection();
27            Console.WriteLine("FrankClassWithDispose has been created ");
28        }
29        //測試方法
30        public void DoSomething()
31        {
32
33            /**/////code here to do something
34            return ;
35        }
36        //實現Dispose,釋放本類使用的資源
37        public void Dispose()
38        {
39            if (_odbcConnection != null)
40                _odbcConnection.Dispose();
41            Console.WriteLine("FrankClassWithDispose has been disposed");
42        }
43    }
44    //沒有實現Finalize,等著GC回收FrankClassFinalize的實例資源,GC運行時候直接回收
45    public class FrankClassNoFinalize
46    {
47        private OdbcConnection _odbcConnection = null;
48        //構造函數
49        public FrankClassNoFinalize()
50        {
51            if (_odbcConnection == null)
52                _odbcConnection = new OdbcConnection();
53            Console.WriteLine("FrankClassNoFinalize  has been created");
54        }
55        //測試方法
56        public void DoSomething()
57        {
58
59            //GC.Collect();
60            /**/////code here to do something
61            return ;
62        }
63    }
64    //實現析構函數,編譯為Finalize方法,調用對象的析構函數
65    //GC運行時,兩次調用,第一次沒釋放資源,第二次才釋放
66    //FrankClassDestructor的實例資源
67    //CLR使用獨立的線程來執行對象的Finalize方法,頻繁調用會使性能下降
68    public class FrankClassWithDestructor
69    {
70        private OdbcConnection _odbcConnection = null;
71        //構造函數
72        public FrankClassWithDestructor()
73        {
74            if (_odbcConnection == null)
75                _odbcConnection = new OdbcConnection();
76            Console.WriteLine("FrankClassWithDestructor  has been created");
77        }
78        //測試方法
79        public void DoSomething()
80        {
81            /**/////code here to do something
82
83            return ;
84        }
85        //析構函數,釋放未托管資源
86        ~FrankClassWithDestructor()
87        {
88            if (_odbcConnection != null)
89                _odbcConnection.Dispose();
90            Console.WriteLine("FrankClassWithDestructor  has been disposed");
91        }
92    }
93}
94

其中使用了非托管的對象OdbcConnection的實例。建立的客戶端進行了簡單的測試。客戶端代碼如下:

1using System;
2using System.Collections.Generic;
3using System.Text;
4using System.Data;
5using MemoryManagement;
6//Coded By Frank Xu Lei 18/2/2009
7//Study the .NET Memory Management
8//Test The Unmanaged Objects Reclaimed.
9//針對非托管代碼的測試,比較
10//托管代碼,GC可以更具策略自己回收,也可以實現IDisposable,調用Dispose()方法,主動釋放。
11namespace MemoryManagementClient
12{
13    class Program
14    {
15        static void Main(string[] args)
16        {
17
18            /**//////////////////////////////////////////(1)////////////////////////////////////////////
19            //調用Dispose()方法,主動釋放。資源,靈活
20            FrankClassWithDispose _frankClassWithDispose = null;
21            try
22            {
23                _frankClassWithDispose = new FrankClassWithDispose();
24                _frankClassWithDispose.DoSomething();
25
26            }
27            finally
28            {
29                if (_frankClassWithDispose!=null)
30                _frankClassWithDispose.Dispose();
31                //Console.WriteLine("FrankClassWithDispose實例已經被釋放");
32            }
33
34            /**//////////////////////////////////////////(2)//////////////////////////////////////////////
35            //可以使用Using語句創建非托管對象,方法執行結束前,會調用
36            using (FrankClassWithDispose _frankClassWithDispose2 = new FrankClassWithDispose())
37            {
38                //_frankClassWithDispose2.DoSomething();
39            }
40
41            /**//////////////////////////////////////////(3)////////////////////////////////////////////
42            //垃圾收集器運行的時候,一次就釋放資源
43            FrankClassNoFinalize _frankClassNoFinalize = new FrankClassNoFinalize();
44            _frankClassNoFinalize.DoSomething();
45
46            /**///////////////////////////////////////////(4)//////////////////////////////////////////////
47            //垃圾收集器運行的時候,兩次才能夠釋放資源
48            FrankClassWithDestructor _frankClassWithDestructor = new FrankClassWithDestructor();
49            _frankClassWithDestructor.DoSomething();
50            /**////////////////////////////////////////////(5)/////////////////////////////////////////////
51            //不能使用Using語句來創建對象,因為其沒實現IDispose接口
52            //using (FrankClassWithDestructor _frankClassWithDestructor2 = new FrankClassWithDestructor())
53            //{
54            //    _frankClassWithDestructor2.DoSomething();
55            //}
56
57            /**///////////////////////////////////////////////////////////////////////////////////////
58            //For Debug
59            Console.WriteLine("Press any key to continue");
60            Console.ReadLine();
61
62
63        }
64    }
65}
66

有些時候資源必須在特定時間釋放,類可以實現執行資源管理和清除任務方法IDisposable.Dispose的接口IDisposable。

如果調用者需要調用Dispose方法清理對象,類作為契約的一部分必須實現Dispose方法。垃圾收集器默認情況下不會調用

Dispose方法;然而,實現Dispose方法可以調用GC裡的方法去規范垃圾收器的終結行為。

值得一提的是:調用Dispose()方法,主動釋放資源,靈活,可以使用Using語句創建非托管對象,方法執行結束前,會調用

Dispose()方法釋放資源,這兩端代碼的效果是一樣的,可以查看編譯後IL。

1.try
2  {
3    IL_0003:  nop
4    IL_0004:  newobj     instance void [MemoryManagement]MemoryManagement.FrankClassWithDispose::.ctor()
5    IL_0009:  stloc.0
6    IL_000a:  ldloc.0
7    IL_000b:  callvirt   instance void [MemoryManagement]MemoryManagement.FrankClassWithDispose::DoSomething()
8    IL_0010:  nop
9    IL_0011:  nop
10    IL_0012:  leave.s    IL_0028
11  }  // end .try
12  finally
13  {
14    IL_0014:  nop
15    IL_0015:  ldloc.0
16    IL_0016:  ldnull
17    IL_0017:  ceq
18    IL_0019:  stloc.s    CS$4$0000
19    IL_001b:  ldloc.s    CS$4$0000
20    IL_001d:  brtrue.s   IL_0026
21    IL_001f:  ldloc.0
22    IL_0020:  callvirt   instance void [MemoryManagement]MemoryManagement.FrankClassWithDispose::Dispose()
23    IL_0025:  nop
24    IL_0026:  nop
25    IL_0027:  endfinally
26  }  // end handler
27  IL_0028:  nop
28  IL_0029:  newobj     instance void [MemoryManagement]MemoryManagement.FrankClassWithDispose::.ctor()
29  IL_002e:  stloc.1
30  .try
31  {
32    IL_002f:  nop
33    IL_0030:  nop
34    IL_0031:  leave.s    IL_0045
35  }  // end .try
36  finally
37  {
38    IL_0033:  ldloc.1
39    IL_0034:  ldnull
40    IL_0035:  ceq
41    IL_0037:  stloc.s    CS$4$0000
42    IL_0039:  ldloc.s    CS$4$0000
43    IL_003b:  brtrue.s   IL_0044
44    IL_003d:  ldloc.1
45    IL_003e:  callvirt   instance void [mscorlib]System.IDisposable::Dispose()
46    IL_0043:  nop
47    IL_0044:  endfinally
48  }  // end handler
49

Using 語句有同樣的效果,來實現非托管對象資源的釋放。這點在面試中也會經常遇到,Using關鍵字的用法有哪幾種等等類似的問題。基本理想的答案都是除了引用命名空間,和命名空間設置別名外,就是這個用法實現如try finally塊一樣作用的對非托管對象資源的回收。只是一種簡便的寫法。

當你用Dispose方法釋放未托管對象的時候,應該調用GC.SuppressFinalize。如果對象正在終結隊列(finalization queue),GC.SuppressFinalize會阻止GC調用Finalize方法。因為Finalize方法的調用會犧牲部分性能。如果你的Dispose方法已經對委托管資源作了清理,就沒必要讓GC再調用對象的Finalize方法(MSDN)。附上MSDN的代碼,大家可以參考.

public class BaseResource: IDisposable
{
// 指向外部非托管資源
private IntPtr handle;
// 此類使用的其它托管資源.
private Component Components;
// 跟蹤是否調用.Dispose方法,標識位,控制垃圾收集器的行為
private bool disposed = false;

// 構造函數
public BaseResource()
{
// Insert appropriate constructor code here.
}

// 實現接口IDisposable.
// 不能聲明為虛方法virtual.
// 子類不能重寫這個方法.
public void Dispose()
{
Dispose(true);
// 離開終結隊列Finalization queue
// 設置對象的阻止終結器代碼
//
GC.SuppressFinalize(this);
}

// Dispose(bool disposing) 執行分兩種不同的情況.
// 如果disposing 等於 true, 方法已經被調用
// 或者間接被用戶代碼調用. 托管和非托管的代碼都能被釋放
// 如果disposing 等於false, 方法已經被終結器 finalizer 從內部調用過,
//你就不能在引用其他對象,只有非托管資源可以被釋放。
protected virtual void Dispose(bool disposing)
{
// 檢查Dispose 是否被調用過.
if(!this.disposed)
{
// 如果等於true, 釋放所有托管和非托管資源
if(disposing)
{
// 釋放托管資源.
Components.Dispose();
}
// 釋放非托管資源,如果disposing為 false,
// 只會執行下面的代碼.
CloseHandle(handle);
handle = IntPtr.Zero;
// 注意這裡是非線程安全的.
// 在托管資源釋放以後可以啟動其它線程銷毀對象,
// 但是在disposed標記設置為true前
// 如果線程安全是必須的,客戶端必須實現。

}
disposed = true;
}
// 使用interop 調用方法
// 清除非托管資源.
[System.Runtime.InteropServices.DllImport("Kernel32")]
private extern static Boolean CloseHandle(IntPtr handle);

// 使用C# 析構函數來實現終結器代碼
// 這個只在Dispose方法沒被調用的前提下,才能調用執行。
// 如果你給基類終結的機會.
// 不要給子類提供析構函數.
~BaseResource()
{
// 不要重復創建清理的代碼.
// 基於可靠性和可維護性考慮,調用Dispose(false) 是最佳的方式
Dispose(false);
}

// 允許你多次調用Dispose方法,
// 但是會拋出異常如果對象已經釋放。
// 不論你什麼時間處理對象都會核查對象的是否釋放,
// check to see if it has been disposed.
public void DoSomething()
{
if(this.disposed)
{
throw new ObjectDisposedException();
}
}

對於需要調用Close方法比Dispose方法更加自然的類型,可以在基類增加一個Close方法。

Close方法無參調用執行恰當清理工作的Dispose方法。

下面的例子演示了Close方法。

// 不要設置方法為virtual.
// 繼承類不允許重寫這個方法
public void Close()
{
// 無參數調用Dispose參數.
Dispose();
}

public static void Main()
{
// Insert code here to create
// and use a BaseResource object.
}
}

總結:看了本文以後,不知對你是否有所幫助,如果你理解了.net垃圾回收的機制和GC的工作原理,以及包含如何管理非托管資源,你就會成為一個內存管理的高手。如果面試官問道這個問題,你就可以詳細闡述你對這類問題的理解和看法。希望這篇文章能對你的工作和學習帶來幫助~

本文配套源碼

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved