当大量内存可用时OutOfMemoryException

时间:2014-12-02 10:29:17

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

我们有一个应用程序在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的流程大小在此处有效。

10 个答案:

答案 0 :(得分:6)

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

我可以想到Redgate的蚂蚁剖析器或JetBrains的dotmemory,下面的链接。

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

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

答案 1 :(得分:5)

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

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

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

答案 2 :(得分:3)

GC.Collect()只会释放一个对象未​​被其他任何东西引用的内存。

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

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

答案 3 :(得分:3)

我建议在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

答案 4 :(得分:2)

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

答案 5 :(得分:1)

使用LargeObjectHeapCompactionMode = CompactOnce进行垃圾收集可能有助于修复碎片。

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

答案 6 :(得分:1)

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

如果您确定碎片的原因是非托管组件并且您没有遗漏某些内容,并且您可以访问umnanaged组件的代码,则可以重新编译它并使用像hoard这样的内存分配器来链接它。但是,如果没有别的事情要做,并且经过严肃的剖析,那就应该这样做。

答案 7 :(得分:1)

在.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);
    }

关于Raymond Chen关于"PulseEvent is fundamentally flawed"和Mike Dimmick"表示:

  

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

如果有人可以访问CLI的源代码,也许他们可以发布更多有关此功能以及内存错误可能来自的内容。

答案 8 :(得分:0)

在没有看到你的代码的情况下,一个有根据的猜测是你在最终确定时遇到STA死锁的问题,特别是看到它是一个高级并发系统,根据你的硬件要求来判断。无论如何看,好像你已经尝试强制GC一个死锁是有道理的,如果最终确定死锁,那么GC将无法完成它的工作。希望这对你有所帮助。

Advanced Techniques to Prevent and Detect Deadlocks in .Net Applications

具体而言,我感兴趣的部分如下所述

  

当您的代码在单线程单元(STA)线程上执行时,会发生等效的独占锁定。只有一个线程可以立即更新GUI窗口或在STA内部的公寓线程COM组件中运行代码。这些线程拥有一个消息队列,系统和应用程序的其他部分将信息放入该消息队列中。 GUI使用此队列来获取重绘请求,要处理的设备输入和窗口关闭请求等信息。 COM代理使用消息队列将跨公寓方法调用转换为组件具有亲缘关系的公寓。在STA上运行的所有代码都负责使用消息循环来抽取消息队列 - 查找和处理新消息 - 否则队列可能会被堵塞,从而导致响应能力丢失。在Win32术语中,这意味着使用MsgWaitForSingleObject,MsgWaitForMultipleObjects(及其Ex对应物)或CoWaitForMultipleHandles API。非抽空等待(例如WaitForSingleObject或WaitForMultipleObjects(以及它们的Ex对应物))将不会输入传入的消息。

     

换句话说,STA“锁定”只能通过泵送消息队列来释放。执行操作的应用程序可以轻松实现死锁,这些操作的性能特征在GUI线程上有很大差异,而不会像前面提到的那样抽取消息。编写良好的程序要么将这种长时间运行的工作安排在别处进行,要么每次阻塞时都要为消息提供消息以避免这个问题。值得庆幸的是,无论何时阻止托管代码(通过调用有争议的Monitor.Enter,WaitHandle.WaitOne,FileStream.EndRead,Thread.Join等),CLR都会为您提供帮助,从而有助于缓解此问题。但是大量的代码 - 甚至是.NET Framework本身的一部分 - 最终会阻塞非托管代码,在这种情况下,阻塞代码的作者可能会或可能不会添加抽取等待。

     

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

答案 9 :(得分:0)

GC没有考虑非托管堆。如果你创建了许多只是C#包装器中的大型非托管内存的对象,那么你的内存正在被吞噬,但GC无法根据这个做出合理的决定,因为它只能看到托管堆。

你最终会遇到GC收集器不认为你的内存不足的情况,因为你的gen 1堆上的大多数东西都是8字节的引用,实际上它们就像海上的冰山一样。大部分记忆都在下面!

您可以使用这些GC调用:

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

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