前文介紹效能測試基礎流程,並以 2MB 文本對比冒泡/快速排序。為探究極限,嘗試 700MB 大文件測試(原擬 2GB,但 32 位進程地址空間上限 2GB,扣除核心 2GB,用戶態僅 2GB;除非啟用 /LARGEADDRESSAWARE 讓用戶態擴至 3GB)。
dir /s /b c:\windows > d:\test.txt
FOR /L %i IN (1,1,100) DO type test.txt >> testdata.txt
生成數百 MB 測試文件。
運行排序程序,拋出 OutOfMemoryException。附加調試器,載入 SOS:
.loadby sos clr
!pe
異常堆疊揭示:StreamReader.ReadToEnd() → StringBuilder.ToString() 觸發 OOM。
!ao
Managed OOM occured after GC #312 (Requested to allocate 1649667816 bytes)
Reason: Didn't have enough memory to allocate an LOH segment
Detail: LOH: Failed to reserve memory (1652555776 bytes)
元兇:大物件堆 (LOH) 無法分配 ~1.5GB 連續空間。
Large object heap starts at 0x02911000
segment begin allocated size
02910000 02911000 02913240 0x2240(8768)
Total Size: 0x629424c0 (1653875904 bytes)
!dumpheap -stat 02911000 02911000+02913240
MT Count TotalSize Class Name
571afb78 1119 31332 System.Text.StringBuilder
571b1d88 1120 17933440 System.Char[]
Char[] 佔據 171MB,StringBuilder 實例 1119 個。
!gcroot 03fd1000 (某 Char[] 實例)
Root: 0191d2e0(StringBuilder) -> ... -> 03fd1000(Char[])
追溯至 0191d2e0 (StringBuilder),再 !gcroot 發現其被多個堆疊幀引用:
ESP:17ed58:Root: 0191d2e0(StringBuilder)
0017ed30 5713e61e System.Text.StringBuilder.ToString()
0017ed5c 57121991 System.IO.StreamReader.ReadToEnd()
0017ed70 002f0136 Program.Main()
鐵證: StreamReader.ReadToEnd() 內部使用 StringBuilder 累積整個 700MB 文件內容,最終 ToString() 嘗試分配 1.5GB 連續字串,擊穿 LOH。
ReadToEnd() 適用於小/中型文件。StreamReader.ReadBlock 或 ReadLine 逐塊處理,避免全量駐留記憶體。x64 或 Any CPU (Prefer 32-bit = false),打破 2GB/3GB 限制。| 命令 | 用途 |
|---|---|
!pe | 顯示當前異常詳細資訊 |
!ao | 分析 OOM 發生的堆區域與請求大小 |
!eeheap -gc | 列出各代堆與 LOH 起始/結束地址 |
!dumpheap -stat [start] [end] | 統計指定範圍內物件類型/數量/大小 |
!dumpheap -mt <MethodTable> [range] | 列出特定類型實例地址 |
!gcroot <addr> | 追蹤物件被誰引用(GC 根鏈) |
!dumpstack | 顯示非託管+託管混合調用堆疊 |
掌握 SOS 核心命令,讓記憶體疑難雜症無所遁形。