需要帮助解释 WinDbg 堆总数以调试内存泄漏

Need Help interpreting WinDbg Heap totals for debugging a memory leak

这个问题非常类似于: windbg memory leak investigation - missing heap memory

除了在我的情况下一切都是 x86,而 post 上提供的答案说 Windbg x64 已损坏。

在我的例子中,当我执行“!heap -s”时,我得到:

************************************************************************************************************************
                                              NT HEAP STATS BELOW
************************************************************************************************************************
LFH Key                   : 0x653c3365
Termination on corruption : DISABLED
  Heap     Flags   Reserv  Commit  Virt   Free  List   UCR  Virt  Lock  Fast 
                    (k)     (k)    (k)     (k) length      blocks cont. heap 
-----------------------------------------------------------------------------
00e70000 00000002  761224 757296 761012   6306  1149    51    0  572b1   LFH
00d60000 00001002    1292    128   1080     26     7     2    0      0   LFH
01050000 00001002    1292   1048   1080    271    18     2    0     31   LFH
..snip..

我对 00e70000 处的堆感兴趣。

接下来当我执行命令时:!heap -stat -h 00e70000 -grp s 0n999

我得到该堆中每个块的 509 行输出,列出了它的组大小、匹配该大小的块数,以及该大小的所有块使用的内存总大小。部分输出为:

0:000> !heap -stat -h 00e70000 -grp s 0n999
 heap @ 00e70000
group-by: TOTSIZE max-display: 999
    size     #blocks     total     ( %) (percent of total busy bytes)
    1000 14a - 14a000  (20.24)
    600c 16 - 84108  (8.10)
    168 408 - 5ab40  (5.56)
    154 404 - 55550  (5.23)
    10d8 2a - 2c370  (2.71)
    24 113f - 26cdc  (2.38)
    22750 1 - 22750  (2.11)

然后我将所有这些粘贴到 excel,并将第 3 列转换为十进制并求和,总共只得到 6.5 兆左右。

!address -summary 和 !heap -s 都表明我应该得到总计 808 兆左右的总和。这让我认为要么我不理解 -stat 命令的单位,要么 x64 和 x86(整个 Windbg)都坏了,或者我有更根本的误解。

有人可以帮助我了解情况吗?

谢谢!

编辑:附加信息 使用 DebugDiag,我看到主(默认)堆有 46/54 个段,它们都有一个共同的特征,它们的大小都是 15.81 兆,而且几乎全部分配完毕。这代表了我所缺少的总差异。

看到这个之后,我记得我们的本机代码使用的是 FASTMM4,这可能解释了这两个段以及为什么我没有让 Windbg 列出其中的那些对象。

因此,我计划从本机代码中删除 FASTMM4,然后 运行 再次进行 perf 测试以查看这是否会有所改变。请随时添加任何对此有帮助的内容。

第二次编辑,附加信息: 从我们的代码库中删除 FASTMM 并重新运行测试后,我看到 15.81 MByte 的段仍然存在并且仍在泄漏。这些可以在 DebugDiag 分析中看到:

Segment Information
Base Address    Reserved Size   Committed Size  Uncommitted Size    Number of uncommitted ranges    Largest uncommitted block   Calculated heap fragmentation
0x00e70000  1020 KBytes 1020 KBytes 0 Bytes 1   0 Bytes 0%  0
0x03be0000  1020 KBytes 1020 KBytes 0 Bytes 1   0 Bytes 0%  0
0x04a20000  2 MBytes    2 MBytes    0 Bytes 1   0 Bytes 0%  0
0x051e0000  4 MBytes    4 MBytes    0 Bytes 1   0 Bytes 0%  0
0x0c4b0000  8 MBytes    8 MBytes    0 Bytes 1   0 Bytes 0%  0
0x19dc0000  15.81 MBytes    15.78 MBytes    28 KBytes   1   28 KBytes   -11928.57%  Unavailable
0x1c3b0000  15.81 MBytes    15.81 MBytes    0 Bytes 1   0 Bytes 0%  0
0x2c900000  15.81 MBytes    15.81 MBytes    0 Bytes 1   0 Bytes 0%  0
..snip..

底部显示的标记为 15.81 MB 的新部分扩展了额外的 46 个新段,代表非托管堆上的 727.26 MB 泄漏内存。

