.NET进程的内存转储中存在大量未解释的内存

8
我无法解释C#进程使用的大部分内存。总内存为10GB,但所有可达和不可达对象总共只有2.5GB。我想知道这7.5GB可能是什么?
我正在寻找最有可能的解释或一种查找此内存的方法。
以下是具体情况。该进程是.NET 4.5.1。它从互联网下载页面并使用机器学习进行处理。根据VMMap显示,内存几乎完全在托管堆中。这似乎排除了非托管内存泄漏。 enter image description here 该进程已运行数天,内存逐渐增长。某个时刻,内存为11GB。我停止了进程中的所有运行,并多次运行垃圾回收,包括大对象堆压缩(间隔一分钟):
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect();

内存降至10 GB。然后我创建了转储:

procdump -ma psid

预期的转储为10 GB。

我使用.NET memory profiler(版本5.6)打开了该转储。 转储显示总共有2.2 GB可达对象和0.3 GB不可达对象。什么可以解释剩余的7.5 GB?

我一直在思考可能的解释:

  • LOH没有真正完全压缩
  • 某些内存用于超出分析器显示的对象

2
这有点相当于“内存泄漏了,请修复。”。你的服务是做什么的?它会调用任何非托管的内容吗?它使用COM吗? - fstam
4
GC后不应该有那么多无法访问的对象。死锁的finalizer线程会导致内存爆炸,请使用WinDbg查看它在做什么。 - Hans Passant
谢谢 Hans。这个无法访问的内存确实让我感到可疑。我会看一下的。 - Benoit Sanchez
1
听起来像是未经管理的内存使用。JetBrains分析器可以显示出来。您也可以使用免费的VMMap.exe来测试这个理论。 - usr
谢谢usr。我使用了VMMap进行检查。我的10GB中有95%在托管堆中。这似乎排除了非托管内存的使用。 - Benoit Sanchez
1
这可能不适用于您确切的情况,但对于长尾(通过搜索找到问题的其他人等)-请注意,在高核心数机器上使用服务器GC也可能具有一些类似的症状:https://blogs.msdn.microsoft.com/maoni/2018/11/16/running-with-server-gc-in-a-small-container-scenario-part-0/ - Marc Gravell
1个回答

12

经过调查,问题出在{{由于固定缓冲区导致的堆碎片}}。我会解释如何进行调查以及什么是固定缓冲区。

我使用的所有分析工具都认为大部分堆都是空闲的。现在我需要查看碎片情况。例如,我可以使用WinDbg来完成:

!dumpheap -stat

然后我看了一下“大于碎片化块”的部分。WinDbg显示对象位于自由块之间,无法进行压缩。然后我查看了这些对象的持有者以及它们是否被固定,例如在地址0000000bfaf93b80处的对象:

!gcroot 0000000bfaf93b80

它显示引用图:

00000004082945e0 (async pinned handle)
-> 0000000535b3a3e0 System.Threading.OverlappedData
-> 00000006f5266d38 System.Threading.IOCompletionCallback
-> 0000000b35402220 System.Net.Sockets.SocketAsyncEventArgs
-> 0000000bf578c850 System.Net.Sockets.Socket
-> 0000000bf578c900 System.Net.SocketAddress
-> 0000000bfaf93b80 System.Byte[]

00000004082e2148 (pinned handle)
-> 0000000bfaf93b80 System.Byte[]

最后两行告诉你对象被固定了。
固定的对象是缓冲区,因为它们的地址与非托管代码共享而无法移动。在这里,你可以猜测它是系统TCP层。当托管代码需要将缓冲区的地址发送给外部代码时,它需要“固定”缓冲区,以使地址保持有效:垃圾回收器无法将其移动。
这些缓冲区虽然只占内存的一小部分,但会导致内存“泄漏”,即使不完全是泄漏,也是一种碎片化问题,从而使压缩变得不可能。这可能发生在LOH上或者在代际堆上。现在的问题是:是什么导致这些固定的对象永远存在:找到导致碎片化的泄漏的根本原因。
你可以在这里阅读类似的问题: 注意:根本原因在于第三方库AerospikeClient使用的是.NET异步Socket API,该API以{固定发送到它的缓冲区}而闻名。尽管AerospikeClient正确使用了缓冲池,但当重新创建客户端时,缓冲池也会重新创建。由于我们每小时重新创建他们的客户端,而不是永久创建一个客户端,缓冲池被重新创建,导致固定的缓冲区数量增加,进而导致无限碎片化。仍然不清楚的是,为什么旧缓冲区在传输结束时或至少在客户端被处理时没有取消固定。

谢谢您!我们有一个非常类似的案例 - 一个ASP.NET应用程序通过.NET内置的“WebClient”从网络下载一些东西,并进行一些非常轻量级的处理。过了一段时间,该进程会占用3-4 GB的内存。这篇文章确实帮助了我的调查。 - Alex from Jitbit

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接