內存管理、資源管理(參考整理自李建忠老師)


內存管理是一個非常重要的東西,一個好的程序員應該對內存模型和內存管理有一個好的認識。首先我們了解一下資源,資源分為兩類,即托管資源和非托管資源。

托管資源:托管堆內存

非托管資源:文件句柄、數據庫鏈接、本地內存等

這里的托管與非托管即指的是我們常說的垃圾收集器,被垃圾收集器管理的資源叫托管資源,我們常說的內存管理即指的是托管資源的管理,而資源管理即指的是非托管資源的管理。

下面我們談談.NET的內存管理

在分配內存的過程中,當保留的內存區域全部被用光時,這時候GC啟動,通過對象圖,進行掃描,找到那些不可達的對象,這些對象即為垃圾對象,然后對內存區域進行壓縮,使垃圾對象被覆蓋掉(這里基於一個前提:托管堆上的內存是連續分配的),由於對象的位置發生了偏移,因此需要進行指針更新。

.NET線式分配,間歇性壓縮和搬移對象內存,對象地址不穩定(快、無碎片)

C++鏈式分配,對象地址穩定不變(慢,有碎片)

垃圾收集器的啟動是不定時的,只有當內存不夠使用時才會被啟動,由於無內存碎片,因此C#比較適合開發服務器端應用程序。

標記壓縮法的三大特點:

1.分配速度快,回收速度慢

2.確保空閑內存區域連續,避免內存碎片

3.對象地址不穩定

分代式垃圾收集:

CLR執行一個單獨的線程來負責垃圾收集器,這時它會掛起當前的所有線程

分代式垃圾收集器區分代齡基於以下假設:

對象越新,其生存周期越短

對象越老,其生存周期越長

每一個托管堆對象分配一個代齡:

0代對象限額:256K

1代對象限額:2M

2代對象限額:10M

每個對象的代齡存儲在對象的第一個保留字段區域

首先垃圾收集器將所有的對象分為0、1、2三代,其中大部分對象被分為0代,當0代對象越來越多,達到了內存限額256K,這時垃圾收集器啟動0代收集,將那些不可達對象進行收集,那些經過0代收集仍然存活的對象,垃圾收集器會對其進行搬移,將其轉為1代對象。1代和2代得收集原理和0代相似,只不過在啟動1代收集時同時會啟動0代收集,在啟動2代收集時同時會啟動0代、1代收集。

對於一些比較小的軟件,垃圾收集器的二代是不會啟動的,如果運行的時間非常長,二代才可能會啟動,垃圾收集器的這種機制就是使那些難以回收的對象的晚一些回收,對於一些比較復雜的對象,垃圾收集器一開始就將其標記為2代對象,對於一些比較簡單的對象,垃圾收集器一開始就將其標記為0代對象,這就是所謂的垃圾收集器“欺軟怕硬”的精神。

代齡的內存分配是動態調整的,如果垃圾收集器發現系統頻繁的調用0代、1代、2代收集器,同時它又發現你系統的內存比較大,它會把每一代的內存限額相應的調大一些,使得垃圾收集器啟動的不那么頻繁。這是它懶惰的表現

垃圾收集器沒有提供任何一種方法只回收一個特定的對象,垃圾收集器從來不屑與去收集一個對象,因為收集一個對象的代價過高,如果我們想回收特定的某一代的對象,GC提供了一個函數

GC.Collect(),這個函數在System命名空間,其參數只能為0、1、2,如果不帶參數,則強行對所有代進行垃圾回收,如果帶參數,則強行對0代到指定代進行垃圾回收。這種方法是不被鼓勵使用的,GetGeneration(Object),返回指定對象的當前代數。這些方法只限於實驗性的使用。

資源管理(非托管資源)

GC主要負責回收內存,對於非托管資源只能進行輔助性的回收,下面我們舉一個例子

using System;
class MyClass
{
int x; //純內存對象,垃圾收集器可完全回收
int y;

IntPtr handle //資源對象,代表一個資源
public MyClass()
{
handle=OpenHandle(); //獲取資源
}
}
class Test
{
public static void Main()
{
MyClass obj=new MyClass();
//............
//............
//GC啟動
}
}

獲取資源:即向Windows操作系統去請求資源,Windows操作系統會標記出當前進程所占用的資源,操作系統是以進程為單位來划分資源的,只要進程獲取了資源,且進程不關閉,Windows操作系統就會一直認為這個資源被占用着,直到進程被關閉。

上面的代碼中,我們定義了一個資源類(如數據訪問類就是一個資源類),然后在測試函數中我們創建了一個資源類的對象,同時在其構造函數中獲取了相應的資源,

下面我們通過圖解說明:

當這個對象使用完以后就成為了垃圾對象,GC啟動對對象進行回收,但是GC並不會對對象所使用的資源進行釋放,因為你沒有告訴垃圾收集器資源不用了。此時即產生了資源泄漏,直至進程關閉,其所占用的資源才會被釋放。在一些小的程序中,即便資源泄漏,也不會對系統產生多大的影響,因為進程可能很快就會被關閉,但是對於一些服務器運行程序,其運行時間一般會很長,其造成的資源泄漏是不容忽視的。

下面我們對上面的代碼進行改進:

