Effective C# 第二章:.Net資源管理(翻譯)

來源:互聯網
上載者:User

Chapter 2. .NET Resource Management
第二章:.Net資源管理

一個簡單的事實:.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介面來避免解構函式在記憶體回收行程上造成的效能損失。接下來的具體的原則將會協助你更有效使用環境來開發程式。

 

   

Chapter 2. .NET Resource Management
The simple fact that .NET programs run in a managed environment has a big impact on the kinds of designs that create effective C#. Taking utmost advantage of that environment requires changing your thinking from native environments to the .NET CLR. It means understanding the .NET Garbage Collector. An overview of the .NET memory management environment is necessary to understand the specific recommendations in this chapter, so let's get on with the overview.

The Garbage Collector (GC) controls managed memory for you. Unlike native environments, you are not responsible for memory leaks, dangling pointers, uninitialized pointers, or a host of other memory-management issues. But the Garbage Collector is not magic: You need to clean up after yourself, too. You are responsible for unmanaged resources such as file handles, database connections, GDI+ objects, COM objects, and other system objects.

Here's the good news: Because the GC controls memory, certain design idioms are much easier to implement. Circular references, both simple relationships and complex webs of objects, are much easier. The GC's Mark and Compact algorithm efficiently detects these relationships and removes unreachable webs of objects in their entirety. The GC determines whether an object is reachable by walking the object tree from the application's root object instead of forcing each object to keep track of references to it, as in COM. The DataSet class provides an example of how this algorithm simplifies object ownership decisions. A DataSet is a collection of DataTables. Each DataTable is a collection of DataRows. Each DataRow is a collection of DataItems. Each DataTable also contains a collection of DataColumns. DataColumns define the types associated with each column of data. There are other references from the DataItems to its appropriate column. Every DataItem also contains a reference to its container, the DataRow. DataRows contain references back to the DataTable, and everything contains a reference back to the containing DataSet.

If that's not complicated enough, you can create DataViews that provide access to filtered sequences of a data table. Those are all managed by a DataViewManager. There are references all through the web of objects that make up a DataSet. Releasing memory is the GC's responsibility. Because the .NET Framework designers did not need to free these objects, the complicated web of object references did not pose a problem. No decision needed to be made regarding the proper sequence of freeing this web of objects; it's the GC's job. The GC's design simplifies the problem of identifying this kind of web of objects as garbage. After the application releases its reference to the dataset, none of the subordinate objects can be reached. It does not matter that there are still circular references to the DataSet, DataTables, and other objects in the web. Because these objects cannot be reached from the application, they are all garbage.

The Garbage Collector runs in its own thread to remove unused memory from your program. It also compacts the managed heap each time it runs. Compacting the heap moves each live object in the managed heap so that the free space is located in one contiguous block of memory. Figure 2.1 shows two snapshots of the heap before and after a garbage collection. All free memory is placed in one contiguous block after each GC operation.


Figure 2.1. The Garbage Collector not only removes unused memory, but it moves other objects in memory to compact used memory and maximize free space.

As you've just learned, memory management is completely the responsibility of the Garbage Collector. All other system resources are your responsibility. You can guarantee that you free other system resources by defining a finalizer in your type. Finalizers are called by the system before an object that is garbage is removed from memory. You canand mustuse these methods to release any unmanaged resources that an object owns. The finalizer for an object is called at some time after it becomes garbage and before the system reclaims its memory. This nondeterministic finalization means that you cannot control the relationship between when you stop using an object and when its finalizer executes. That is a big change from C++, and it has important ramifications for your designs. Experienced C++ programmers wrote classes that allocated a critical resource in its constructor and released it in its destructor:

// 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.
}

 

This common C++ idiom ensures that resource deallocation is exception-proof. This doesn't work in C#, howeverat least, not in the same way. Deterministic finalization is not part of the .NET environment or the C# language. Trying to force the C++ idiom of deterministic finalization into the C# language won't work well. In C#, the finalizer eventually executes, but it doesn't execute in a timely fashion. In the previous example, the code eventually exits the critical section, but, in C#, it doesn't exit the critical section when the function exits. That happens at some unknown time later. You don't know when. You can't know when.

Relying on finalizers also introducesperformance penalties. Objects that require finalization put a performance drag on the Garbage Collector. When the GC finds that an object is garbage but also requires finalization, it cannot remove that item from memory just yet. First, it calls the finalizer. Finalizers are not executed by the same thread that collects garbage. Instead, the GC places each object that is ready for finalization in a queue and spawns yet another thread to execute all the finalizers. It continues with its business, removing other garbage from memory. On the next GC cycle, those objects that have been finalized are removed from memory. Figure 2.2 shows three different GC operations and the difference in memory usage. Notice that the objects that require finalizers stay in memory for extra cycles.


Figure 2.2. This sequence shows the effect of finalizers on the Garbage Collector. Objects stay in memory longer, and an extra thread needs to be spawned to run the Garbage Collector.

This might lead you to believe that an object that requires finalization lives in memory for one GC cycle more than necessary. But I simplified things. It's more complicated than that because of another GC design decision. The .NET Garbage Collector defines generations to optimize its work. Generations help the GC identify the likeliest garbage candidates more quickly. Any object created since the last garbage collection operation is a generation 0 object. Any object that has survived one GC operation is a generation 1 object. Any object that has survived two or more GC operations is a generation 2 object. The purpose of generations is to separate local variables and objects that stay around for the life of the application. Generation 0 objects are mostly local variables. Member variables and global variables quickly enter generation 1 and eventually enter generation 2.

The GC optimizes its work by limiting how often it examines first- and second-generation objects. Every GC cycle examines generation 0 objects. Roughly 1 GC out of 10 examines the generation 0 and 1 objects. Roughly 1 GC cycle out of 100 examines all objects. Think about finalization and its cost again: An object that requires finalization might stay in memory for nine GC cycles more than it would if it did not require finalization. If it still has not been finalized, it moves to generation 2. In generation 2, an object lives for an extra 100 GC cycles until the next generation 2 collection.

To close, remember that a managed environment, where the Garbage Collector takes the responsibility for memory management, is a big plus: Memory leaks and a host of other pointer-related problems are no longer your problem. Nonmemory resources force you to create finalizers to ensure proper cleanup of those nonmemory resources. Finalizers can have a serious impact on the performance of your program, but you must write them to avoid resource leaks. Implementing and using the IDisposable interface avoids the performance drain on the Garbage Collector that finalizers introduce. The next section moves on to the specific items that will help you create programs that use this environment more effectively.

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.