【问题标题】:OutOfMemoryException when a lot of memory is available大量内存可用时出现 OutOfMemoryException
【发布时间】:2016-10-20 16:47:51
【问题描述】:

我们有一个运行在 5 个(服务器)节点(16 个内核,每个 128 GB 内存)上的应用程序,在每台机器上加载近 70 GB 数据。该应用程序是分布式的并为并发客户端提供服务,因此,有很多套接字使用。同样,对于多线程之间的同步,也有一些同步技术正在使用,主要是使用System.Threading.Monitor

现在的问题是,当应用程序正在运行并且数据在这些服务器节点之间以及客户端和服务器之间传输时,即使还有 40+% 的内存可用,一台或两台服务器机器开始接收OutOfMemoryException。我们有一种感觉,这个异常来自非托管代码。虽然,我们没有直接进行任何非托管调用,但我们已经看到 OOM 异常堆栈跟踪中的最后一次调用始终是内部调用非托管代码的框架调用。

以下是几个例子。

Exception of type 'System.OutOfMemoryException' was thrown.
   at System.Threading.Monitor.ObjPulseAll(Object obj)
   ....

Exception of type 'System.OutOfMemoryException' was thrown.
   at System.Threading.Monitor.ObjWait(Boolean exitContext, Int32 millisecondsTimeout, Object obj)
   at System.Threading.Monitor.Wait(Object obj, TimeSpan timeout)
   ....

我们对导致此问题的原因一无所知。我们已经在这些机器上多次诱导 GC,但这似乎也没有帮助。

任何帮助将不胜感激..

编辑:

以下是更多细节;

  • 应用程序正在 x64 进程中运行。
  • Windows Server 2012 R2
  • .NET Framework 4.5
  • 已启用服务器 GC
  • AllowLargeObject 标志已设置。

EDIT2:请注意,这不是内存泄漏。 70 GB 进程大小在此处有效。

【问题讨论】:

  • 您使用的是什么位操作系统?根据操作系统是否为 32/64 位,每个进程都有限制。
  • 如果您尝试超过大多数对象的 2GB 限制,托管代码仍可能引发 OOM 异常
  • 您是如何确定 LOH 的大小的?有多少个LOH?如果多个,每个 LOH 组合或每个单独 LOH 的大小是 10GB?另外,页面文件是启用还是禁用?页面文件有多大?操作系统显示的可用内存是多少?
  • 您是否使用过任务管理器或进程资源管理器来找出正在使用的句柄、GDI 对象和用户对象的数量?

标签: c# .net out-of-memory clr


【解决方案1】:

其他用户提出的一些初步问题很酷,但您是否考虑过懒惰并分析您的应用程序?

我可以想到 Redgate 的 Ants profiler 或 JetBrains 的 dotmemory,链接如下。

http://www.red-gate.com/products/dotnet-development/ants-memory-profiler/

https://www.jetbrains.com/dotmemory/

【讨论】:

  • 对应用程序进行分析有什么懒惰的......这是很多工作:)(恕我直言)。 .无论如何,分析一个加载 70GB 数据的应用程序,可能仍然非常棘手......
  • ANTS 是一个非常好的工具。我过去曾遇到过内存问题,并且借助此工具能够检测到这些问题。
  • 是的,它们应该会有所帮助,但对于 70 GB 的进程则没有。我严重怀疑这些分析工具是否曾经用如此大的数据进行过测试。
【解决方案2】:

即使非托管代码存在内存泄漏,如果您有 40% 的可用内存,您应该能够分配对象。我在想的是这是一个碎片问题而不是内存泄漏。

1- 您尝试分配的数据是大块还是小块?

2- 您是否尝试强制垃圾收集器(通过调用 GC.Collect())?垃圾收集不仅可以释放内存,还可以压缩内存以消除碎片。

【讨论】:

  • 是的,这不是内存泄漏。这很可能是一个碎片问题。数据也是大块的,LOH 的使用量约为 10 GB。我们尽可能地优化了它的使用,尽可能地重用数组和缓冲区。是的,我们通过调用 GC.Collect() 来强制 GC,该参数还收集第 2 代堆和 LOH。
  • 碎片相关的OOM是指您的虚拟空间碎片化,因此VirtualAlloc无法找到空闲的地址范围。在 64 位系统上,虚拟空间是如此巨大,以至于极不可能变得如此碎片化而导致 OOM。而且你不能在不释放内存的情况下压缩虚拟空间。
【解决方案3】:

GC.Collect() 只会在对象未被任何其他对象引用的情况下释放内存。

可能发生泄漏的常见情况是在将事件处理程序设置为 null 之前未断开事件处理程序与对象的连接。

