運行在 Windows 下面的程序分配內存以便表現所需要的、不同類型的資源。可以將這些分配當作用來封裝程序所需要的內存和其他任何資源狀態的對象。 應用程序正確運行時,系統將釋放被使用的資源和內存,以便讓系統中的其他程序使用。但有時候,如果應用程序出現錯誤,則資源狀態或內存(或者這二者)都不會被正確釋放,這就會造成資源或內存洩漏。這些錯誤可能是很難識別的。垃圾回收器 (GC) 負責確保程序所分配的、用於完成任務的內存能夠在不需要開發人員關注它的情況下被釋放。 對垃圾回收了解得越多,就越能更好地構造程序與之配合使用。.NET 中的對象是從稱為托管堆的一片內存中分配來的。堆被描述為托管是因為您向它申請內存後,垃圾回收器會負責執行清理工作。這似乎需要很多開銷,因為垃圾回收器必須跟蹤在 .NET 公共語言運行庫 (CLR) 中所分配的每個對象,但實際上它工作得很有效率。 對象可以是小型對象,可以包含少量整數或更大的數據,也可以包含數據庫連接和很多狀態信息。對象可以是獨立的,也可以在內部包含或使用其他對象。GC 的工作是確定什麼時候應當回收對象,以便釋放內存供其他程序使用,當它認為它已被裝滿時就會對可以刪除的對象作標記,然後從托管堆中將它們刪除。當垃圾回收器試圖分配新的對象、卻發現托管堆沒有更多的可用內存時,垃圾回收器就會認為它已被裝滿。GC 試圖分配內存但確定它已被裝滿時,它將嘗試清理已為您的應用程序分配的某些內存,以便為新對象騰出空間。 GC 以略微不同的方式看待您的對象,並在決定什麼時候回收它認為不再有用的對象時考慮到這些對象的差異。它這樣做的一個方法是,它有一組根對象,用來確定哪些對象可以被回收。如果對對象的引用大體上屬於如下分類中的某一個,則該引用就被看作是根:全局或靜態對象指針、線程的堆棧上的所有局部變量和參數對象指針、或包含托管堆中的對象的指針的任何 CPU 寄存器。如果對象的引用是根引用,那麼它可能有或可能沒有與它關聯的、還會在垃圾回收後幸存的子對象。GC 首先找到根對象,然後沿著引用找到被根引用的其他對象,以便避免回收這些對象。 如圖 1 所示,托管堆中有四個被分配的對象:(S)mall、(L)arge、(F)inalized 和 (R)eferenced。假設每個對象通過其主要特征(例如,小型對象都不會包含引用或其他組合)來標識自己。在堆中分配這些對象時,它們將相互緊鄰地放在內存中。我也有一個位於 (G)lobal 范圍的根引用,它包含對 Z 的引用。 GC 開始垃圾回收時,它首先假設所有對象都是不必要的,直到這些對象被證明是需要的為止。對象基本上通過它“認識”誰或引用了誰,或誰引用了它或認識它,來證明自己是必要的。對於 GC,根引用為誰認識誰提供了起點。GC 從根對象開始沿著對象層次結構檢查引用情況,以確定對象是否是可到達的,或是否有可能被另一個對象使用。如果對象被證明是可到達的,則它不是該垃圾回收周期的處理對象。如果對象被證明無法從任何引用到達它,則 GC 將把該對象標記為可回收,然後它會被丟棄。GC 使用“標記和壓縮”方法,這意味著一旦 GC 確定對象是垃圾,則 GC 的另一個部分將刪除無法到達的對象,並將壓縮堆中的空間以確保分配將繼續非常快速地進行。 GC 以代的方式看待回收周期中所涉及的對象。每當對象被認為是可到達的時,它就會被提升到下一代。這意味著,引用您的對象的對象越多,或您的對象的操作范圍越大,它的存活時間就越長。GC 當前最多有三代,從 0 到 2。第 0 代通常填充較小、短期使用的對象,並且回收它們的次數最多。這意味著,如果您有小型或很少使用的對象,則它們將被頻繁地回收。第 1 代和第 2 代是壽命更長和被更頻繁訪問的對象的儲存庫,因此被回收的頻率更低。GC 中一個基本假設是,您的程序中有更小、壽命更短的對象,更頻繁地清理它們對您有好處。理解這一點很重要,因為您設計系統的方式會對您使用多少內存和占用內存多長時間有巨大的影響,這是由於您的工作集將是大型的工作集。內存使用量越大,應用程序性能將降低得越多。 85,000 字節以下的對象被認為是小型對象,並且從托管堆的主要部分直接分配。超過 85,000 字節的對象從托管堆的特殊部分(稱為大型對象堆)分配。托管堆對待小型和大型對象的方式有兩個主要差異。首先,小型對象在被壓縮時將移到托管堆內;而大型對象則不是這樣。其次,大型對象總是被當作第 2 代的一部分,而小型對象通常被當作第 0 代的一部分。如果您分配了很多短壽命的大型對象,這將造成第 2 代被更頻繁地回收。由於從第 0 代到第 2代越往後的回收成本越高,這將有損應用程序的性能。 我想討論的垃圾回收的最後一個方面是終結 (finalization) 的概念。當對象被 GC 回收時,終結幫助開發人員釋放他們在其對象中使用的資源。對象需要實現 Finalize 方法才能完成該操作。當對象要被銷毀時,GC 將調用 Finalize 方法,以便允許對象清理它的內部資源和狀態。在 C# 和托管 C 中,Finalize 方法實際上偽裝在析構函數的語法 (~Object) 中,這裡的 Finalize 方法與純 C 中的 Finalize 方法之間的重大差異是,在 C# 和托管 C 中,只有當 GC 清理對象時才調用該方法,而在純 C 的析構函數中,當對象脫離范圍時才會調用該方法。將 Finalize 方法添加到您的對象中意味著它將總是被 GC 調用,但要小心,因為將 Finalize 方法添加到對象中時,該對象將總是會在對第一代的垃圾回收後幸存下來。因此,所有終結對象的壽命會更長。由於試圖讓 GC 盡可能有效地執行清理,因此,只有當您有非托管資源需要清理或者在對象創建成本高昂的特殊情況下(對象池),才應當使用終結。 讓我們返回圖 1 中的原始示例,該示例有一個托管堆,其中包含四個對象和一個根引用。如果在這個時候發生垃圾回收(這是由於這時不滿足啟動垃圾回收的條件,而開發人員手動干預造成的),結果是 (S)mall 對象將被當作垃圾回收。 大型對象將在該垃圾回收後幸存下來,因為大型對象被指派為第 2 代。被終結的對象被 GC 注意到,並且將調用 Finalize 方法,但是對象本身仍將保留下來,直到進行下一次垃圾回收為止(在某些情形下可能會更長)。包含根引用 G 的對象將保留下來;因為它是根引用,是可到達的。
現在,讓我們假設下一次發生的垃圾回收針對的是第 0 到第 2 代(可以通過調用 System.GC.Collect 方法並將 2 作為參數來完成該操作)。(L)arge 對象將在第 2 代清理期間被回收,而 (F)inalized 對象在第 0 代回收期間被回收,這是因為 Finalize 已被調用並且已在回收開始之前結束操作。只有包含全局引用的對象仍然存在,因而會在應用程序生存期內保留下來。 良好的內存使用率 GC 負責處理內存洩漏,但它不能防止內存保留。作為開發人員,您可以控制您的對象的生存期。如果可以減少應用程序的工作集,則性能將有所提高。如果您的應用程序被設計為有很多對象長時間存活,則可能會有內存洩漏。即使最後清理了內存,仍然會有損性能,所以知道您的對象存活多長時間是值得的。 GC 可以提供很大幫助,但它只能處理我討論過的一種原始類型的洩漏。資源洩漏仍然是個問題,但如果將非托管資源包裝在終結類中,GC 仍然可以幫助您確保正確處置它們。最好對對象實現 Close 或 Dispose 方法,以便在使用完對象時資源可以盡可能早得到清理,而不用等待 GC 來清理它們(在您停止使用對象後,等待 GC 清理它們可能需要很長時間)。如果您對使用非托管資源的類實現了 Finalize,並且正在使用托管堆,則可以相當安全地避免真正的洩漏。當然,這並不意味著您應當讓應用程序的工作集很龐大,因為這仍然會有損性能。 Profiler API 概述 為了說明應用程序使用了多少內存,以及對象存在了多久,我開發了一個稱為 MemoryUsage 的應用程序。MemoryUsage 有兩個不同的部分。第一部分編寫為 C# 應用程序,它將啟動要監視的進程,並在目標進程中設置一個環境變量,以指示 CLR 應當加載 .NET 分析器 (profiler)。第二部分編寫為基於 C 的 .NET 分析器,該分析器名為 MemProfiler,,CLR 將通過環境變量中的信息加載它。.NET 分析器是使用作為 CLR 的一部分提供的 Profiler API 來編寫的,它允許分析器作為被監視的進程的一部分運行,並在發生某些事件時接收通知。當應用程序執行時,它為您提供各種通知。為了從 CLR 接收這些通知,您要提供一個 Profiler API 中指定的回調接口 (ICorProfilerCallback),然後,當各種事件發生時,CLR 將調用這個回調接口的方法(參見圖 2)。 下面是需要注意的主要分析器回調方法:RuntimeSuspendStarted、RuntimeSuspendFinished、RuntimeResumeStarted、ObjectAllocated、ObjectsAllocatedByClass、MovedReferences、RootReferences 和 ObjectReferences。 如果不熟悉 Profiler API,可以閱讀 Profiler.doc(位於 Visual Studio .NET 安裝目錄下面的 \FrameworkSDK\Tool Developers Guide\docs 文件夾中),來了解某些更深入的信息。 使用分析器時有幾件事情要考慮到,包括線程安全和同步,以及分析器對性能的影響。Profiler API 實際上允許您將它作為 CLR 的一部分運行,這樣,因為多個線程將調用您的分析器,所以您必須知道存在同步問題。Microsoft 提供的 Profiler API 規范聲明:回調不會被序列化。這就需要由開發人員自己來正確保護他的代碼,方法是創建線程安全的數據結構,並在一旦需要防止多個線程並行訪問代碼時鎖定分析器代碼。 我需要使對對象跟蹤系統以及