垃圾回收(二)【Windows 系統上的大型物件堆】
https://docs.microsoft.com/zh-cn/dotnet/standard/garbage-collection/large-object-heap
.NET
垃圾回收器 (GC) 將物件分為小型和大型物件。 如果是大型物件,它的某些特性將比物件較小時顯得更為重要。 例如,壓縮大型物件(也就是在記憶體中將其複製到堆上的其他地方)的費用相當高。 因此,.NET 垃圾回收器將大型物件放置在大型物件堆 (LOH) 上。 在本主題中,我們將深度探討大型物件堆。 我們將討論符合什麼條件的物件才能稱之為大型物件,如何回收這些大型物件,以及大型物件具備哪些效能意義。
本主題僅討論 .NET Framework 中的大型物件堆和 Windows 系統上執行的 .NET Core。 不包括在其他平臺上的 .NET 實現上執行的 LOH。
物件如何在大型物件堆上結束以及 GC 如何處理它們
如果物件大於或等於 85,000 位元組,將被視為大型物件。 此數字根據效能優化確定。 物件分配請求為 85,000 位元組或更大時,執行時會將其分配到大型物件堆。
若要了解其意義,可檢視 .NET GC 的部分相關基礎知識。
.NET 垃圾回收器是分代回收器。 它包含三代:第 0 代、第 1 代和第 2 代。 包含 3 代的原因是,在優化良好的應用中,大部分物件都在第 0 代就清除了。 例如,在伺服器應用中,與每個請求相關的分配應在請求完成後清除。 仍存在的分配請求將轉到第 1 代,並在那裡進行清除。 從本質上講,第 1 代是新物件區域與生存期較長的物件區域之間的緩衝區。
小型物件始終在第 0 代中進行分配,或者根據它們的生存期,可能會提升為第 1 代或第 2 代。 大型物件始終在第 2 代中進行分配。
大型物件屬於第 2 代,因為只有在第 2 代回收期間才能回收它們。 回收一代時,同時也會回收它前面的所有代。 例如,執行第 1 代 GC 時,將同時回收第 1 代和第 0 代。 執行第 2 代 GC 時,將回收整個堆。 因此,第 2 代 GC 還可稱為“完整 GC”。 本文引用第 2 代 GC 而不是完整 GC,但這兩個術語是可以互換的。
代可提供 GC 堆的邏輯檢視。 實際上,物件存在於託管堆段中。 託管堆段是 GC 通過呼叫 VirtualAlloc 功能代表託管程式碼在作業系統上保留的記憶體塊。 載入 CLR 時,GC 分配兩個初始堆段:一個用於小型物件(小型物件堆或 SOH),一個用於大型物件(大型物件堆)。
然後,通過將託管物件置於這些託管堆段上來滿足分配請求。 如果該物件小於 85,000 位元組,則將它置於 SOH
的段上,否則,將它置於 LOH
段。 隨著分配到各段上的物件越來越多,會以較小塊的形式提交這些段。
對於 SOH
,GC
未處理的物件將提升為下一代。 第 0 代回收未處理的物件現在視為第 1 代物件,以此類推。 但是,最後一代回收未處理的物件仍會被視為最後一代中的物件。 也就是說,第 2 代垃圾回收未處理的物件仍是第 2 代物件;LOH 未處理的物件仍是 LOH
物件(由第 2 代回收)。
使用者程式碼只能在第 0 代(小型物件)或 LOH(大型物件)中分配。 只有 GC 可以在第 1 代(通過提升第 0 代回收未處理的物件)和第 2 代(通過提升第 1 代和第 2 代回收未處理的物件)中“分配”物件。
觸發垃圾回收後,GC 將尋找存在的物件並將它們壓縮。 但是由於壓縮費用很高,GC 會掃過 LOH,列出沒有被清除的物件列表以供以後重新使用,從而滿足大型物件的分配請求。 相鄰的被清除物件將組成一個自由物件。
.NET Core 和 .NET Framework(從 .NET Framework 4.5.1 開始)包括 System.Runtime.GCSettings.LargeObjectHeapCompactionMode
屬性,該屬性可讓使用者指定在下一完整阻止 GC
期間壓縮 LOH
。 並且在以後,.NET
可能會自動決定壓縮 LOH
。 這就意味著,如果分配了大型物件並希望確保它們不被移動,則應將其固定起來。
圖 1 說明了一種情況,在第一次第 0 代 GC 後 GC 形成了第 1 代,其中 Obj1
和 Obj3
被清除;在第一次第 1 代 GC
後形成了第 2 代,其中 Obj2
和 Obj5
被清除。 請注意此圖和下圖僅用於說明,它們只包含能更好展示堆上的情況的極少幾個物件。 實際上,GC
中通常包含更多的物件。
圖 1:第 0 代和第 1 代 GC。
圖 2 顯示了第 2 代 GC 發現 Obj1
和 Obj2
被清除後,GC 在記憶體中形成了相鄰的可用空間,由 Obj1
和 Obj2
佔用,然後用於滿足 Obj4
的分配要求。 從最後一個物件 Obj3
到此段末尾的空間仍可用於滿足分配請求。
圖 2:第 2 代 GC 之後
如果沒有足夠的可用空間來容納大型物件分配請求,GC
首先嚐試從作業系統獲取更多段。 如果失敗了,它將觸發第 2 代 GC
,試圖釋放部分空間。
在第 1 代或第 2 代 GC 期間,垃圾回收器會通過呼叫 VirtualFree 功能將不包含活動物件的段釋放回作業系統。 將退回最後一個活動物件到段末尾的空間(第 0 代/第 1 代存在的短暫段上的空間除外,垃圾回收器會在該段上會儲存部分提交內容,因為應用程式將在其中立即分配)。 而且,儘管已重置可用空間,但仍會提交它們,這意味著作業系統無需將其中的資料重新寫入磁碟。
由於 LOH 僅在第 2 代 GC 期間進行回收,所以 LOH 段僅在此類 GC
期間可用。 圖 3 說明了一種情況,在此情況下,垃圾回收器將某段(段 2)釋放回作業系統並且退回剩餘段上更多的空間。 如果需要使用該段末尾的已退回空間來滿足大型物件分配請求,它會再次提交該記憶體。 (有關提交/退回的解釋說明,請參閱 VirtualAlloc 的文件)。
圖 3:第 2 代 GC 後的 LOH
何時收集大型物件?
通常情況下,出現以下三種情形中的任一情況,都會執行 GC
:
-
分配超出第 0 代或大型物件閾值。
閾值是某代的屬性。 垃圾回收器在其中分配物件時,會為代設定閾值。 超出閾值後,會在該代上觸發 GC。 因此,分配小型或大型物件時,需要分別使用第 0 代和 LOH 的閾值。 當垃圾回收器分配到第 1 代和第 2 代中時,將使用它們的閾值。 執行此程式時,會動態調整這些閾值。
這是典型情況,大部分
GC
執行都因為託管堆上的分配。 -
呼叫
System.GC.Collect
方法。如果呼叫無引數
System.GC.Collect
方法,或另一個過載作為引數傳遞到System.GC.MaxGeneration
,將會一起收集 LOH 和剩餘的託管堆。 -
系統處於記憶體不足的狀況。
垃圾回收器收到來自作業系統 的高記憶體通知時,會發生以上情況。 如果垃圾回收器認為執行第 2 代 GC 會有效率,它將觸發第 2 代。
LOH 效能意義
大型物件堆上的分配通過以下幾種方式影響效能。
-
分配成本。
CLR 確保清除了它提供的每個新物件的記憶體。 這意味著大型物件的分配成本完全由清理的記憶體(除非觸發了 GC)決定。 如果需要 2 輪才能清除一個位元組,即需要 170,000 輪才能清除最小的大型物件。 清除 2GHz 計算機上 16MB 物件的記憶體大約需要 16ms。 這些成本相當大。
-
回收成本。
因為 LOH 和第 2 代一起回收,如果超出了它們之中任何一個的閾值,則觸發第 2 代回收。 如果由於 LOH 觸發第 2 代回收,第 2 代沒有必要在 GC 後變得更小。 如果第 2 代上資料不多,則影響較小。 但是,如果第 2 代很大,則觸發多次第 2 代 GC 可能會產生效能問題。 如果很多大型物件都在非常短暫的基礎上進行分配,並且擁有大型 SOH,則可能會花費太多時間來執行 GC。 除此之外,如果連續分配並且釋放真正的大型物件,那麼分配成本可能會增加。
-
具有引用型別的陣列元素。
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;
} ;
不要將左側節點的資料引用為 left.d
,而是將其引用為 binary_tr[left_index].d
。 而垃圾回收器無需檢視左側節點和右側節點的任何引用。
在這三種因素中,前兩個通常比第三個更重要。 因此,建議分配重複使用的大型物件池,而不是分配臨時大型物件。
收集 LOH 的效能資料
收集特定區域的效能資料之前,應完成以下操作:
-
找到應檢視此區域的證據。
-
排查你知道的其他區域,確保未發現可解釋上述效能問題的內容。
參閱部落格嘗試找出解決方案之前先了解問題獲取記憶體和 CPU 的基礎知識的詳細資訊。
可使用以下工具來收集 LOH 效能資料:
.NET CLR 記憶體效能計數器
這些效能計數器通常是調查效能問題的第一步(但是推薦使用 ETW 事件)。 通過新增所需計數器配置效能監視器,如圖 4 所示。 與 LOH 相關的是:
-
第 2 代回收次數
顯示自程式開始起第 2 代 GC 發生的次數。 此計數器在第 2 代回收結束時遞增(也稱為完整垃圾回收)。 此計數器顯示上次觀測的值。
-
大型物件堆大小
以位元組顯示當前大小,包括 LOH 的可用空間。 此計數器在垃圾回收結束時更新,不在每次分配時更新。
檢視效能計數器的常用方法是使用效能監視器 (perfmon.exe)。 使用“新增計數器”可為關注的程式新增感興趣的計數器。 可將效能計數器資料儲存在日誌檔案中,如圖 4 所示。
圖 4:第 2 代 GC 後的 LOH
也可以程式設計方式查詢效能計數器。 大部分人在例行測試過程中都採用此方式進行收集。 如果發現計數器顯示的值不正常,則可以使用其他方法獲得更多詳細資訊以幫助調查。
建議使用 ETW 事件代替效能計數,因為 ETW 提供更豐富的資訊。
ETW
垃圾回收器提供豐富的 ETW 事件集,幫助瞭解堆的工作內容和工作原理。 以下部落格文章演示瞭如何使用 ETW 收集和了解 GC 事件:
若要標識由臨時 LOH 分配造成的過多第 2 代 GC 次數,請檢視 GC 的“觸發原因”列。 有關僅分配臨時大型物件的簡單測試,可使用以下 PerfView 命令列收集 ETW 事件的資訊:
perfview /GCCollectOnly /AcceptEULA /nogui collect
結果類似於以下類容:
圖 5:使用 PerfView 顯示的 ETW 事件
如下所示,所有 GC 都是第 2 代 GC,並且都由 AllocLarge 觸發,這表示分配大型物件會觸發此 GC。 我們知道這些分配是臨時的,因為“LOH 未清理率 %”列顯示為 1%。
可以收集顯示分配這些大寫物件的人員的其他 ETW 事件。 以下命令列:
perfview /GCOnly /AcceptEULA /nogui collect
收集 AllocationTick
事件,大約每 10 萬次分配就會觸發該事件。 換句話說,每次分配大型物件都會觸發事件。 然後可檢視某個 GC 堆分配檢視,該檢視顯示分配大型物件的呼叫堆疊:
圖 6:GC 堆分配檢視
如圖所示,這是從 Main
方法分配大型物件的簡單測試。
偵錯程式
如果只有記憶體轉儲,則需要檢視 LOH 上實際有哪些物件,你可使用 .NET 提供的 SoS 偵錯程式擴充套件來檢視。
此部分提到的除錯命令適用於 Windows 偵錯程式。
以下內容顯示了分析 LOH 的示例輸出:
0:003> .loadby sos mscorwks
0:003> !eeheap -gc
Number of GC Heaps: 1
generation 0 starts at 0x013e35ec
sdgeneration 1 starts at 0x013e1b6c
generation 2 starts at 0x013e1000
ephemeral segment allocation context: none
segment begin allocated size
0018f2d0 790d5588 790f4b38 0x0001f5b0(128432)
013e0000 013e1000 013e35f8 0x000025f8(9720)
Large object heap starts at 0x023e1000
segment begin allocated size
023e0000 023e1000 033db630 0x00ffa630(16754224)
033e0000 033e1000 043cdf98 0x00fecf98(16699288)
043e0000 043e1000 05368b58 0x00f87b58(16284504)
Total Size 0x2f90cc8(49876168)
------------------------------
GC Heap Size 0x2f90cc8(49876168)
0:003> !dumpheap -stat 023e1000 033db630
total 133 objects
Statistics:
MT Count TotalSize Class Name
001521d0 66 2081792 Free
7912273c 63 6663696 System.Byte[]
7912254c 4 8008736 System.Object[]
Total 133 objects
LOH 堆大小為 (16,754,224 + 16,699,288 + 16,284,504) = 49,738,016 位元組。 在地址 023e1000 和地址 033db630 之間,8,008,736 位元組由 System.Object
物件的陣列佔用,6,663,696 位元組由 System.Byte
物件的陣列佔用,2,081,792
位元組由可用空間佔用。
有時,偵錯程式顯示 LOH
的總大小少於 85,000 個位元組。 這是由於執行時本身使用 LOH 分配某些小於大型物件的物件引起的。
因為不會壓縮 LOH
,有時會懷疑 LOH
是碎片源。 碎片表示:
-
託管堆的碎片由託管物件之間的可用空間量來表示。 在 SoS 中,
!dumpheap –type Free
命令顯示託管物件之間的可用空間量。 -
虛擬記憶體 (
VM
) 地址空間的碎片是標識為MEM_FREE
的記憶體。 可在 windbg 中使用各種偵錯程式命令來獲取碎片。
以下示例顯示 VM
空間中的碎片:
0:000> !address
00000000 : 00000000 - 00010000
Type 00000000
Protect 00000001 PAGE_NOACCESS
State 00010000 MEM_FREE
Usage RegionUsageFree
00010000 : 00010000 - 00002000
Type 00020000 MEM_PRIVATE
Protect 00000004 PAGE_READWRITE
State 00001000 MEM_COMMIT
Usage RegionUsageEnvironmentBlock
00012000 : 00012000 - 0000e000
Type 00000000
Protect 00000001 PAGE_NOACCESS
State 00010000 MEM_FREE
Usage RegionUsageFree
… [omitted]
-------------------- Usage SUMMARY --------------------------
TotSize ( KB) Pct(Tots) Pct(Busy) Usage
701000 ( 7172) : 00.34% 20.69% : RegionUsageIsVAD
7de15000 ( 2062420) : 98.35% 00.00% : RegionUsageFree
1452000 ( 20808) : 00.99% 60.02% : RegionUsageImage
300000 ( 3072) : 00.15% 08.86% : RegionUsageStack
3000 ( 12) : 00.00% 00.03% : RegionUsageTeb
381000 ( 3588) : 00.17% 10.35% : RegionUsageHeap
0 ( 0) : 00.00% 00.00% : RegionUsagePageHeap
1000 ( 4) : 00.00% 00.01% : RegionUsagePeb
1000 ( 4) : 00.00% 00.01% : RegionUsageProcessParametrs
2000 ( 8) : 00.00% 00.02% : RegionUsageEnvironmentBlock
Tot: 7fff0000 (2097088 KB) Busy: 021db000 (34668 KB)
-------------------- Type SUMMARY --------------------------
TotSize ( KB) Pct(Tots) Usage
7de15000 ( 2062420) : 98.35% : <free>
1452000 ( 20808) : 00.99% : MEM_IMAGE
69f000 ( 6780) : 00.32% : MEM_MAPPED
6ea000 ( 7080) : 00.34% : MEM_PRIVATE
-------------------- State SUMMARY --------------------------
TotSize ( KB) Pct(Tots) Usage
1a58000 ( 26976) : 01.29% : MEM_COMMIT
7de15000 ( 2062420) : 98.35% : MEM_FREE
783000 ( 7692) : 00.37% : MEM_RESERVE
Largest free region: Base 01432000 - Size 707ee000 (1843128 KB)
通常看到的更多是由臨時大型物件導致的 VM
碎片,這些物件要求垃圾回收器頻繁從作業系統獲取新的託管堆段,並將空託管堆段釋放回作業系統。
要驗證 LOH 是否會生成 VM 碎片,可在 VirtualAlloc 和 VirtualFree 上設定一個斷點,檢視是誰呼叫了它們。 例如,如果想知道誰曾嘗試從作業系統分配大於 8MBB 的虛擬記憶體塊,可按以下方式設定斷點:
bp kernel32!virtualalloc "j (dwo(@esp+8)>800000) 'kb';'g'"
只有在分配大小大於 8MB (0x800000) 的情況下呼叫 VirtualAlloc 時,此命令才會進入偵錯程式並顯示呼叫堆疊。
CLR 2.0 增加了稱為“VM 囤積”的功能,用於頻繁獲取和釋放段(包括在大型和小型物件堆上)的情況。 若要指定 VM 囤積,可通過託管 API 指定稱為 STARTUP_HOARD_GC_VM
的啟動標記。 CLR 退回這些段上的記憶體並將其新增到備用列表中,而不會將該空段釋放回作業系統。 (請注意 CLR 不會針對太大型的段執行此操作。)CLR 稍後將使用這些段來滿足新段請求。 下一次應用需要新段時,CLR 將使用此備用列表中的某個足夠大的段。
VM
囤積還可用於想要儲存已獲取的段的應用程式(例如屬於系統上執行的主要應用的部分伺服器應用),以避免記憶體不足的異常。
強烈建議你在使用此功能時認真測試應用程式,以確保應用程式的記憶體使用情況比較穩定。
相關文章
- 淺析堆與垃圾回收
- 託管堆和垃圾回收(GC)GC
- 探索JVM的垃圾回收(堆記憶體)JVM記憶體
- 垃圾回收_上
- jvm(4)---垃圾回收(哪些物件可以被回收)JVM物件
- 【JVM之記憶體與垃圾回收篇】堆JVM記憶體
- windows10系統怎麼垃圾清理_windows10系統垃圾清理的方法Windows
- ☕[JVM技術指南](3)垃圾回收子系統(Garbage Collection System)之垃圾回收器JVM
- ☕[JVM技術指南](1)垃圾回收子系統(Garbage Collection System)之回收標記和物件引用的介紹JVM物件
- 垃圾回收(一)【垃圾回收的基礎】
- 剖析垃圾回收機制(上)
- Axure高保真web端後臺管理系統/垃圾回收分類系統/垃圾回收高保真原型設計 /垃圾分類後臺管理系統/垃圾回收分類平臺//垃圾回收分類智慧管理系統/訂單管理/財務管理/系統管理/庫存管理/裝置管理Web原型
- 垃圾回收(三)【垃圾回收通知】
- ☕[JVM技術指南](2)垃圾回收子系統(Garbage Collection System)之常見的垃圾回收演算法JVM演算法
- windows系統垃圾清理指令碼Windows指令碼
- Python中的物件引用、可變性和垃圾回收Python物件
- Java教程分享:JVM垃圾回收機制之物件回收演算法JavaJVM物件演算法
- 垃圾回收
- Java虛擬機器-GC垃圾回收演算法-判定一個物件是否是可回收的物件Java虛擬機GC演算法物件
- 物件回收判定與垃圾回收演算法-JVM學習筆記(1)物件演算法JVM筆記
- JVM學習(二)——GC垃圾回收機制JVMGC
- [Inside HotSpot] Serial垃圾回收器 (二) Minor GCIDEHotSpotGC
- JVM 垃圾回收演算法和垃圾回收器JVM演算法
- Kubernetes 中的垃圾回收
- JavaScript 中的垃圾回收JavaScript
- JVM 中的垃圾回收JVM
- 聊聊Dotnet的垃圾回收
- JVM垃圾回收JVM
- 垃圾回收_下
- javascript垃圾回收JavaScript
- [JVM]垃圾回收JVM
- golang垃圾回收Golang
- Python:垃圾回收Python
- 智慧垃圾分類回收系統解決方案(經典案例)
- PHP的垃圾回收機制-回收週期PHP
- 關於JVM的垃圾回收JVM
- JS的垃圾回收機制JS
- jvm的垃圾回收機制JVM