搜索 15.81 MBytes 的值使我找到了一些与 Microsoft VC 运行时相关的各种引文:

https://social.msdn.microsoft.com/Forums/vstudio/en-US/e7534d01-57ed-455c-bc0d-edb1b87d0f52/microsoft-vc-runtime-heap-fragmentation?forum=vclanguage https://docs.microsoft.com/en-us/visualstudio/debugger/crt-debug-heap-details?view=vs-2019 Debugdiag shows "Microsoft VC Runtime Heap" using over 1gb

使用 Windbg,我可以显示有关分配的分配信息,如下所示:

    61f130c8: 08008 . 10008 [101] - busy (10000) Internal 
    61f230d0: 10008 . 10008 [101] - busy (10000) Internal 
    61f330d8: 10008 . 10008 [101] - busy (10000) Internal 
    61f430e0: 10008 . 08008 [101] - busy (8000) Internal 
    61f4b0e8: 08008 . 10008 [101] - busy (10000) Internal 
    61f5b0f0: 10008 . 10008 [101] - busy (10000) Internal

但是,由于它们被标记为 "Internal",因此它们不参与 "stack back traces"(gflags 选项 -ust)来确定为分配它们而执行的实际代码。

任何人都可以告诉我有关此泄漏的任何其他信息吗?它确实最终会导致我们的应用程序崩溃。我需要任何可以指导我确定我们如何影响它以减少或消除这种泄漏的方法。

我post将此作为答案,因为最终有几种方法确实引导我们找到了代码中非托管泄漏的根源。我在这里要说的一些内容只是假设,因为我没有在 Microsoft 文档中找到任何内容来验证它。

在最初的 post 中,我展示了 DebugDiag 分析的一部分,该部分描述了进程默认堆中 15.81 段数量不断增加的情况。我开始相信这只是 Windows 允许堆(在可能有许多堆的系统中)增长的方式,而不对在重负载情况下哪个堆需要增长做出任何假设。它们似乎是用一个 1mb 的段创建的,然后是另一个,然后是 2mb 的段,然后是 4mb、8mb 和 16mb。此后,它们只会根据需要增长 16mb(即 15.81)。

当本机堆泄漏时,段会像那样被一遍又一遍地添加。

在此处的问题甚至开始之前,我们使用在 92 小时扩展负载测试的各个点生成的转储文件进行了托管内存分析。我们同时使用了 Visual Studio 和 Windbg 的 SOS 命令,没有发现 "managed" 增长。唯一的问题是非托管代码泄漏,如原始 post.

所示

那时我们在流程上使用 "Gflags +ust" 来获取堆栈回溯跟踪。这给了我们完全有效的信息,但数据不足。它显示了大量泄漏块,并声称它们是由 SecureString.ctor 分配的。在托管堆上没有看到任何 SecureStrings(存活或死亡),我们选择忽略它当时告诉我们的内容。

然后我们采用了一种技术含量较低的方法来定位泄漏代码。我们将在单独的扩展负载测试中测试每个 API 调用,并从 DebugDiag 进行转储分析,直到我们看到它正在泄漏或接受它没有。

一旦我们发现 API 正在泄漏,我们将服务器修改为基本上 "gut-out" 它正在执行的代码的大量跟踪,并重复 debugdiag 分析直到我们的 "gutted-code" 没有更长的时间显示泄漏。

那时我们开始返回它的部分,反复进行测量,并在服务器上使用两种方法之一来 1) 通过注释掉代码路径来消除它,或者 2) 通过放置循环来加剧它1=1 到 1000(或适当放大泄漏的值)

一旦代码路径被证明存在泄漏,我们就会深入其范围并重复该过程作为一种二进制搜索。最终,这将我们带到了 3 行,其中第一行(并非巧合)分配了一个 "SecureString".

这被传递给一个解码它的方法,其中大约包含以下代码:

//Convert to IntPtr using marshal
IntPtr tmp = Marshal.SecureStringToBSTR(SecureString_Param1);
//convert to string using marshal
string plain = Marshal.PtrToStringAuto(tmp);
//Return the now plain string
return plain;

泄漏的内存是非托管 BSTR,实际上,它最初是由 SecureString.ctor 分配的。此代码在单独的测试应用程序中进行了测试以进行验证。

请随时对此添加任何评论 post。