一:背景
1.講故事
上個月 .NET除錯訓練營 裡的一位老朋友給我發了一個 8G 的dump檔案,說他的程式記憶體洩露了,一時也沒找出來是哪裡的問題,讓我幫忙看下到底是怎麼回事,畢竟有了一些除錯功底也沒分析出來,說明還是有一點複雜的,現實世界中的dump遠比課上說的複雜的多。
還是那句話,找我分析是免費的,沒有某軟高額的工時費,接下來我們上 WinDbg 說話。
二:WinDbg 分析
1. 託管還是非託管洩露
這是我們首先就要做出的抉擇,否則就會南轅北轍,可以使用 !address -summary & !eeheap -gc
來定位一下。
0:000> !address -summary
--- Usage Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
Free 1311 7ffc`e2b37000 ( 127.988 TB) 99.99%
<unknown> 4799 2`4f798000 ( 9.242 GB) 74.19% 0.01%
Heap 3029 0`906fe000 ( 2.257 GB) 18.12% 0.00%
Image 3435 0`2b530000 ( 693.188 MB) 5.43% 0.00%
Stack 226 0`11e00000 ( 286.000 MB) 2.24% 0.00%
Other 90 0`0025c000 ( 2.359 MB) 0.02% 0.00%
TEB 75 0`00096000 ( 600.000 kB) 0.00% 0.00%
PEB 1 0`00001000 ( 4.000 kB) 0.00% 0.00%
--- Type Summary (for busy) ------ RgnCount ----------- Total Size -------- %ofBusy %ofTotal
MEM_PRIVATE 7990 2`e6964000 ( 11.603 GB) 93.14% 0.01%
MEM_IMAGE 3445 0`2b536000 ( 693.211 MB) 5.43% 0.00%
MEM_MAPPED 220 0`0b61f000 ( 182.121 MB) 1.43% 0.00%
--- State Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
MEM_FREE 1311 7ffc`e2b37000 ( 127.988 TB) 99.99%
MEM_COMMIT 8158 1`cf52a000 ( 7.239 GB) 58.11% 0.01%
MEM_RESERVE 3497 1`4df8f000 ( 5.218 GB) 41.89% 0.00%
0:000> !eeheap -gc
Number of GC Heaps: 1
generation 0 starts at 0x0000023ba303e940
generation 1 starts at 0x0000023ba2ebd0d0
generation 2 starts at 0x00000239a80f1000
ephemeral segment allocation context: none
...
Large object heap starts at 0x00000239b80f1000
segment begin allocated size
00000239b80f0000 00000239b80f1000 00000239bfe174a8 0x7d264a8(131228840)
0000023a6f050000 0000023a6f051000 0000023a73780800 0x472f800(74643456)
Total Size: Size: 0xea9878f8 (3935860984) bytes.
------------------------------
GC Heap Size: Size: 0xea9878f8 (3935860984) bytes.
從卦中的 MEM_COMMIT
和 GC Heap Size
這兩個指標來看,主要還是託管記憶體洩露,雖然非託管記憶體也不小,大機率還是託管這邊導致的,有了這些資訊之後,後面就是看下 託管堆
到底都是些什麼物件。
0:000> !dumpheap -stat
Statistics:
MT Count TotalSize Class Name
...
00007ffa2d7a1080 4923008 118152192 System.WeakReference
00007ffa2d725e70 2224022 125834760 System.Object[]
00007ffa2701de10 1044218 133659904 System.Windows.Documents.Paragraph
00007ffa2706b470 1045023 142123128 System.Windows.Documents.Run
00007ffa2706a9b0 2098480 151090560 System.Windows.Documents.TextTreeTextNode
00007ffa2d7267d0 1138661 159949302 System.Char[]
00007ffa2d7259c0 1231039 160962948 System.String
00007ffa29580cd8 214 165608376 MS.Internal.WeakEventTable+EventKey[]
00007ffa2d729750 2116556 169324480 System.Collections.Hashtable
00007ffa2d724478 2117718 209740224 System.Collections.Hashtable+bucket[]
00007ffa2706eb08 4175733 367464504 System.Windows.Documents.TextTreeTextElementNode
00007ffa2700ca48 2088016 384194944 System.Windows.ResourceDictionary
00007ffa2957fdc8 2344569 405666920 System.Windows.EffectiveValueEntry[]
從卦中的 TotalSize
來看並沒有明顯的特徵,但從 Count
看還是有一些蛛絲馬跡的,比如 System.Windows.Documents.TextTreeTextElementNode
物件為什麼高達 417w
? 為什麼 System.Windows.Documents.TextTreeTextNode
有 209w
? 雖然都是 WPF 框架的內部類,但從名字上看貌似和 文字類
控制元件有關係。
2. TextTreeTextElementNode 為什麼沒被回收
有了這些可疑資訊,接下來就需要看下他們為什麼沒有被 GC 收掉?要想找到答案就需要抽幾個 TextTreeTextElementNode
看下使用者根是什麼?可以使用 !dumpheap -mt xxx
找到 address 之後再用 !gcroot
觀察一下。
0:000> !dumpheap -mt 00007ffa2706eb08
Address MT Size
00000239a815f028 00007ffa2706eb08 88
00000239a815f080 00007ffa2706eb08 88
00000239a815f2e8 00007ffa2706eb08 88
00000239a815f340 00007ffa2706eb08 88
00000239a8259f18 00007ffa2706eb08 88
...
0:000> !gcroot 0000023a637180e0
!gcroot 0000023a637180e0
Thread e6c:
000000aebe7fec20 00007ffa296c0298 System.Windows.Threading.Dispatcher.GetMessage(System.Windows.Interop.MSG ByRef, IntPtr, Int32, Int32)
rsi:
-> 00000239a8101688 System.Windows.Threading.Dispatcher
-> 0000023b4630e9a8 System.EventHandler
-> 0000023b4630a990 System.Object[]
-> 00000239a8425648 System.EventHandler
...
結果刷了半天都沒刷完,還把 windbg 給弄死了,看樣子這個引用鏈得有幾十萬哈。。。截圖如下:
那遇到這種情況怎麼辦呢? 為了能夠記錄到所有的引用鏈,大家可以用 windbg 的 .logopen
和 .logclose
命令將所有的輸出記錄到文字中,喝了杯咖啡之後,終於output完了,看檔案有 81w 行,真的心累。
一眼望去大多是 TextTreeTextElementNode 和 TextTreeFixupNode 之間的交叉引用,還得耐點心慢慢往上翻,看看可有什麼蛛絲馬跡,經過仔細排查,發現有一個 RickTextBox
控制元件,截圖如下:
從名字上來看,可能是想用 RichTextBox
記錄日誌,接下來看下 OperatorLogItemRichTextBox
這個類是怎麼寫的。
public sealed class OperatorLogItemRichTextBox : RichTextBox, IOperatorLogger
{
private static readonly DependencyProperty MaximumLogCountProperty = DependencyProperty.Register("MaximumLogCount", typeof(int), typeof(OperatorLogItemRichTextBox), new PropertyMetadata(1024));
private static readonly DependencyProperty VerboseBrushProperty = DependencyProperty.Register("VerboseBrush", typeof(Brush), typeof(OperatorLogItemRichTextBox), new PropertyMetadata(Brushes.Gray));
private static readonly DependencyProperty DebugBrushProperty = DependencyProperty.Register("DebugBrush", typeof(Brush), typeof(OperatorLogItemRichTextBox), new PropertyMetadata(Brushes.Cyan));
...
private static readonly DependencyProperty ExceptionBrushProperty = DependencyProperty.Register("ExceptionBrush", typeof(Brush), typeof(OperatorLogItemRichTextBox), new PropertyMetadata(Brushes.Magenta));
private static readonly DependencyProperty SpecialBrushProperty = DependencyProperty.Register("SpecialBrush", typeof(Brush), typeof(OperatorLogItemRichTextBox), new PropertyMetadata(Brushes.Magenta));
...
}
從原始碼看,朋友在專案中實現了一個自定義的 RichTextBox
控制元件來實現日誌記錄,記憶體洩露問題應該就在這裡。
有車的朋友都知道 4S 店有一個好的傳統,那就是 只換不修
,又簡單又能輕鬆掙錢,所以我給朋友的建議是:把 OperatorLogItemRichTextBox
從專案中給踢掉,排查下還有沒有記憶體洩露的問題。
終於在一週後,收到了朋友的反饋,問題也終於解決了,截圖如下:
三: 總結
其實關於 RichTextBox
的問題我遇到過二次,上次是崩潰相關的,如果要用它來記錄日誌,建議還是用信得過的第三方富文字控制元件,自己實現的話,難免會踩到很多坑。