在開始以前,咱們須要明確什麼是C#(或者說.NET)中的資源,打碼的時候咱們常常說釋放資源,那麼到底什麼是資源,簡單來說,C#中的每一種類型都是一種資源,而資源又分爲託管資源和非託管資源,那這又是什麼?!java
託管資源:由CLR管理分配和釋放的資源,也就是咱們直接new出來的對象;git
非託管資源:不受CLR控制的資源,也就是不屬於.NET自己的功能,每每是經過調用跨平臺程序集(如C++)或者操做系統提供的一些接口,好比Windows內核對象、文件操做、數據庫鏈接、socket、Win32API、網絡等。web
咱們下文討論的,主要也就是非託管資源的釋放,而託管資源.NET的垃圾回收已經幫咱們完成了。其實非託管資源有部分.NET的垃圾回收也幫咱們實現了,那麼若是要讓.NET垃圾回收幫咱們釋放非託管資源,該如何去實現。數據庫
假設咱們要使用FileStream,咱們一般的作法是將其using起來,或者是更老式的try…catch…finally…這種作法,由於它的實現調用了非託管資源,因此咱們必須用完以後要去顯式釋放它,若是不去釋放它,那麼可能就會形成內存泄漏。c#
這聽上去貌似很簡單,但咱們編碼的時候可能不少時候會忽略掉釋放資源這個問題,.NET的垃圾回收又如何幫咱們釋放非託管資源,接下來咱們一探究竟吧,一個標準的釋放非託管資源的類應該去實現IDisposable接口:緩存
1
2
3
4
5
6
7
|
public
class
MyClass:IDisposable
{
/// <summary>執行與釋放或重置非託管資源關聯的應用程序定義的任務。</summary>
public
void
Dispose()
{
}
}
|
咱們實例化的時候就能夠將這個類using起來:網絡
1
2
3
|
using
(var mc =
new
MyClass())
{
}
|
看上去很簡單嘛,可是,要是就這麼簡單的話,也沒有這篇文章的必要了。若是要實現IDisposable接口,咱們其實應該這樣作:dom
實現Dispose方法;socket
提取一個受保護的Dispose虛方法,在該方法中實現具體的釋放資源的邏輯;ide
添加析構函數;
添加一個私有的bool類型的字段,做爲釋放資源的標記
接下來,咱們來實現這樣的一個Dispose模式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
|
public
class
MyClass : IDisposable
{
/// <summary>
/// 模擬一個非託管資源
/// </summary>
private
IntPtr NativeResource {
get
;
set
; } = Marshal.AllocHGlobal(100);
/// <summary>
/// 模擬一個託管資源
/// </summary>
public
Random ManagedResource {
get
;
set
; } =
new
Random();
/// <summary>
/// 釋放標記
/// </summary>
private
bool
disposed;
/// <summary>
/// 爲了防止忘記顯式的調用Dispose方法
/// </summary>
~MyClass()
{
//必須爲false
Dispose(
false
);
}
/// <summary>執行與釋放或重置非託管資源關聯的應用程序定義的任務。</summary>
public
void
Dispose()
{
//必須爲true
Dispose(
true
);
//通知垃圾回收器再也不調用終結器
GC.SuppressFinalize(
this
);
}
/// <summary>
/// 非必需的,只是爲了更符合其餘語言的規範,如C++、java
/// </summary>
public
void
Close()
{
Dispose();
}
/// <summary>
/// 非密封類可重寫的Dispose方法,方便子類繼承時可重寫
/// </summary>
/// <param name="disposing"></param>
protected
virtual
void
Dispose(
bool
disposing)
{
if
(disposed)
{
return
;
}
//清理託管資源
if
(disposing)
{
if
(ManagedResource !=
null
)
{
ManagedResource =
null
;
}
}
//清理非託管資源
if
(NativeResource != IntPtr.Zero)
{
Marshal.FreeHGlobal(NativeResource);
NativeResource = IntPtr.Zero;
}
//告訴本身已經被釋放
disposed =
true
;
}
}
|
這裏面每行代碼都有它獨自的含義,文章裏不可能每句話都講解透徹,爲了突出重點,因此接下來就挑出幾個重要的地方逐一解釋咯,固然截止如今,咱們只須要記住:
若是類型須要顯式的釋放資源,那麼就必定要實現IDisposable接口。
實現IDisposable接口其實也是爲了方便使用using這個語法糖,以方便編譯器幫咱們自動生成Dispose的IL代碼:
1
2
3
|
using
(var mc =
new
MyClass())
{
}
|
就至關於:
1
2
3
4
5
6
7
8
9
10
11
12
|
MyClass mc =
null
;
try
{
mc =
new
MyClass();
}
finally
{
if
(mc !=
null
)
{
mc.Dispose();
}
}
|
若是要同時管理多個相同類型的對象:
1
2
3
|
using
(MyClass mc1=
new
MyClass(),mc2=
new
MyClass())
{
}
|
若是類型不一致:
1
2
3
4
5
6
|
using
(var client =
new
HttpClient())
{
using
(var stream = File.Create(
""
))
{
}
}
|
在以前咱們實現的更標準的Dispose模式中,咱們注意到了,類裏面包含了一個~開頭的析構方法:
1
2
3
4
5
|
~MyClass()
{
//必須爲false
Dispose(
false
);
}
|
這個析構方法更規範的說法叫作終結器,它的意義在於,若是咱們忘記了顯式調用Dispose方法,垃圾回收器在掃描內存的時候,會做爲釋放資源的一種補救措施。
爲何加了析構方法就會有這種效果,咱們知道在new對象的時候,CLR會爲對象建立一塊內存空間,一旦對象再也不被引用,就會被垃圾回收器回收掉,對於沒有實現IDisposable接口的類來講,垃圾回收時將直接回收掉這片內存空間,而對於實現了IDisposable接口的類來講,因爲析構方法的存在,在建立對象之初,CLR會將該對象的一個指針放到終結器列表中,在GC回收內存以前,會首先將終結器列表中的指針放到一個freachable隊列中,同時,CLR還會分配專門的內存空間來讀取freachable隊列,並調用對象的終結器,只有在這個時候,對象才被真正的被標識爲垃圾,在下一次垃圾回收的時候纔回收這個對象所佔用的內存空間。
那麼,實現了IDisposable接口的對象在回收時要通過兩次GC才能被真正的釋放掉,由於GC要先安排CLR調用終結器,基於這個特色,若是咱們顯式調用了Dispose方法,那麼GC就不會再進行第二次垃圾回收了,固然,若是忘記了Dispose,也避免了忘記調用Dispose方法形成的內存泄漏。
提示:析構方法是在C++中的一種說法,由於終結器和析構方法二者特色很像,爲了沿襲C++的叫法,稱之爲析構方法也沒有什麼不妥,但它們又不徹底一致,因此微軟後來又肯定它叫終結器。
還有一點咱們也注意到了,若是已經顯式的調用了Dispose方法,那麼隱式釋放資源就再不必運行了,GC的SuppressFinalize方法就是通知GC的這一點:
1
2
3
4
5
6
7
|
public
void
Dispose()
{
//必須爲true
Dispose(
true
);
//通知垃圾回收器再也不調用終結器
GC.SuppressFinalize(
this
);
}
|
因此在實現的Dispose方法中先調用咱們正常的資源釋放代碼,再通知GC不要調用終結器了。
咱們注意到了,實現自Idisposable接口的Dispose方法並無作實際的清理工做,而是調用了咱們這個受保護的Dispose虛方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
protected
virtual
void
Dispose(
bool
disposing)
{
if
(disposed)
{
return
;
}
//清理託管資源
if
(disposing)
{
if
(ManagedResource !=
null
)
{
ManagedResource =
null
;
}
}
//清理非託管資源
if
(NativeResource != IntPtr.Zero)
{
Marshal.FreeHGlobal(NativeResource);
NativeResource = IntPtr.Zero;
}
//告訴本身已經被釋放
disposed =
true
;
}
|
之因此是虛方法,就是考慮到它若是被其餘類繼承時,子類也實現了Dispose模式,這個虛方法能夠提醒子類,清理的時候要注意到父類的清理工做,即若是子類從新該方法,必須調用base.Dispose方法,假設如今咱們有個子類,繼承自MyClass:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
public
class
MyClassChild : MyClass
{
/// <summary>
/// 模擬一個非託管資源
/// </summary>
private
IntPtr NativeResource {
get
;
set
; } = Marshal.AllocHGlobal(100);
/// <summary>
/// 模擬一個託管資源
/// </summary>
public
Random ManagedResource {
get
;
set
; } =
new
Random();
/// <summary>
/// 釋放標記
/// </summary>
private
bool
disposed;
/// <summary>
/// 非密封類可重寫的Dispose方法,方便子類繼承時可重寫
/// </summary>
/// <param name="disposing"></param>
protected
override
void
Dispose(
bool
disposing)
{
if
(disposed)
{
return
;
}
//清理託管資源
if
(disposing)
{
if
(ManagedResource !=
null
)
{
ManagedResource =
null
;
}
}
//清理非託管資源
if
(NativeResource != IntPtr.Zero)
{
Marshal.FreeHGlobal(NativeResource);
NativeResource = IntPtr.Zero;
}
base
.Dispose(disposing);
}
}
|
若是不是虛方法,那麼就頗有可能讓開發者在子類繼承的時候忽略掉父類的清理工做,因此,基於繼承體系的緣由,咱們要提供這樣的一個虛方法。
其次,提供的這個虛方法是一個帶bool參數的,帶這個參數的目的,是爲了釋放資源時區分對待託管資源和非託管資源,而實現自IDisposable的Dispose方法調用時,傳入的是true,而終結器調用的時候,傳入的是false,當傳入true時表明要同時處理託管資源和非託管資源;而傳入false則只須要處理非託管資源便可。
那爲何要區別對待託管資源和非託管資源?在這個問題以前,其實咱們應該先弄明白:託管資源須要手動清理嗎?不妨將C#的類型分爲兩類:一類實現了IDisposable,另外一類則沒有。前者咱們定義爲非普通類型,後者爲普通類型。非普通類型包含了非託管資源,實現了IDisposable,但又包含有自身是託管資源,因此不普通,對於咱們剛纔的問題,答案就是:普通類型不須要手動清理,而非普通類型須要手動清理。
而咱們的Dispose模式設計思路在於:若是顯式調用Dispose,那麼類型就該循序漸進的將本身的資源所有釋放,若是忘記了調用Dispose,那就假定本身的全部資源(哪怕是非普通類型)都交給GC了,因此不須要手動清理,因此這就理解爲何實現自IDisposable的Dispose中調用虛方法是傳true,終結器中傳false了。
同時咱們還注意到了,虛方法首先判斷了disposed字段,這個字段用於判斷對象的釋放狀態,這意味着屢次調用Dispose時,若是對象已經被清理過了,那麼清理工做就不用再繼續。
但Dispose並不表明把對象置爲了null,且已經被回收完全不存在了。但事實上,對象的引用還可能存在的,只是再也不是正常的狀態了,因此咱們明白有時候咱們調用數據庫上下文有時候爲何會報「數據庫鏈接已被釋放」之類的異常了。
因此,disposed字段的存在,用來表示對象是否被釋放過。
這句話讀起來可能有點繞啊,也就是說,若是對象的某些字段或屬性是IDisposable的子類,好比FileStream,那麼這個類也應該實現IDisposable。
以前咱們說過C#的類型分爲普通類型和非普通類型,非普通類型包含普通的自身和非託管資源。那麼,若是類的某個字段或屬性的類型是非普通類型,那麼這個類型也應該是非普通類型,應該也要實現IDisposable接口。
舉個栗子,若是一個類型,組合了FileStream,那麼它應該實現IDisposable接口,代碼以下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
public
class
MyClass2 : IDisposable
{
~MyClass2()
{
Dispose(
false
);
}
public
FileStream FileStream {
get
;
set
; }
/// <summary>
/// 釋放標記
/// </summary>
private
bool
disposed;
/// <summary>執行與釋放或重置非託管資源關聯的應用程序定義的任務。</summary>
public
void
Dispose()
{
Dispose(
true
);
GC.SuppressFinalize(
this
);
}
/// <summary>
/// 非密封類可重寫的Dispose方法,方便子類繼承時可重寫
/// </summary>
/// <param name="disposing"></param>
protected
virtual
void
Dispose(
bool
disposing)
{
if
(disposed)
{
return
;
}
//清理託管資源
if
(disposing)
{
//todo
}
//清理非託管資源
if
(FileStream !=
null
)
{
FileStream.Dispose();
FileStream =
null
;
}
//告訴本身已經被釋放
disposed =
true
;
}
}
|
由於類型包含了FileStream類型的字段,因此它包含了非普通類型,咱們仍舊須要爲這個類型實現IDisposable接口。
可能不少人會問啊,GC已經幫咱們隱式的釋放了資源,爲何還要主動地釋放資源,咱們先來看一個例子:
1
2
3
4
5
6
7
8
|
private
void
button6_Click(
object
sender, EventArgs e)
{
var fs =
new
FileStream(
@"C:\1.txt"
,FileMode.OpenOrCreate,FileAccess.ReadWrite);
}
private
void
button7_Click(
object
sender, EventArgs e)
{
GC.Collect();
}
|
上面的代碼在WinForm程序中,單擊按鈕6,打開一個文件流,單擊按鈕7執行GC回收全部「代」(下文將指出代的概念)的垃圾,若是連續單擊兩次按鈕6,將會拋異常:
若是單擊按鈕6再單擊按鈕7,而後再單擊按鈕6則不會出現這個問題。
咱們來分析一下:在單擊按鈕6的時候打開一個文件,方法已經執行完畢,fs已經沒有被任何地方引用了,因此被標記爲了垃圾,那麼何時被回收呢,或者GC何時開始工做?微軟官方的解釋是,當知足如下條件之一時,GC纔會工做:
系統具備較低的物理內存;
由託管堆上已分配的對象使用的內存超出了可接受的範圍;
手動調用GC.Collect方法,但幾乎全部的狀況下,咱們都沒必要調用,由於垃圾回收器會自動調用它,但在上面的例子中,爲了體驗一下不及時回收垃圾帶來的危害,因此手動調用了GC.Collect,你們也能夠仔細體會一下運行這個方法帶來的不一樣。
GC還有個「代」的概念,一共分3代:0代、1代、2代。而這三代,至關因而三個隊列容器,第0代包含的是一些短時間生存的對象,上面的例子fs就是個短時間對象,當方法執行完後,fs就被丟到了GC的第0代,但不進行垃圾回收,只有當第0代滿了的時候,系統認爲此時知足了低內存的條件,纔會觸發垃圾回收事件。因此咱們永遠不知道fs何時被回收掉,在回收以前,它實際上已經沒有用處了,但始終佔着系統資源不放(佔着茅坑不拉屎),這對系統來講是種極大的浪費,並且這種浪費還會干擾整個系統的運行,好比咱們的例子,因爲它始終佔着資源,就致使了咱們不能再對文件進行訪問了。
不及時釋放資源還會帶來另外的一個問題,雖然以前咱們說實現IDisposable接口的類,GC能夠自動幫咱們釋放,但這個過程被延長了,由於它不是在一次回收中完成全部的清理工做,即便GC自動幫咱們釋放了,那也是先調用FileStream的終結器,在下一次的垃圾回收時纔會真正的被釋放。
瞭解到危害後,咱們在打碼過程當中,若是咱們明知道它應該被using起來時,必定要using起來:
1
2
3
|
using
(var fs =
new
FileStream(
@"C:\1.txt"
, FileMode.OpenOrCreate, FileAccess.ReadWrite))
{
}
|
在上文的內容中,咱們都提到要釋放資源,但並無說明需不須要將再也不使用的對象置爲null,而這個問題也是一直以來爭議很大的問題,有人認爲將對象置爲null能讓GC更早地發現垃圾,也有人認爲這並無什麼卵用。其實這個問題首先是從方法的內部被提起的,爲了更好的說明這個問題,咱們先來段代碼來檢驗一下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
private
void
button6_Click(
object
sender, EventArgs e)
{
var mc1 =
new
MyClass() { Name =
"mc1"
};
var mc2 =
new
MyClass() { Name =
"mc2"
};
mc1 =
null
;
}
private
void
button7_Click(
object
sender, EventArgs e)
{
GC.Collect();
}
public
class
MyClass
{
public
string
Name {
get
;
set
; }
~MyClass()
{
MessageBox.Show(Name +
"被銷燬了"
);
}
}
|
單擊按鈕6,再單擊按鈕7,咱們發現:
沒有置爲null的mc2會先被釋放,雖然它在mc1被置爲null以後;
在CLR託管的應用程序中,有一個「根」的概念,類型的靜態字段、方法參數以及局部變量均可以被做爲「根」存在(值類型不能做爲「根」,只有引用類型才能做爲「根」)。
上面的代碼中,mc1和mc2在代碼運行過程當中分別會在內存中建立一個「根」。在垃圾回收的過程當中,GC會沿着線程棧掃描「根」(棧的特色先進後出,也就是mc2在mc1以後進棧,但mc2比mc1先出棧),檢查完畢後還會檢查全部引用類型的靜態字段的集合,當檢查到方法內存在「根」時,若是發現沒有任何一個地方引用這個局部變量的時候,無論你是否已經顯式的置爲null這都意味着「根」已經被中止,而後GC就會發現該根的引用爲空,就會被標記爲可被釋放,這也表明着mc1和mc2的內存空間能夠被釋放,因此上面的代碼mc1=null沒有任何意義(方法的參數變量也是如此)。
其實.NET的JIT編譯器是一個優化過的編譯器,因此若是咱們代碼裏面將局部變量置爲null,這樣的語句會被忽略掉:
s=null;
若是咱們的項目是在Release配置下的,上面的代碼壓根就不會被編譯到dll,正是因爲咱們上面的分析,因此不少人都會認爲將對象賦值爲null徹底沒有必要,可是,在另外一種狀況下,就徹底有必要將對象賦值爲null,那就是靜態字段或屬性,但這斌不意味着將對象賦值爲null就是將它的靜態字段賦值爲null:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
private
void
button6_Click(
object
sender, EventArgs e)
{
var mc =
new
MyClass() { Name =
"mc"
};
}
private
void
button7_Click(
object
sender, EventArgs e)
{
GC.Collect();
}
public
class
MyClass
{
public
string
Name {
get
;
set
; }
public
static
MyClass2 MyClass2 {
get
;
set
; } =
new
MyClass2();
~MyClass()
{
//MyClass2 = null;
MessageBox.Show(Name +
"被銷燬了"
);
}
}
public
class
MyClass2
{
~MyClass2()
{
MessageBox.Show(
"MyClass2被釋放"
);
}
}
|
上面的代碼運行咱們會發現,當mc被回收時,它的靜態屬性並無被GC回收,而咱們將MyClass終結器中的MyClass2=null的註釋取消,再運行,當咱們兩次點擊按鈕7的時候,屬性MyClass2才被真正的釋放,由於第一次GC的時候只是在終結器裏面將MyClass屬性置爲null,在第二次GC的時候纔看成垃圾回收了,之因此靜態變量不被釋放(即便賦值爲null也不會被編譯器優化),是由於類型的靜態字段一旦被建立,就被做爲「根」存在,基本上不參與GC,因此GC始終不會認爲它是個垃圾,而非靜態字段則不會有這樣的問題。
因此在實際工做當中,一旦咱們感受靜態變量所佔用的內存空間較大的時候,而且不會再使用,即可以將其置爲null,最典型的案例就是緩存的過時策略的實現了,將靜態變量置爲null這或許不是頗有必要,但這絕對是一個好的習慣,試想一個項目中,若是將某個靜態變量做爲全局的緩存,若是沒有作過時策略,一旦項目運行,那麼它所佔的內存空間只增不減,最終頂爆機器內存,因此,有個建議就是:儘可能地少用靜態變量。