Effective C# 第二章:.Net资源管理(翻译)
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.
轉載于:https://www.cnblogs.com/WuCountry/archive/2007/02/28/660157.html
總結
以上是生活随笔為你收集整理的Effective C# 第二章:.Net资源管理(翻译)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 开发指南专题十四:JEECG微云快速开发
- 下一篇: JEECG 开源平台全视频和文档