using System;
class MyClass
{
int x; //純內存對象,垃圾收集器可完全回收
int y;

IntPtr handle //資源對象,代表一個資源
public MyClass()
{
handle=OpenHandle(); //獲取資源
}
~MyClass() //析構器
{
CloseHandle(handle); //釋放資源
}
}
class Test
{
public static void Main()
{
MyClass obj=new MyClass();
//............
//............
//GC啟動
}
}

在這里我們在析構器中對資源進行了釋放,當GC啟動的時候,如果析構器不存在,GC就會直接回收對象的內存,但由於有了析構器,這時GC就不會選擇直接回收對象內存了,轉而去調用析構器了,而且GC在對對象進行第一次回收時只會執行析構器,不會回收內存,也就是說垃圾收集器在一次執行過程中只能做一件事情。因此對象就會變為1代對象,這就是為什么會析構器會托大對象的代齡了。

為了避免對象的代齡被托大,同時為了能夠及時的釋放對象所占用的資源,把釋放資源的任務交給垃圾收集器是不可靠的,下面我們看一個改進的例子:

using System;
class MyClass : IDisposable
{
int x; //純內存對象,垃圾收集器可完全回收
int y;
IntPtr handle //資源對象,代表一個資源
public MyClass()
{
handle=OpenHandle(); //獲取資源
}
public void Dispose()
{
CloseHandle(handle) //釋放資源
GC.SuppressFinalize(this); //告訴系統就當我沒有寫析構器,避免托大代齡
}
~MyClass() //析構器
{
CloseHandle(handle) //釋放資源
}
}
class Test
{
public static void Main()
{
MyClass obj=new MyClass();
obj.Dispose();
//............
//............
//GC啟動
}
}

在這里我們通過IDisposable接口實現了Dispose()方法,即我們在對象使用完畢以后可以自行調用Dispose()方法來實現對資源的釋放,不用等待析構器去釋放資源,同時我們在這個函數中告訴垃圾收集器不要調用析構器,避免代齡被托大,其實Dispose()方法可以用一個普通的方法來表示,但是,通過接口來實現資源的釋放,可以讓別人一眼就能看出這是一個資源類。

這里又有一個問題,既然通過Dispose()方法實現了資源的自行釋放,那為什么還需要寫析構器了,原因就是以防程序員在編寫程序的過程中忘記了調用Dispose()方法去釋放資源,這時也會造成資源的泄露,因此為了絕對的安全,還是需要將析構器寫上。

上面這些也就形成了資源處理的一個設計模式,這個設計模式有一個約定,即把釋放資源的方法的名字叫做Dispose,同時該方法所在的類需要實現IDisposable接口,有了這個接口以后,就可以讓使用類的程序員知道該類為一個資源類,當對象用完的時候一定要調用Dispose()方法。

析構器的本質是一個Finalize方法,當我們編譯完上面的代碼,用Ildasm工具去查看去IL代碼,就會發現析構器被編譯成了一個Finalize方法。它是重寫了基類的一個受保護的虛方法。

但是如果程序在執行過程中拋出了異常,導致Dispose()方法沒有被執行,這時雖然有析構器確保資源被釋放,但是這並不是我們的本意,因此為了確保Dispose()方法被執行,我們需要寫一個異常處理的語句結構,下面我們看一看代碼

using System;
class MyClass : IDisposable
{
int x; //純內存對象,垃圾收集器可完全回收
int y;
IntPtr handle //資源對象,代表一個資源
public MyClass()
{
handle=OpenHandle(); //獲取資源
}
public void Dispose()
{
CloseHandle(handle) //釋放資源
GC.SuppressFinalize(this); //告訴系統就當我沒有寫析構器,避免托大代齡
}
~MyClass() //析構器
{
CloseHandle(handle) //釋放資源
}
public void Process()
{
//...........
}
}
class Test
{
public static void Main()
{
MyClass obj=null;
try
{
obj=new MyClass();
obj.Process();
}
catch(Exception e)
{
throw e;
}
finally
{
if (obj!=null)
{
obj.Dispose();
}
}
//............
//............
//GC啟動
}
}

這樣就確保了Dispose()方法會被執行,這種確保資源被釋放的處理方法可以使用using語句來處理,using語句塊可以確保對象的Dispose()方法被執行,不管是否出現異常,其實using語句的實現就是通過上面的異常處理方式實現的,因此前提是對象的類實現了IDisposable接口,內部具有Dispose()方法。上面的代碼可以改為:
using System;
class MyClass : IDisposable
{
int x; //純內存對象,垃圾收集器可完全回收
int y;
IntPtr handle //資源對象,代表一個資源
public MyClass()
{
handle=OpenHandle(); //獲取資源
}
public void Dispose()
{
CloseHandle(handle) //釋放資源
GC.SuppressFinalize(this); //告訴系統就當我沒有寫析構器,避免托大代齡
}
~MyClass() //析構器
{
CloseHandle(handle) //釋放資源
}
public void Process()
{
//...........
}
}
class Test
{
public static void Main()
{
using(MyClass obj=new MyClass())
{
obj.Process();
}
//............
//............
//GC啟動
}
}
這里補充一些:析構器里面一般只用於釋放資源,最好不要去做其他事情,因為在有些情況下,析構器根本就不會被調用。









免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2026 CODEPRJ.COM