经过调查,问题恰好是由于 pinned buffers 导致的堆碎片。我将解释如何调查以及固定缓冲区是什么。
我用过的所有分析器都同意说大部分堆是免费的。现在我需要看看碎片。例如,我可以使用 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层。当托管代码需要将缓冲区的地址发送给外部代码时,它需要“固定”缓冲区以使地址保持有效:GC 无法移动它。
这些缓冲区虽然只是内存的一小部分,但无法进行压缩,从而导致大量内存“泄漏”,即使它不完全是泄漏,更多的是碎片问题。这可能发生在 LOH 或世代堆上。现在的问题是:是什么导致这些固定对象永远存在:找到导致碎片的泄漏的根本原因。
你可以在这里阅读类似的问题:
注意:根本原因是在第三方库AerospikeClient中使用 .NET 异步套接字 API,该 API以固定发送给它的缓冲区而闻名。虽然 AerospikeClient 正确使用了缓冲池,但在重新创建客户端时重新创建了缓冲池。由于我们每小时重新创建他们的客户端,而不是永远创建一个,缓冲池被重新创建,导致越来越多的固定缓冲区,进而导致无限碎片。尚不清楚的是为什么旧缓冲区在传输结束时或至少在其客户端被释放时永远不会取消固定。