【问题标题】:.NET Heap fragmentation when using MSMQ async IO使用 MSMQ 异步 IO 时的 .NET 堆碎片
【发布时间】:2012-09-22 08:23:05
【问题描述】:

我有一个从大量 MSMQ 队列(目前大约 10000 个)读取的应用程序。 我使用queue.BeginPeek 和 UInt32.MaxValue 超时从队列接收消息。当消息出现在队列中时,我对其进行处理并再次调用queue.BeginPeek。所以我听所有的队列,但是消息处理是在线程池上完成的。

我注意到内存使用量增长缓慢(两周的工作导致从 200 MB 增长到 800 MB)。在调查了转储文件后,我看到了带有许多空闲对象的典型堆碎片图片(其中一些有大约几兆字节的大小)。并且孔之间有被钉住的物体。

在处理创建固定对象的非托管代码调用时,这似乎很常见。但是我在互联网上没有找到任何解决方案。

那么 .NET 中的内存管理是否如此纯粹,以至于它甚至无法完成如此简单的场景,或者我错过了什么?

编辑:我对示例应用程序进行了一些调查。在为新对象分配内存时,固定对象之间的空洞(空闲内存区域,所谓的空闲对象)被 GC 重用。 但是在我的生产应用程序中,固定对象是长期存在的,它们最终出现在第二代,它们之间有孔(因为 GC 只是移动了分隔代的边界)。由于我几乎没有正常的长寿命对象,因此我在转储文件的第二代中看到了这个漏洞。

所以我的应用程序的内存消耗可以增长到 10000*(孔的平均大小)。 (10000 是将来还可以增加的队列数)。我目前不知道如何解决这个问题。唯一的方法是不时重启应用程序。

我只能再次问,为什么 .NET 没有用于固定对象的单独堆? (也许这是新手问题)。目前我发现调用使用非托管代码的异步操作可能会导致内存问题。

【问题讨论】:

  • 是的,当您有 10,000 次 BeginPeek() 调用时,会发生很多固定。不要期待奇迹。增长并没有多大意义,只有当它最终无法稳定并导致应用程序因 OOM 崩溃时,您才会遇到问题。
  • 我编辑了我的帖子,我得到了线性增长(两周内从 200MB 增长到 800MB)。我不确定我什么时候会得到 OOM,如果这会发生,但这仍然是巨大的内存量。我只能重复一遍——我认为这种情况很典型。我是内存管理工具的新手,但为什么他们不能为固定对象实现单独的堆?另一种选择是使用 OverlappedData 结构创建缓存,但由于 System.Messaging 不是开源的,因此很难实现这种行为。使用当前的 GC 实现,固定对象只能在简单的场景中使用
  • 您对 BeginPeek 有多少并发调用。你总是打电话给 EndPeek,对吧?在我看来,你不应该留下任何东西来让系统静默(没有未完成的呼叫)。
  • 因为我总是需要监听队列,所以我在处理完消息后再次调用 BeginPeek。所以我总是有 10000 个并发 BeginPeek。
  • 固定对象不会放在单独的堆上,因为它们一开始就没有固定。它们仅在调用需要在调用期间访问内存的非托管函数时被固定。在您的情况下,这些非托管调用是长期存在的,这意味着您有许多固定对象。

标签: .net msmq memory-fragmentation


【解决方案1】:

查看 MSMQ 托管包装器的源代码后,您似乎偶然发现了使用 API 的真正问题。调用 BeginPeek 将创建一组属性,然后在传递给非托管 API 之前将其固定。只有在收到消息时才会取消固定这些属性,但要继续接收消息,此时您必须调用 BeginPeek,这会导致内存随着时间的推移而碎片化。

如果这个碎片是一个真正的问题,我能想出的唯一方法是每隔一小时左右,你应该取消所有对BeginPeek 的调用,强制进行垃圾收集,然后恢复正常的监听操作。这应该允许垃圾收集器处理碎片。

【讨论】:

  • 这很有趣。在 EndPeek 之后,内存将被释放并能够被回收,对吗?问题是它已经移动到用于长寿命物体的区域,因此它不会被尽快清理,对吧?偶尔强制垃圾收集不会清理那些未固定的区域吗?
  • 来源有点复杂,但如果我没有误读它,那么当从队列接收到消息时,属性将被取消固定(您等待无限超时)。这些属性当时并没有被释放,但只要它们被固定,它们就不能被垃圾收集并且不移动。垃圾收集器通过在每个 GC 上移动托管对象来压缩堆,但在你的情况下,使用 X*10,000 个固定对象,我猜压缩很难。
  • 您的建议应该如我所见。我也想过这样的解决方案,但是同步 Peek 比重新启动服务要复杂得多(因为我没有 GUI 应用程序,这没什么大不了的)。如果没有其他好的建议,我会将其标记为答案。谢谢。
【解决方案2】:

好吧,如果固定对象长期存在问题,那么一个简单的解决方法是在BeginPeek 上使用更短的超时时间,以防止它们进入下一代。在每次超时和EndPeek 之后,您可以重新发出BeginPeek。根据 Martin 所指的属性的创建和处置时间,您可能必须重新创建队列对象(显然不是队列本身,只是经过组合的包装器)。虽然运气好的话,你不必走那么远。

【讨论】:

  • 这没有帮助。此外,这种解决方案会导致更快的堆碎片(因为这个观察,我将超时更改为无限)。由于我无法同步 BeginPeek 调用,因此在重新发布 BeginPeek 之间可能会(并且确实如此)进行垃圾收集,因此其中一些将进入第二代,并且不会压缩堆,并且此固定对象将位于顶部第二代,所以内存使用只会增加。每 10 秒,我们将至少增加 GC 启动时处于第 0 代的固定对象大小的内存使用量。
猜你喜欢
  • 2011-07-26
  • 2013-11-27
  • 2012-09-09
  • 2011-08-07
  • 1970-01-01
  • 2014-07-14
  • 1970-01-01
  • 1970-01-01
  • 2010-10-29
相关资源
最近更新 更多