作为避免泄漏的练习,最好在对象上实现IDisposable(即使它用于释放非托管对象),仅从确保所有处理程序断开连接、清除集合的角度来看正确,并且任何其他对象引用都设置为 null。

【讨论】:

    【解决方案4】:

    我建议在this exception occurs 时使用ADPlus 或其他工具获取您的进程的转储。使用此转储,您可以使用WinDbg 调试您的转储文件。以下所有命令均取自博文Investigating ASP.Net Memory Dumps for Idiots (like Me)

    调查内存泄漏

    为了查看内存,我们需要使用以下命令

    !dumpheap
    

    “dumpheap”命令将为您提供对象计数和对象的内存使用情况。 然后您可以调查哪些对象类型占用了您的大部分内存。

    !dumpheap -type System.IO.MemoryStream
    

    “dumpheap -type”命令将列出堆上所有类型为 MemoryStream 的对象。 WinDbg 的好处是您可以调查非托管内存泄漏:Example 1Example2

    【讨论】:

    • 70 GB 转储几乎需要永远在 windbg 中打开 :(
    • 如何在生产 环境中调查?
    • @Kiquenet ADPlus 用于生产环境。在开发中或多或少是不必要的。
    【解决方案5】:

    如果这是一个碎片问题,那么如果不进行某种分析就无法解决它。搜索支持碎片检测的内存分析器,以准确了解此碎片的原因。

    【讨论】:

      【解决方案6】:

      LargeObjectHeapCompactionMode = CompactOnce 的垃圾收集可能有助于修复碎片。

      GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
      GC.Collect();
      

      【讨论】:

        【解决方案7】:

        请注意,当订阅事件处理程序时,事件的发布者会持有对订阅者的引用。这是.NET中内存泄漏的常见原因,在您的情况下,这不会是严重的泄漏,但是如果托管对象保留指向非托管对象的指针或句柄,那么它不会删除此非托管对象,因此会导致内存碎片.

        如果您确定碎片的原因是非托管组件并且您没有遗漏任何东西,并且如果您可以访问非托管组件的代码,您可以重新编译它并使用像hoard这样的内存分配器链接它.但这应该在没有其他事情可做并且经过认真分析之后进行。

        【讨论】:

          【解决方案8】:

          在 .NET 4.5 中,CLR team enhanced large object heap (LOH) allocation。即便如此,他们仍然建议使用对象池来提高大型对象的性能。听起来 LOH 碎片在 4.5 中发生的频率较低,但它仍然可能发生。但从堆栈跟踪来看,它看起来与 LOH 无关。

          Daniel Lane 建议 GC 死锁。我们在生产系统上也看到了这些情况,它们肯定会导致进程大小和内存不足的问题。

          您可以做的一件事是运行Debug Diagnostics Tool,在发生 OutOfMemoryException 时捕获完整转储,然后让工具分析转储以获取崩溃和内存信息。从这份报告中,我看到原生堆和托管堆都发生了一些有趣的事情。例如,我们发现打印机驱动程序在 32 位系统上分配了 1 GB 的非托管堆。更新驱动程序解决了这个问题。当然,那是一个客户端系统,但类似的事情可能会发生在您的服务器上。

          我同意这听起来像是原生模式错误。查看来自.NET 4.5 Reference CodeSystem.Threading.Monitor.WaitObjWaitPulseAllObjPulseAll 的实现表明这些类正在调用本机方法:

             /*========================================================================
              ** Sends a notification to all waiting objects. 
              ========================================================================*/
              [System.Security.SecurityCritical]  // auto-generated
              [ResourceExposure(ResourceScope.None)]
              [MethodImplAttribute(MethodImplOptions.InternalCall)]
              private static extern void ObjPulseAll(Object obj);
          
              [System.Security.SecuritySafeCritical]  // auto-generated
              public static void PulseAll(Object obj)
              {
                  if (obj == null)
                  {
                      throw new ArgumentNullException("obj");
                  }
                  Contract.EndContractBlock();
          
                  ObjPulseAll(obj);
              }
          

          “Mike Dimmick”对 Raymond Chen 关于"PulseEvent is fundamentally flawed" 的文章的评论说:

          Monitor.PulseAll 是 Monitor.ObjPulseAll 的包装器,它是 对 CLR 内部函数 ObjectNative::PulseAll 的内部调用。 这反过来包装了 ObjHeader::PulseAll,它包装了 SyncBlock::PulseAll。这只是位于调用 SetEvent 的循环中,直到 没有更多线程正在等待该对象。

          如果有人可以访问 CLI 的源代码,也许他们可以发布更多关于此函数以及内存错误可能来自何处的信息。

          【讨论】:

          • 70 GB 转储几乎需要永远在 windbg 中打开 :(。但你的回答真的很有帮助。
          【解决方案9】:

          没有看到您的代码的有根据的猜测是,您在完成时遇到 STA 死锁问题,尤其是从您的大量硬件要求来看,它似乎是一个高并发系统。无论如何,如果您尝试强制 GC 死锁是有道理的,如果最终确定是死锁的,那么 GC 将无法完成其工作。希望对您有所帮助。

          Advanced Techniques to Prevent and Detect Deadlocks in .Net Applications

          特别感兴趣的部分是我在下面引用的

          当您的代码在单线程单元 (STA) 线程上执行时,会发生相当于排他锁的情况。只有一个线程可以一次更新 GUI 窗口或在 STA 内的单元线程 COM 组件内运行代码。这样的线程拥有一个消息队列,系统和应用程序的其他部分将要处理的信息放入其中。 GUI 使用此队列获取重绘请求、要处理的设备输入和窗口关闭请求等信息。 COM 代理使用消息队列将跨Apartment 方法调用转换到组件具有关联性的Apartment。在 STA 上运行的所有代码都负责抽取消息队列(使用消息循环查找和处理新消息),否则队列可能会堵塞,从而导致失去响应能力。在 Win32 术语中,这意味着使用 MsgWaitForSingleObject、MsgWaitForMultipleObjects(及其 Ex 对应物)或 CoWaitForMultipleHandles API。非泵送等待,例如 WaitForSingleObject 或 WaitForMultipleObjects(及其 Ex 对应项)不会泵送传入消息。

          也就是说,STA“锁”只能通过pump消息队列来释放。如果应用程序在 GUI 线程上执行其性能特征差异很大的操作,而无需泵送消息(如前面提到的那些),则很容易死锁。编写良好的程序要么将此类长时间运行的工作安排在其他地方进行,要么在每次阻塞时抽取消息以避免此问题。值得庆幸的是,每当您阻塞托管代码(通过调用有争议的 Monitor.Enter、WaitHandle.WaitOne、FileStream.EndRead、Thread.Join 等)时,CLR 都会为您提供帮助,有助于缓解此问题。但是大量代码(甚至 .NET Framework 本身的一部分)最终会阻塞在非托管代码中,在这种情况下,阻塞代码的作者可能会或可能不会添加泵送等待。

          这是一个典型的 STA 引起的死锁示例。在 STA 中运行的线程会生成大量单元线程 COM 组件实例,并隐含地生成它们相应的运行时可调用包装器 (RCW)。当然,这些 RCW 必须在它们变得不可访问时由 CLR 最终确定,否则它们会泄漏。但是 CLR 的终结器线程总是加入进程的多线程单元 (MTA),这意味着它必须使用转换到 STA 的代理才能在 RCW 上调用 Release。如果 STA 没有进行泵送以接收终结器对给定 RCW 调用 Finalize 方法的尝试(可能是因为它选择使用非泵送等待来阻塞),则终结器线程将被卡住。它被阻塞,直到 STA 解除阻塞和泵送。如果 STA 从不抽水,那么终结器线程将永远不会取得任何进展,并且随着时间的推移,所有可终结资源的缓慢、静默积累将发生。这反过来又会导致随后的内存不足崩溃或 ASP.NET 中的进程回收。显然,这两种结果都不令人满意。 Windows 窗体、Windows Presentation Foundation 和 COM 等高级框架隐藏了 STA 的大部分复杂性,但它们仍然可能以不可预知的方式失败,包括死锁。 COM 同步上下文引入了类似但略有不同的挑战。此外,许多此类故障只会在一小部分测试运行中发生,而且通常仅在高压力下发生。

          【讨论】:

          【解决方案10】:

          GC 不考虑非托管堆。如果您在 C# 中创建大量对象,而这些对象仅仅是对更大的非托管内存的包装,那么您的内存正在被吞噬,但 GC 无法基于此做出合理的决定,因为它只看到托管堆。

          您最终会遇到 GC 收集器不认为您内存不足的情况,因为您的第 1 代堆上的大部分内容都是 8 字节引用,实际上它们就像海上的冰山。大部分内存都在下面!

          您可以使用这些 GC 调用:

          System::GC::AddMemoryPressure(sizeOfField);
          System::GC::RemoveMemoryPressure(sizeOfField);
          

          这些方法允许垃圾收集器查看非托管内存(如果您提供正确的数字)

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2010-10-29
            • 2012-08-09
            • 1970-01-01
            • 1970-01-01
            • 2021-01-03
            • 2020-12-13
            • 2012-05-18
            • 1970-01-01
            相关资源
            最近更新 更多