CLR 全面透徹解析--大型對象堆揭祕

CLR 垃圾回收器 (GC) 將對象分爲大型、小型兩類。如果是大型對象,與其相關的一些屬性將比對象較小時顯得更爲重要。例如,壓縮大型對象(將內存複製到堆上的其他位置)的費用相當高。在本月的專欄中,我將深入探討大型對象堆。我將討論符合什麼條件的對象才能稱之爲大型對象,如何回收這些大型對象,以及大型對象具備哪些性能意義。
大型對象堆和 GC
在 Microsoft® .NET Framework 1.1 和 2.0 中,如果對象大於或等於 85,000 字節,將被視爲大型對象。此數字根據性能優化的結果確定。當對象分配請求傳入後,如果符合該大小閾值,便會將此對象分配給大型對象堆。這究竟是什麼意思呢?要理解這些內容,先了解一些關於 .NET 垃圾回收器的基礎知識可能會有所幫助。
衆所周知,.NET 垃圾回收器是分代回收器。它包含三代:第 0 代、第 1 代和第 2 代。之所以分代,是因爲在良好調優的應用程序中,您可以在第 0 代清除大部分對象。例如,在服務器應用程序中,與每個請求關聯的分配將在完成請求後清除。仍存在的分配請求將轉到第 1 代,並在那裏進行清除。從本質上講,第 1 代是新對象區域與生存期較長的對象區域之間的緩衝區。
從分代的角度來說,大型對象屬於第 2 代,因爲只有在第 2 代回收過程中才能回收它們。回收一代時,同時也會回收所有前面的代。例如,執行第 1 代垃圾回收時,將同時回收第 1 代和第 0 代。執行第 2 代垃圾回收時,將回收整個堆。因此,第 2 代垃圾回收也稱爲完整垃圾回收。在本專欄中,我將使用術語“第 2 代垃圾回收”而不是“完整垃圾回收”,但它們可以互換。
垃圾回收器堆的各代是按邏輯劃分的。實際上,對象存在於託管堆棧段上。託管堆棧段是垃圾回收器通過調用 VirtualAlloc 代表託管代碼在操作系統上保留的內存塊。加載 CLR 時,將分配兩個初始堆棧段(一個用於小型對象,另一個用於大型對象),我將它們分別稱爲小型對象堆 (SOH) 和大型對象堆 (LOH)。
然後,通過將託管對象置於任一託管堆棧段上來滿足分配請求。如果對象小於 85,000 字節,則將其放在 SOH 段上;否則將其放在 LOH 段上。隨着分配到各段上的對象越來越多,會以較小塊的形式提交這些段。
對於 SOH,垃圾回收未處理的對象將進入下一代;由此第 0 代回收未處理的對象將被視爲第 1 代對象,依此類推。但是,最後一代回收未處理的對象仍會被視爲最後一代中的對象。也就是說,第 2 代垃圾回收未處理的對象仍是第 2 代對象;LOH 未處理的對象仍是 LOH 對象(由第 2 代回收)。用戶代碼只能在第 0 代(小型對象)或 LOH(大型對象)中分配。只有垃圾回收器可以在第 1 代(通過提升第 0 代回收未處理的對象)和第 2 代(通過提升第 1 代和第 2 代回收未處理的對象)中“分配”對象。
觸發垃圾回收後,垃圾回收器將尋找存在的對象並將它們壓縮。不過對於 LOH,由於壓縮費用很高,CLR 團隊會選擇掃過所有對象,列出沒有被清除的對象列表以供以後重新使用,從而滿足大型對象的分配請求。相鄰的被清除對象將組成一個自由對象。
有一點必須注意,雖然目前我們不會壓縮 LOH,但將來可能會進行壓縮。因此,如果您分配了大型對象並希望確保它們不被移動,則應將其固定起來。
請注意,下面的圖僅用於說明。我使用了很少的對象,只爲說明堆上發生的事件。實際上,還存在許多對象。
圖 1 說明了一種情況,在第一次第 0 代 GC 後形成了第 1 代,其中 Obj1 和 Obj3 被清除;在第一次第 1 代 GC 後形成了第 2 代,其中 Obj2 和 Obj5 被清除。
cc534993.fig01.gif
圖 1 SOH 分配和垃圾回收(單擊圖像可查看大圖)
圖 2 說明在第 2 代垃圾回收後,您將看到 Obj1 和 Obj2 被清除,內存中原來存放 Obj1 和 Obj2 的空間將成爲一個可用空間,隨後可用於滿足 Obj4 的分配請求。從最後一個對象 Obj3 到此段末尾的空間仍可用於以後的分配請求。
cc534993.fig02.gif
圖 2 LOH 分配和垃圾回收(單擊圖像可查看大圖)
如果沒有足夠的可用空間來容納大型對象分配請求,我會先嚐試從操作系統獲取更多段。如果失敗,我將觸發第 2 代垃圾回收以便釋放一些空間。
在第 2 代垃圾回收期間,我會把握時機將不包含任何活動對象的段釋放回操作系統(通過調用 VirtualFree)。從最後一個存在的對象到該段末尾的內存將退回。而且,儘管已重置可用空間,但仍會提交它們,這意味着操作系統無需將其中的數據重新寫入磁盤。圖 3 說明了一種情況,我將一個段(段 2)釋放回操作系統,並在剩下的段中退回了更多空間。如果需要使用該段末尾的已退回空間來滿足新的大型對象分配請求,我可以再次提交該內存。
cc534993.fig03.gif
圖 3 垃圾回收期間在 LOH 上釋放的已消除段(單擊圖像可查看大圖)
有關提交/退回的說明,請參閱有關 VirtualAlloc 的 MSDN® 文檔,網址爲 go.microsoft.com/fwlink/?LinkId=116041
何時回收大型對象
要確定何時回收大型對象,我們首先討論一下通常何時會執行垃圾回收。如果發生下列情況之一,將執行垃圾回收:
分配超出第 0 代或大型對象閾值 大部分 GC 都是由於需在託管堆上進行分配而執行(這是最典型的情況)。
調用 System.GC.Collect 如果對第 2 代調用 GC.Collect(通過不向 GC.Collect 傳遞參數或將 GC.MaxGeneration 作爲參數傳遞),將立即回收 LOH 及其他託管堆。
系統內存太低 收到來自操作系統的高內存通知時會發生此情況。如果我認爲執行第 2 代垃圾回收會有所幫助,就會觸發一個垃圾回收。
閾值是各代的屬性。將對象分配給某代時,會增加該代的內存量,使之接近該代的閾值。當超出某代的閾值時,便會在該代觸發垃圾回收。因此,當您分配小型或大型對象時,需要分別使用第 0 代和 LOH 的閾值。當垃圾回收器分配到第 1 代和第 2 代中時,將使用第 1 代的閾值。運行此程序時,會動態調整這些閾值。
LOH 性能意義
下面,我們來看一下分配成本。CLR 確保清除了我提供的每個新對象的內存。這意味着大型對象的分配成本完全由清理的內存(除非觸發了垃圾回收)決定。如果需要兩輪才能清除 1 個字節,則意味着需要 170,000 輪才能清除最小的大型對象。這對於分配較大的大型對象的人們來說很平常。對於 2GHz 計算機上的 16MB 對象,大約需要 16ms 才能清除內存。這些成本相當大。
現在我們來看一下回收成本。前面曾提到,LOH 和第 2 代將一起回收。如果超過兩者中任何一個的閾值,都會觸發第 2 代回收。如果由於第 2 代爲 LOH 而觸發了第 2 代回收,則第 2 代本身在垃圾回收後不一定會變得更小。因此,如果第 2 代中的數據不多,這將不是問題。但是,如果第 2 代很大,則觸發多次第 2 代垃圾回收可能會產生性能問題。如果要臨時分配許多大型對象,並且您擁有一個大型 SOH,則運行垃圾回收可能會花費很長時間;毫無疑問,如果仍繼續分配和處理真正的大型對象,分配成本肯定會大幅增加。
LOH 上的特大對象通常是數組(很少會有非常大的實例對象)。如果數組元素包含很多引用,則成本將會很高。如果元素不包含任何引用,則根本無需處理此數組。例如,如果使用數組存儲二進制樹中的節點,一種實現方法是按實際節點引用某個節點的左側節點和右側節點:
class Node
{
    Data d;
    Node left;
    Node right;
};

