什么时候可以捕获OutOfMemoryException以及如何处理它?

昨天我参加了关于SO的讨论,讨论了OutOfMemoryException以及处理它的优点和缺点( C#try {} catch {} )。

我的专业人员处理它是:

  • OutOfMemoryException被抛出的事实通常并不意味着程序的状态被破坏;
  • 根据文档“下面的Microsoft中间(MSIL)指令抛出OutOfMemoryException:box,newarr,newobj”只是(通常)意味着CLR试图find一个给定大小的内存块,并无法做到这一点; 这并不意味着我们的处置没有单个字节;

但是并不是所有的人都同意这个观点,并且在这个exception之后推测出未知的程序状态,并且由于需要更多的内存而无法做一些有用的事情。

所以我的问题是:什么是严重的原因不处理OutOfMemoryException并立即放弃,当它发生?

编辑:你认为OOME和ExecutionEngineException一样致命吗?

我们都写不同的应用程序。 在WinForms或ASP.Net应用程序中,我可能只是loggingexception,通知用户,尝试保存状态,并关机/重新启动。 但正如伊戈尔在评论中提到的,这很可能是从构build某种forms的图像编辑应用程序,加载第100个20MB RAW图像的过程可能会推动应用程序的边缘。 你真的想用这个简单的话来把所有的工作都弄掉吗? “对不起,此时无法加载更多图片”。

捕获内存exception的另一个常见实例是在后端批处理中。 你可以有一个标准的模型,将多兆字节的文件加载到内存中进行处理,但是有一天,一个多千兆字节的文件被加载。 发生内存不足时,可以将消息logging到用户通知队列,然后转到下一个文件。

是的,有可能是其他的东西可能会同时吹,但如果可能的话,也会被logging和通知。 如果最后GC无法处理更多的内存,那么应用程序将无法继续下去。 (GC运行在一个不受保护的线程中。)

不要忘记我们都开发不同types的应用程序。 除非你使用的是老式的,受限制的机器,否则对于典型的商业应用程序来说,你可能永远不会得到OutOfMemoryException …但是,我们并不是所有人都是商业工具开发人员。

要编辑…

内存不足可能是由非托pipe内存碎片和locking造成的。 这也可能是由于大量的分配请求造成的。 如果我们要竖起一面白旗,在这样简单的问题上划一条线,那么在大型数据处理项目中就没有任何事情可做。 现在将它与一个致命的引擎exception进行比较,那么在运行时在代码中死亡的时候你就没有办法做到这一点。 希望你能够login(但可能不是)为什么你的代码在脸上,所以你可以在将来防止它。 但是,更重要的是,希望您的代码能够以尽可能多的数据安全恢复的方式编写。 也许甚至可以恢复应用程序中最后一个已知的良好状态,并可能跳过有问题的损坏数据,并允许它被手动处理和恢复。

但同时,SQL注入,软件不同步版本,指针操作,缓冲区溢出以及其他许多问题都会导致数据损坏。 避免只是因为你认为可能无法恢复的问题是一个很好的方式给用户错误信息的build设性, 请联系您的系统pipe理员

国际海事组织,因为你不能预测你可以/不能做一个OOM之后(所以你不能可靠地处理错误),或什么也没有/没有发生时,展开到您所在的位置(所以BCL没有可靠地处理错误),现在你的应用程序必须被假定为处于腐败状态。 如果你通过处理这个exception来“修复”你的代码,那么你正在将自己的头埋在沙中。

我可能在这里错了,但对我来说,这个消息说,大难题。 正确的解决方法是找出为什么你已经记忆,并解决(例如,你有泄漏?你可以切换到一个stream式API?)。 即使切换到x64也不是这里的魔力子弹, 数组(因此列表)仍然大小有限; 而增加的参考大小意味着您可以在2GB的对象上限中修复数量较less的引用。

如果你需要处理一些数据,并且很高兴失败:启动第二个进程(一个AppDomain不够好)。 如果爆炸,推倒过程。 问题解决了,你的原始进程/ AppDomain是安全的。

有些评论者指出,有些情况下,OOM可能是尝试分配大量字节(graphics应用程序,分配大型数组等等)的直接结果。 请注意,为此,您可以使用MemoryFailPoint类,该类引发一个InsufficientMemoryException (本身从OutOfMemoryException派生)。 这是可以被安全地捕获的,因为它在实际分配内存之前被提出。 但是,这只能真正减lessOOM的可能性,从来没有完全阻止它。

这一切都取决于情况。

几年前,我正在研究一个实时的3D渲染引擎。 当我们将模型的所有几何graphics加载到内存中时,只是在我们需要显示它们时才加载纹理图像。 这意味着当我们的客户加载巨大的(2GB)模型的时候,我们能够应付。 几何占用小于2GB,但是当所有的纹理添加时,将是> 2GB。 通过捕获当我们尝试加载纹理时引发的内存不足错误,我们可以继续显示模型,但是和平面几何一样。

我们仍然有一个问题,如果几何是> 2GB,但这是一个不同的故事。

显然,如果你的应用程序有一些基本的内存错误,那么你别无select,只能closures – 但尽可能优雅地做到这一点。

build议Christopher Brumme在“框架devise指南”第238页(7.3.7 OutOfMemoryException)中的评论:

在这个范围的另一端,OutOfMemoryException可能是因为隐式自动装箱失败而获得12个字节的结果,或者是为了批量退出而需要的一些代码的失败。 这些情况是灾难性的失败,理想情况下会导致过程终止。 另一方面,OutOfMemoryException可能是线程要求1 GB字节数组的结果。 我们未能进行这种分配尝试的事实对其余过程的一致性和可行性没有影响。

令人遗憾的是,CRL 2.0无法区分这个频谱上的任何一点。 在大多数托pipe进程中,所有的OutOfMemoryException都被认为是等价的,它们都会导致一个托pipe的exception在线程中传播。 但是,您不能依赖于正在执行的退出代码,因为我们可能无法执行某些退出方法,或者可能无法执行退出所需的静态构造函数。

另外,请记住,如果没有足够的内存来实例化其他exception对象,则所有其他exception都可能会折叠到OutOfMemoryException中。 另外,如果可以的话,我们会给你一个独特的OutOfMemoryException和自己的堆栈跟踪。 但是如果我们对记忆力足够紧张,那么你将会与这个过程中的其他人共享一个无趣的全球实例。

我最好的build议是,像任何其他应用程序exception一样对待OutOfMemoryException。 你尽最大的努力来处理它和拉曼一致。 未来,我希望CLR能够更好地将灾难性的OOM与1GB字节数组的情况区分开来。 如果是这样,我们可能会挑起终止灾难性案件的程序,让申请处理风险较小的案件。 通过将所有的OOM案例作为风险较小的案例进行威胁,你就是在为那一天做准备。

Marc Gravell已经提供了一个很好的答案。 看到我如何部分“启发”这个问题,我想补充一点:

exception处理的核心原则之一是不会在exception处理程序中抛出exception。 (注意 – 重新抛出一个域特定和/或包装的exception是好的;我在这里谈论一个意外的exception。)

有各种各样的原因,为什么你需要防止这种情况发生:

  • 充其量,你掩盖了最初的例外; 无法确定程序最初失败的位置。

  • 在某些情况下,运行时可能仅仅是无法在exception处理程序中处理未处理的exception(比如快5倍)。 例如,在ASP.NET中,在pipe道的某些阶段安装一个exception处理程序,并在该处理程序中失败将简单地终止请求 – 或者使工作进程崩溃,我忘记了这一点。

  • 在其他情况下,您可能会打开自己在exception处理程序中的无限循环的可能性。 这可能听起来像一个愚蠢的事情,但我已经看到有人试图通过login来处理exception的情况下,当日志logging失败…他们尝试logging失败。 我们大多数人可能不会故意写这样的代码,但取决于你如何构build你的程序的exception处理,你可能会意外地做到这一点。

那么这与OutOfMemoryException具体有什么关系呢?

OutOfMemoryException不会告诉你任何有关内存分配失败的原因。 你可能会认为这是因为你试图分配一个巨大的缓冲区,但可能不是。 也许在系统上的其他一些stream氓进程已经消耗了所有可用的地址空间,并且没有剩下单个字节。 也许在你自己的程序中的其他线程出错了,进入了一个无限循环,在每次迭代中分配新的内存,并且该线程早在您的当前栈帧出现OutOfMemoryException时就失败了。 重要的是,即使你认为自己真的不知道内存情况有多糟糕

所以现在开始考虑这种情况。 某些操作只是在.NET框架深处的一个非特定的地方失败了,并传播了一个OutOfMemoryException 。 你可以在exception处理程序中执行哪些有意义的工作,而不涉及分配更多内存? 写入日志文件? 这需要记忆。 显示错误讯息? 这需要更多的记忆。 发送一个警报电子邮件? 甚至不要去想它。

如果你试图做这些事情,并且失败了,那么你最终会遇到不确定的行为。 你可能会掩盖内存不足的错误,并从神秘的错误消息中获取神秘的错误消息,这些错误消息是由你写的不应该失败的各种低级组件冒出来的。 从根本上说,你已经违反了你自己的程序的不变性,如果你的程序在低内存条件下运行,这将是一个噩梦。

之前给我的一个参数是,你可能会捕获一个OutOfMemoryException ,然后切换到较小的内存代码,比如一个更小的缓冲区或stream模型。 然而,这个“ outlook处理 ”是一个众所周知的反模式。 如果你知道你即将咀嚼大量的内存,并且不确定系统是否可以处理它,那么检查可用的内存 ,或者更好的方法是重构你的代码,使其不需要这么多的记忆一次。 不要依赖OutOfMemoryException来为你这样做,因为 – 谁知道 – 也许这个分配几乎不会成功,并exception处理程序(可能在一些完全不同的组件) 立即触发一堆内存不足错误。

所以我对这个问题的简单回答是: 从不。

我对这个问题的回答是: 如果你真的非常小心,那么在全局exception处理程序中就可以了。 不在try-catch块中。

捕获此exception的一个实际原因是尝试正常closures,并使用友好的错误消息而不是exception跟踪。

问题在于 – 与其他exception相反 – 当发生exception时(除非内存分配很大,但是您不知道何时捕获该exception),通常内存情况很less。

因此,在处理此exception时,您必须非常小心,不要分配内存。 虽然这听起来很容易,但事实并非如此,实际上很难避免任何内存分配,并做一些有用的事情。 因此,抓住它通常不是一个好主意恕我直言。

问题比.NET大。 如果没有内存可用,几乎所有从五十年代到现在的应用程序都有很大的问题。

使用虚拟地址空间的问题已被打捞出来,但并未解决,因为2GB或4GB的地址空间可能会变得太小。 没有常用的模式来处理内存不足。 可能会有一个内存不足的警告方法,恐慌的方法等,保证仍然有内存可用。

如果你从.NET中收到一个OutOfMemoryException,几乎可能是这样。 2 MB仍然可用,只需100字节,无论如何。 我不想捕捉这个exception(除非closures没有失败对话框)。 我们需要更好的概念。 那么你可能会得到一个MemoryLowException,你可以对各种情况作出反应。

编写代码,不要劫持JVM。 当虚拟机谦虚地告诉你一个内存分配请求失败时,最好的办法就是放弃应用程序的状态以避免破坏应用程序数据。 即使您决定要捕捉OOM,您也应该尝试收集诸如转储日志,堆栈跟踪等诊断信息。请不要尝试启动退出程序,因为您不确定是否有机会执行。

真实世界的比喻:你在飞机上旅行,所有的引擎都失败了。 捕获AllEngineFailureException后,你会做什么? 最好的办法是抓住面具,并准备崩溃。

在OOM的时候,转储!