一個簡單的事實:.Net應用程序是在一個托管的環境裡運行的,這個環境和 不同的設計器有很大的沖突,這就才有了Effective C#。極大限度上的討論這個 環境的好處,須要把你對本地化環境的想法改變為.Net CLR。也就意味著要明白 .Net的垃圾回收器。在你明白這一章裡所推薦的內容時,有必要對.Net的內存管 理環境有個大概的了解。那我們就開始大概的了解一下吧。
垃圾回收器 (GC)為你控制托管內存。不像本地運行環境,你不用負責對內存洩漏,不定指針 ,未初始化指針,或者一個其它內存管理的服務問題。但垃圾回收器前不是一個 神話:你一樣要自己清理。你要對非托管資源負責,例如文件句柄,數據鏈接, GDI+對象,COM對象,以及其它一些系統對象。
這有一個好消息:因為GC 管理內存,明確的設計風格可以更容易的實現。循環引用,不管是簡單關系還是 復雜的網頁對象,都非常容易。GC的標記以及嚴謹的高效算法可以檢測到這些關 系,並且完全的刪除不可達的網頁對象。GC是通過對從應用程序的根對象開始, 通過樹形結構的“漫游”來斷定一個對象是否可達的,而不是強迫每 個對象都保持一些引用跟蹤,COM就是這樣的。DataSet就是一個很好的例子,展 示了這樣的算法是如何簡化並決定對象的所屬關系的。DataSet是一個DataTable 的集合,而每一個DataTable又是DataRow的集合,每一個DataRow又是DataItem 的集合,DataColum定義了這些類型的關系。這裡就有一些從DataItem到它的列 的引用。而同時,DataTime也同樣有一個引用到它的容器上,也就是DataRow。 DataRow包含引用到DataTable,最後每個對象都包含一個引用到DataSet。
(譯注:作者這裡是想說:你看,這麼復雜的引用關系,GC都可以輕松的 搞定,你看GC是不是很強大?)
如果這還不夠復雜,那可以創建一個 DataView,它提供對經過過濾後的數據表的順序訪問。這些都是由 DataViewManager管理的。所有這些貫穿網頁的引用構成了DataSet。釋放內存是 GC的責任。因為.Net框架的設計者讓你不必釋放這些對象,這些復雜的網頁對象 引用不會造成問題。沒有必須關心這些網頁對象的合適的釋放順序,這是GC的工 作。GC的設計結構可以簡化這些問題,它可以識別這些網頁對象就是垃圾。在應 用程序結束了對DataSet的引用後,沒有人可以引用到它的子對象了(譯注:就是 DataSet裡的對象再也引用不到了)。因此,網頁裡還有沒有對象循環引用 DataSet,DataTables已經一點也不重要了,因為這些對象在應用程序都已經不 能被訪問到了,它們是垃圾了。
垃圾回收器在它獨立的線程上運行,用 來從你的程序裡移除不使用的內存。而且在每次運行時,它還會壓縮托管堆。壓 縮堆就是把托管堆中活動的對象移到一起,這樣就可以空出連續的內存。圖2.1 展示了兩個沒有進行垃圾回收時的內存快照。所有的空閒內存會在垃圾回收進行 後連續起來。
圖2.1 垃圾回收器不僅僅是移動不使用的內存,還移除動其它的對象 ,從而壓縮使用的內存,讓出最多的空閒內存。
正如你剛開始了解的, 垃圾回收器的全部責任就是內存管理。但,所有的系統資源都是你自己負責的。 你可以通過給自己的類型定義一個析構函數,來保證釋放一些系統資源。析構函 數是在垃圾回收器把對象從內存移除前,由系統調用的。你可以,也必須這樣來 釋放任何你所占用的非托管資源。對象的析構函數有時是在對象成為垃圾之後調 用的,但是在內存歸還之前。這個非確定的析構函數意味著在你無法控制對象析 構與停止使用之間的關系(譯注:對象的析構與對象的無法引用是兩個完全不同 的概念。關於GC,本人推薦讀者參考一下Jeffrey的".Net框架程序設計(修 訂版)"中討論的垃圾回收器)。對C++來說這是個重大的改變,並且這在設 計上有一個重大的分歧。有經驗的C++程序員寫的類總在構造函數內申請內存並 且在析構函數中釋放它們:
// Good C++, bad C#:
class CriticalSection
{
public:
// Constructor acquires the system resource.
CriticalSection( )
{
EnterCriticalSection( );
}
// Destructor releases system resource.
~CriticalSection( )
{
ExitCriticalSection( );
}
};
// usage:
void Func( )
{
// The lifetime of s controls access to
// the system resource.
CriticalSection s;
// Do work.
//...
// compiler generates call to destructor.
// code exits critical section.
}
這是一種很常見 的C++風格,它保證資源無異常的釋放。但這在C#裡不工作,至少,與這不同。 明確的析構函數不是.Net環境或者C#的一部份。強行用C++的風格在C#裡使用析 構函數不會讓它正常的工作。在C#裡,析構函數確實是正確的運行了,但它不是 即時運行的。在前面那個例子裡,代碼最終在critical section上,但在C#裡, 當析構函數存在時,它並不是在critical section上。它會在後面的某個未知時 間上運行。你不知道是什麼時候,你也無法知道是什麼時候。
依懶於析 構函數同樣會導致性能上的損失。須要析構的對象在垃圾回收器上放置了一劑性 能毒藥。當GC發現某個對象是垃圾但是須要析構時,它還不能直接從內存上刪除 這個對象。首先,它要調用析構函數,但析構函數的調用不是在垃圾回收器的同 一個線程上運行的。取而代之的是,GC不得不把對象放置到析構隊列中,讓另一 個線程讓執行所有的析構函數。GC繼續它自己的工作,從內存上移除其它的垃圾 。在下一個GC回收時,那些被析構了的對象才會再從內存上移除。圖2.2展示了 三個內存使用不同的GC情況。注意,那些須要析構的對象會待在內存裡,直到下 一次GC回收。
圖2.2 這個順序展示了析構函數在垃圾回收器上起的作用。對象會在 內存裡存在的時間更長,須要啟動另一個線程來運行垃圾回收器。
這用 使你相信:那些須要析構的對象在內存至少多生存一個GC回收循環。但,我是簡 化了這些事。實際上,因為另一個GC的介入(譯注:其實只有一個GC,作者是想 引用回收代的問題。),使得情況比這復雜得多。.Net回收器采用”代 “來優化這個問題。代可以幫助GC來很快的標識那些看上去看是垃圾的對 象。所以從上一次回後開始創建的對象稱為第0代對象,所有那些經過一次GC回 收後還存在的對象稱為第1代對象。所有那些經過2次或者2次以上GC回收後還存 在的對象稱為第2代對象(譯注:因為目前GC只支持3代對象,第0代到第2代,所 以最多只有第2代對象,如果今後GC支持更多的代,那麼會出現更代的對 象,.Net 1.1與2.0都只支持3代,這是MS證實比較合理的數字)。
分代的 目的就是用來區分臨時變量以及一些應用程序的全局變量。第0代對象很可能是 臨時的變量。成員變量,以及一些全局變量很快會成為第1代對象,最終成為第2 代對象。
GC通過限制檢測第1以及第2代對象來優化它的工作。每個GC循 環都檢測第0代對象。粗略假設個GC會超過10次檢測來檢測第0代對象,而要超過 100次來檢測所有對象。再次考慮析構函數的開銷:一個須要析構函數的對象可 能要比一個不用析構函數的對象在內存裡多待上9個GC回收循環。如果它還沒有 被析構,它將會移到第2代對象。在第2代對象中,一個可以生存上100個GC循環 直到下一個第2代集合(譯注:沒理解,不知道說的什麼)。
結束時,記得 一個垃圾回收器負責內存管理的托管環境的最大好處:內存洩漏,其它指針的服 務問題不在是你的問題。非內存資源迫使你要使用析構函數來確保清理非內存資 源。析構函數會對你的應用程序性能產生一些影響,但你必須使用它們來防止資 源洩漏(譯注:請注意理解非內存資源是什麼,一般是指文件句柄,網絡資源, 或者其它不能在內存中存放的資源)。通過實現IDisposable接口來避免析構函數 在垃圾回收器上造成的性能損失。接下來的具體的原則將會幫助你更有效的使用 環境來開發程序。
返回教程目錄