Node[] binary_tr = new Node [num_nodes];
如果 num_nodes 很大,則意味着至少需要對每個元素處理兩個引用。另一種方法是存儲左側節點和右側節點的索引:
class Node
{
    Data d;
    uint left_index;
    uint right_index;
};
這樣,您可將左側節點的數據作爲 binary_tr[left_index].d 引用,而非作爲 left.d 引用;而垃圾回收器無需查看左側節點和右側節點的任何引用。
在這三個回收原因中,通常前兩個比第三個出現得多。因此,最好能夠分配一個大型對象池並重新使用這些對象,而不是分配臨時對象。Yun Jin 在其博客日誌 (go.microsoft.com/fwlink/?LinkId=115870) 中介紹了一個此類緩衝池的示例。當然,您可能希望增加緩衝區大小。
回收 LOH 的性能數據
可以通過某些方法來回收與 LOH 相關的性能數據。不過,在介紹它們之前,我們先談論一下爲什麼要進行回收。
在開始回收特定區域的性能數據前,希望您已經找到需查看此區域的原因,或您已查看了其他已知區域但未發現任何問題可解釋您需要解決的性能問題。
有關詳細解釋,建議您閱讀我的博客日誌(請參見 go.microsoft.com/fwlink/?LinkId=116467)。在日誌中,我介紹了內存和 CPU 的基礎知識。另外,2006 年 11 月期刊中的“CLR 全面透徹解析”針對內存問題進行了調查,介紹了在託管過程中診斷可能與託管堆相關的性能問題涉及的步驟(請參見 msdn2.microsoft.com/magazine/cc163528)。
.NET CLR 內存性能計數器通常是調查性能問題的第一步。與 LOH 相關的計數器顯示第 2 代回收的數目和大型對象堆的大小。第 2 代回收的數目顯示了自回收過程開始執行第 2 代垃圾回收的次數。計數器會在第 2 代垃圾回收(也稱爲完整垃圾回收)結束時遞增。此計數器顯示最後看到的值。
大型對象堆大小指的是大型對象堆的當前大小(以字節爲單位,包括可用空間)。此計數器將在垃圾回收結束時更新,而不是在每次分配時更新。
查看性能計數器的常用方法是使用性能監視器 (PerfMon.exe)。使用“添加計數器”可爲您關注的過程添加感興趣的計數器,如圖 4 所示。
cc534993.fig04.gif
圖 4 在性能監視器中添加計數器(單擊圖像可查看大圖)
您可以將性能計數器數據保存在性能監視器的日誌文件中,也可以編程方式查詢性能計數器。大部分人在例行測試過程中都採用此方式進行收集。如果發現計數器顯示的值不正常,則可以使用其他方法獲得更多詳細信息以幫助調查。
使用調試器
在開始之前,請注意我此部分提及的調試命令僅適用於 Windows® 調試器。如果需要查看 LOH 上實際存在的對象,您可以使用 CLR 提供的 SoS 調試器擴展,在前面提到的 2006 年 11 月期刊中已對此進行了介紹。圖 5 中顯示了分析 LOH 的輸出示例。
圖 5 中的加粗部分顯示 LOH 堆的大小爲 (16,754,224 + 16,699,288 + 16,284,504 =) 49,738,016 個字節。而在 023e1000 和 033db630 之間,System.Object[] 對象佔用了 8,008,736 個字節;System.Byte[] 對象佔用了 6,663,696 個字節;可用空間佔用了 2,081,792 個字節。
有時,您會看到 LOH 的總大小少於 85,000 個字節。爲什麼會這樣?這是因爲運行時本身實際使用 LOH 分配某些小於大型對象的對象。
由於不會壓縮 LOH,有時人們會懷疑 LOH 是碎片源。事實上,在得出這個結論前,您最好先弄清什麼是碎片。有一種託管堆碎片,由託管對象之間的可用空間量指示(換句話說,在 SoS 中執行 !dumpheap –type Free 時看到的內容);還有虛擬內存 (VM) 地址空間碎片,即標記爲 MEM_FREE 的內存以及在 windbg 中使用各種調試器命令可看到的內容(請參見 go.microsoft.com/fwlink/?LinkId=116470)。圖 6 顯示了虛擬內存空間中的碎片(請注意圖中的加粗文本)。
前面曾提到,託管堆上的碎片用於分配請求。通常看到的更多是由臨時大型對象導致的虛擬內存碎片,需要頻繁進行垃圾回收以便從操作系統獲取新的託管堆段,並將空託管堆段釋放回操作系統。
要驗證 LOH 是否會生成 VM 碎片,可在 VirtualAlloc 和 VirtualFree 上設置一個斷點,查看是誰調用了它們。例如,如果想知道誰曾嘗試從操作系統分配大於 8MB 的 VM 塊,可按以下方式設置斷點:
bp kernel32!virtualalloc "j (dwo(@esp+8)>800000) 'kb';'g'"
如果調用 VirtualAlloc 時分配大小大於 8MB (0x800000),此代碼會中斷調試器並顯示調用堆棧,否則不會中斷調試器。
在 CLR 2.0 中,我們添加了名爲 VM Hoarding 的功能,如果需要經常獲取和釋放段(包括用於大型對象堆和小型對象堆兩者的段),則可以使用此功能。要指定 VM Hoarding 功能,請通過宿主 API 指定名爲 STARTUP_HOARD_GC_VM 的啓動標誌(請參見 go.microsoft.com/fwlink/?LinkId=116471)。指定此標誌後,只會退回這些段上的內存並將其添加到備用列表中,而不會將該空段釋放回操作系統。備用列表上的段以後可用於滿足新的段請求。因此,下次需要新段時,如果可以從此備用列表找到足夠大的段,便可以使用它。
請注意,對於太大的段,該功能不起作用。此功能還可供某些應用程序用以承載其已獲得的段,如一些服務器應用程序,它們會儘可能避免生成 VM 空間碎片以防出現內存不足錯誤。由於它們通常是計算機上的主應用程序,所以可以執行這些操作。強烈建議您在使用此功能時認真測試您的應用程序,以確保內存使用情況比較穩定。
大型對象費用很高。由於 CLR 需要清除一些新分配大型對象的內存,以滿足 CLR 清除所有新分配對象內存的保證,所以分配成本相當高。LOH 將與堆的其餘部分一起回收,所以請仔細分析這會對您的應用程序性能造成什麼影響。如果可以,建議重新使用大型對象以避免託管堆和 VM 空間中生成碎片。
最後,到目前爲止,在回收過程中尚不能壓縮 LOH,但不應依賴於此實現詳情。因此,要確保某些內容未被 GC 移動,請始終將其固定起來。現在,請利用您剛學到的 LOH 知識對堆進行控制。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章