我有一个应用程序,它可以从大量MSMQ队列中读取(目前大约有10000个)。我使用带有UInt32.MaxValue超时的queue.BeginPeek
从队列接收消息。当消息出现在队列中时,我处理它并再次调用queue.BeginPeek
。所以我监听所有队列,但消息处理是在线程池上完成的。
我注意到内存使用量增长缓慢(两周的工作会导致从200 MB增长到800 MB)。在研究了转储文件后,我看到了典型的堆碎片图片,其中有许多空闲对象(其中一些对象的大小约为几兆字节)。孔之间有固定的物体。
这似乎是处理对非托管代码的调用时的常见情况,这些代码创建了固定对象。但我在互联网上没有找到任何解决方案。
那么,.NET中的内存管理是不是太纯粹了,甚至不允许完成如此简单的场景,或者我错过了一些东西?
编辑:我对示例应用程序进行了一些调查。GC在为新对象分配内存时会重用固定对象之间的孔(空闲内存区域,也称为空闲对象)。但在我的生产应用程序中,固定对象是长寿的,它们最终出现在第二代中,中间有洞(因为GC只是改变了分隔几代的边界)。由于我几乎没有正常的长寿物体,我在转储文件的第二代中看到了这个洞。
所以我的应用程序的内存消耗可以增长到10000*(洞的平均大小)。(10000是将来还会增加的队列数量)。我现在不知道如何解决这个问题。唯一的方法是不时地重新启动应用程序。
我只能再一次问,为什么.NET没有单独的固定对象堆?(也许这是个新手问题)。目前,我发现调用使用非托管代码的异步操作可能会导致内存问题。
查看了MSMQ托管包装器的源代码后,您似乎在使用API时偶然发现了一个真正的问题。调用BeginPeek
将创建一个属性集合,然后在传递到非托管API之前对其进行固定。只有当收到消息时,这些属性才会被取消固定,但要继续接收消息,您必须在此时调用BeginPeek
,这会随着时间的推移导致内存碎片。
如果这种碎片是一个真正的问题,我唯一能想到的方法是每隔一个小时左右取消对BeginPeek
的所有调用,强制进行垃圾收集,然后恢复正常的侦听操作。这应该允许垃圾收集器处理碎片。
好吧,如果固定对象是长寿命的问题,那么一个简单的解决方案是在BeginPeek
上使用一个持续时间较短的超时,以防止它们进入下一代。在每次超时和EndPeek
之后,您可以重新发出BeginPeek
。根据Martin引用的属性是在什么时候创建和处理的,您可能需要重新创建队列对象(显然不是队列本身,只是一个封装器)。尽管运气好的话,你不必走那么远。