泉缘泉 发表于 2022-12-6 12:18:22

记一次 .NET 某工控软件 内存泄露分析

一:背景

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
00000239b80f000000000239b80f100000000239bfe174a80x7d264a8(131228840)
0000023a6f0500000000023a6f0510000000023a737808000x472f800(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
...
00007ffa2d7a10804923008    118152192 System.WeakReference
00007ffa2d725e702224022    125834760 System.Object[]
00007ffa2701de101044218    133659904 System.Windows.Documents.Paragraph
00007ffa2706b4701045023    142123128 System.Windows.Documents.Run
00007ffa2706a9b02098480    151090560 System.Windows.Documents.TextTreeTextNode
00007ffa2d7267d01138661    159949302 System.Char[]
00007ffa2d7259c01231039    160962948 System.String
00007ffa29580cd8      214    165608376 MS.Internal.WeakEventTable+EventKey[]
00007ffa2d7297502116556    169324480 System.Collections.Hashtable
00007ffa2d7244782117718    209740224 System.Collections.Hashtable+bucket[]
00007ffa2706eb084175733    367464504 System.Windows.Documents.TextTreeTextElementNode
00007ffa2700ca482088016    384194944 System.Windows.ResourceDictionary
00007ffa2957fdc82344569    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 给弄死了,看样子这个引用链得有几十万哈。。。截图如下:
https://img2023.cnblogs.com/blog/214741/202212/214741-20221206093833015-1576718209.png
那遇到这种情况怎么办呢? 为了能够记录到所有的引用链,大家可以用 windbg 的 .logopen 和 .logclose 命令将所有的输出记录到文本中,喝了杯咖啡之后,终于output完了,看文件有 81w 行,真的心累。
https://img2023.cnblogs.com/blog/214741/202212/214741-20221206093832986-660206487.png
一眼望去大多是 TextTreeTextElementNode 和 TextTreeFixupNode 之间的交叉引用,还得耐点心慢慢往上翻,看看可有什么蛛丝马迹,经过仔细排查,发现有一个 RickTextBox 控件,截图如下:
https://img2023.cnblogs.com/blog/214741/202212/214741-20221206093832991-1634123072.png
从名字上来看,可能是想用 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 从项目中给踢掉,排查下还有没有内存泄露的问题。
终于在一周后,收到了朋友的反馈,问题也终于解决了,截图如下:
https://img2023.cnblogs.com/blog/214741/202212/214741-20221206093833021-328679430.png
三: 总结

其实关于 RichTextBox 的问题我遇到过二次,上次是崩溃相关的,如果要用它来记录日志,建议还是用信得过的第三方富文本控件,自己实现的话,难免会踩到很多坑。
https://images.cnblogs.com/cnblogs_com/huangxincheng/345039/o_210929020104最新消息优惠促销公众号关注二维码.jpg
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
页: [1]
查看完整版本: 记一次 .NET 某工控软件 内存泄露分析