“内存不足”是否是可恢复的错误?


75

我已经进行了很长时间的编程,并且看到的程序在内存不足时会尝试清理并退出,即正常失败。我不记得上一次看到一个实际尝试恢复并继续正常运行的情况了。

如此多的处理取决于能否成功分配内存,尤其是在垃圾回收语言中,似乎内存不足错误应归类为不可恢复。(不可恢复的错误包括堆栈溢出之类的东西。)

使它成为可恢复的错误的有力论据是什么?


抱歉,如果以上评论不礼貌。但是例如,Java GUI应用程序可以很好地处理OutOfMemoryError(我不是Java迷,只是注意我的经验)-终止的原因是例如仅来自单个用户操作的请求。
KarolDepka 2010年

同样,有时候让单线程死掉,但让程序的其余部分运行是一件好事,有时这是自然发生的。
2010年

同样,有时可以包含故障,以破坏单个事件的处理(例如在GUI事件分配循环/线程中)。
KarolDepka 2010年

这是Walter Bright的进一步讨论,为什么OOM无法恢复
Alexander Malakhov 2013年

Answers:


37

这实际上取决于您要构建的内容。

Web服务器使一个请求/响应对失败,然后继续进行其他请求并非完全不合理。但是,您必须确保单个故障不会对全局状态产生有害影响-这将是棘手的事情。鉴于故障会在大多数托管环境(例如.NET和Java)中引起异常,因此我怀疑如果该异常是通过“用户代码”处理的,则将来的请求是否可以恢复-例如,如果一个请求尝试分配10GB的内存并失败,那不会损害系统的其余部分。但是,如果系统在尝试将请求传递给用户代码时内存不足,则可能会更麻烦。


40
哇... Jon Skeet回答了Walter Bright的问题。我只是在脊椎骨上颤抖(也许是因为这里的温度是华氏62度)。
迈克尔·迈尔斯

17

在库中,您想有效地复制文件。这样做时,通常会发现,使用少量大块进行复制比复制许多较小的块要有效得多(例如,通过复制15个1MB块来复制15MB文件比复制15'000块要快得多) 1K块)。

但是代码可以与任何块大小一起使用。因此,虽然使用1MB的块可能会更快,但是如果您设计用于复制大量文件的系统,则明智的做法是捕获OutOfMemoryError并减小块的大小,直到成功为止。

另一个地方是存储在数据库中的对象的缓存。您希望在缓存中保留尽可能多的对象,但又不想干扰应用程序的其余部分。由于可以重新创建这些对象,因此这是一种节省内存的聪明方法,可以将缓存附加到内存不足处理程序中以删除条目,直到应用程序的其余部分有足够的空间来呼吸。

最后,对于图像处理,您希望将尽可能多的图像加载到内存中。同样,OOM处理程序使您可以实现该功能,而无需事先知道用户或操作系统将为您的代码提供多少内存。

[编辑]请注意,我在这里假设您为应用程序提供了固定的内存量,该数量小于不包括交换空间的总可用内存。如果您可以分配太多的内存,以致必须交换掉一部分内存,那么我的一些评论就不再有意义了。


7
在具有虚拟内存的系统上,探测可分配的内存量意味着您将分配分页到磁盘的内存,这是对磁盘缓冲区的一种悲观表示。
Walter Bright

1
防御中的数据点:当没有更多动态分配的内存时,uClibc具有8字节左右的内部静态缓冲区用于文件I / O。
法尔肯教授2010年

9

使用大型数组进行算术运算时,MATLAB用户始终会耗尽内存。例如,如果变量x适合内存,并且它们运行“ x + 1”,则MATLAB为结果分配空间,然后填充结果。如果分配失败,则MATLAB错误,用户可以尝试其他操作。如果在此用例出现时退出MATLAB,那将是一场灾难。


8

OOM应该是可恢复的,因为关机不是从OOM恢复的唯一策略。

实际上,在应用程序级别上,有一种非常标准的OOM问题解决方案。在应用程序设计中,确定从内存不足的状况中恢复所需的最小安全内存量。(例如,自动保存文档,显示警告对话框,记录关闭数据所需的内存)。

在应用程序开始或关键块开始时,请预先分配该内存量。如果检测到内存不足的情况,请释放保护内存并执行恢复。该策略仍然可以失败,但是总的来说,它可以带来很大的收益。

请注意,该应用程序无需关闭。它可以显示模式对话框,直到OOM条件得到解决。

我不是100%肯定,但是我很确定' Code Complete '(所有受尊敬的软件工程师都需要阅读)涵盖了这一点。

PS:您可以扩展您的应用程序框架以帮助实现此策略,但请不要在库中实施此类策略(未经应用程序同意,好的库不会做出全局决策)


代码完成中提到了这一点。如果我没记错的话,该技术称为“降落伞”。
杰森·贝克

不幸的是,不能可靠地在Java中实现,因为JVM是允许在抛出OOM任何时候,不只是当你拿到一个new。因此,如果您捕获了一个OOM,它可能会抛出到使程序处于不一致状态的位置。见stackoverflow.com/questions/8728866/...
Raedwald

5

我认为,就像许多事情一样,这是一项成本/收益分析。您可以通过编程来尝试从malloc()故障中恢复,尽管这可能很困难(您的处理程序最好不要犯同样的内存短缺问题)。

您已经注意到,最常见的情况是清理并正常失败。在那种情况下,可以确定的是,正常中止的成本要低于开发成本和恢复性能成本的总和。

我敢肯定,您会想到一些自己的例子,其中终止程序是一个非常昂贵的选择(生命支持机,飞船控制,长期运行和对时间要求严格的财务计算等),尽管第一道防线是当然,要确保程序具有可预测的内存使用量,并确保环境可以提供该内存使用量。


1
如果由于内存不足错误而终止程序是不可想象的,那么您手中就有一个非常严重的设计问题,可能需要自定义解决方案(例如静态分配所有数据或预先分配它们)。
Walter Bright

是的,但是有些事情是不可想象的,您可以在恢复策略中付出一些努力。我敢肯定,例如在开发游戏机时,这是一个非常普遍的考虑因素。
苗条的

一种对于某些应用程序可能足够的琐碎策略:void * my_malloc(size_t nbytes){for(int i = 0; i <MALLOC_RETRIES){void * p = malloc(nbytes; if(null!= p)返回p; 睡眠(5); }
苗条的

5

我正在为IO缓存分配内存以提高性能的系统上工作。然后,在检测到OOM时,会将其收回一部分,以便业务逻辑可以继续进行,即使这意味着更少的IO缓存和稍低的写入性能。

我还与嵌入式Java应用程序一起工作,这些Java应用程序试图通过强制垃圾回收来管理OOM,可以选择释放一些非关键对象,例如预取或缓存的数据。

OOM处理的主要问题是:

1)可以在发生的地方重试,或者可以从更高的位置回滚并重试。大多数现代程序都过于依赖该语言来抛出,并且无法真正管理它们的最终位置以及如何重试该操作。如果操作不是设计为保留,通常会丢失操作的上下文

2)能够实际释放一些内存。这意味着一种资源管理器,它知道哪些对象是关键的,哪些不是关键的,并且系统能够在以后以及如果它们后来变得关键时重新请求释放的对象

另一个重要的问题是能够回滚而不触发另一个OOM情况。在高级语言中,这是很难控制的。

而且,底层操作系统必须具有与OOM有关的可预测行为。例如,如果启用了内存过量使用,则Linux不会。许多启用交换功能的系统将比向有问题的应用程序报告OOM更快地死掉。

而且,在某些情况下,情况并非由您的进程造成的,因此,如果有问题的进程继续泄漏,则释放内存将无济于事。

因此,通常是大型嵌入式系统采用此技术,因为它们具有对OS和内存的控制权以启用它们,并具有实施它们的纪律/动机。


因此,您认为从OOM中恢复就像是使用自定义内存分配方法(而不是标准库)那样?
Walter Bright

如果您知道要发布的内容,并且不需要新的内存就可以使用标准方法执行此操作。我们使用标准的Java内存管理器成功完成了此任务。
n-alexander

4

仅当您捕获并正确处理它后,它才能恢复。

例如,在相同情况下,请求尝试分配大量内存。这是可以预见的,您可以很好地处理它。

但是,在许多情况下,在多线程应用程序中,OOE也可能在后台线程上发生(包括由system / 3rd-party库创建的)。几乎不可能进行预测,并且您可能无法恢复所有线程的状态。


3

不能。GC的内存不足错误通常不应在当前线程内恢复。(不过,应该支持可恢复线程(用户或内核)的创建和终止)

关于反例:我目前正在一个D编程语言项目中,该项目使用NVIDIA的CUDA平台进行GPU计算。我没有手动管理GPU内存,而是创建了代理对象来利用D的GC。因此,当GPU返回内存不足错误时,我将运行一次完全收集,并且仅在第二次失败时才会引发异常。但是,这实际上不是内存不足恢复的示例,它更像是GC集成之一。恢复的其他示例(高速缓存,空闲列表,不具有自动收缩功能的堆栈/哈希等)都是具有自己的收集/压缩内存方法的结构,这些方法与GC分开,并且往往不在分配本地功能。因此人们可能会实现以下内容:

T new2(T)( lazy T old_new ) {
    T obj;
    try{
        obj = old_new;
    }catch(OutOfMemoryException oome) {
        foreach(compact; Global_List_Of_Delegates_From_Compatible_Objects)
            compact();
        obj = old_new;
    }
    return obj;
}

通常,这是为增加向垃圾回收器注册/注销自收集/压缩对象的支持的一个不错的理由。


1

在一般情况下,它是不可恢复的。

但是,如果您的系统包括某种形式的动态缓存,则内存不足处理程序通常可以转储缓存中最旧的元素(甚至整个缓存)。

当然,您必须确保“转储”过程不需要新的内存分配:)此外,要恢复失败的特定分配可能很棘手,除非您能够直接在分配器处插入缓存转储代码级别,这样故障就不会传播到调用方。


通过将某些分配标记为“弱”,这是否会更一般和更好地处理,这意味着分配器可以在需要更多内存时释放它们?
Walter Bright

可能,尽管您可能需要另一个名称,因为通常“弱”用于引用而不是分配,并且以这种方式重载可能会导致混乱。并且采用您的方法无法像缓存清理(或其他oom处理程序)那样具有选择性
Mike G.

1

这取决于您用尽内存的含义。

什么时候 malloc()在大多数系统上,故障时,是因为您用完了地址空间。

如果大部分内存是通过缓存或通过mmap的区域占用的,则可以通过释放缓存或取消映射来回收其中的一些内存。但是,这确实需要您知道您正在使用该内存的用途,并且您已经注意到大多数程序没有使用它,或者没有任何区别。

如果您习惯setrlimit()了自己(为防止无法预料的攻击,或者根本上是对您的攻击),则可以放宽错误处理程序中的限制。我经常这样做-在可能的情况下提示用户并记录事件之后。

另一方面,捕获堆栈溢出会更加困难,并且不可移植。如果您采用这种方式,我为ECL写了一个posixish解决方案,并描述了Windows实现。几个月前已将其检入ECL,但是如果您有兴趣,我可以挖掘原始补丁。


1

尤其是在垃圾回收的环境中,如果您在应用程序的较高级别捕获到OutOfMemory错误,则可能引述很多内容超出范围,可以回收这些东西以提供给您内存。

在单个过多分配的情况下,该应用程序可能能够继续正常运行。当然,如果您遇到了逐渐出现的内存泄漏,您将再次遇到问题(很可能早于迟来),但是让应用程序有机会正常关闭,保存未保存的更改仍然是一个好主意。 GUI应用程序等的情况


1

是的,OOM是可恢复的。作为一个极端的例子,Unix和Windows操作系统大多数时候都可以从OOM条件中很好地恢复。应用程序失败,但是OS可以生存(假设首先有足够的内存供OS正确启动)。

我仅引用此示例说明它可以完成。

处理OOM的问题实际上取决于您的程序和环境。

例如,在许多情况下,OOM最可能发生的地方并不是从OOM状态实际恢复的最佳地方。

现在,自定义分配器可能可以作为可处理OOM的代码的中心点。Java分配器将在实际引发OOM异常之前执行完整的GC。

分配器越“了解应用程序”,就更适合作为OOM的中央处理程序和恢复代理。再次使用Java,它的分配器并不特别了解应用程序。

在这里,像Java这样的东西很容易让人感到沮丧。您不能覆盖分配器。因此,尽管您可以在自己的代码中捕获OOM异常,但是没有什么可以说您正在使用的某些库正在正确捕获,甚至正确地抛出了OOM异常。创建一个永远不会被OOM异常破坏的类是微不足道的,因为某些对象被设置为null且“永不发生”,并且它永远不可恢复。

因此,是的,OOM是可恢复的,但它可能很难,尤其是在Java之类的现代环境中,并且它拥有各种质量的第三方库。


1

该问题被标记为“与语言无关”,但是如果不考虑语言和/或底层系统,就很难回答。(我看到了几个矿穴

如果内存分配是隐式的,并且没有机制来检测给定的分配是否成功,那么从内存不足的状态恢复可能很困难或不可能。

例如,如果调用一个试图分配巨大数组的函数,则大多数语言只是在无法分配数组的情况下没有定义行为。(在Ada中Storage_Error,至少在原则上提出了一个例外,应该可以处理该例外。)

另一方面,如果您有一种尝试分配内存并能够报告失败的机制(例如Cmalloc()或C ++ new),那么可以,当然可以从该失败中恢复。至少在malloc()和的情况下new,失败的分配除了报告失败之外不执行其他任何操作(例如,它不会破坏任何内部数据结构)。

尝试恢复是否有意义取决于应用程序。如果应用程序在分配失败后无法成功执行,则它应尽其所能清理并终止。但是,如果分配失败仅意味着不能执行一项特定任务,或者如果仍然可以用更少的内存来更慢地执行该任务,则继续运行是有意义的。

一个具体的例子:假设我正在使用文本编辑器。如果我尝试在需要大量内存的编辑器中执行某些操作,而该操作无法执行,我希望编辑器告诉我它无法执行我要求的操作,并让我继续进行编辑。不保存我的工作而终止将是无法接受的响应。保存我的工作并终止会更好,但是仍然不必要地引起用户敌视。


0

这是一个难题。乍看之下,似乎没有更多的内存意味着“走运”,但是,您还必须看到,如果一个人坚持不懈,就可以摆脱很多与内存有关的东西。让我们以其他方式使用损坏的函数strtok,该函数一方面没有内存问题。然后从Glib库中获取对应的g_string_split,该库在很大程度上取决于内存的分配,因为这几乎是glib或基于GObject的程序中的所有内容。可以肯定地说,在更多动态语言中,与在更不灵活的语言(尤其是C)中相比,内存分配的使用要多得多。但是让我们看看替代方法。如果仅在内存不足的情况下结束程序,即使精心开发的代码也可能会停止工作。但是,如果您有可恢复的错误,则可以采取一些措施。所以论点

因此,最引人注目的原因是。如果提供了一种恢复方法,则可以尝试恢复,如果您没有选择,则全部取决于始终获得足够的内存...

问候


0

现在只是让我感到困惑。

在工作中,我们有大量的应用程序一起工作,并且内存不足。虽然问题可能是使应用程序捆绑包变为64位(因此,可以超出正常Win32 OS的2 Go限制),和/或减少了内存使用量,但此问题是“如何从OOM中恢复过来”不会放弃我的头。

当然,我没有解决方案,但仍在为C ++寻找一种解决方案(主要是因为RAII和异常)。

也许应该正常恢复的进程应该在原子/可回滚的任务中分解其处理(即仅使用提供强/无异常保证的功能/方法),并保留“缓冲区/内存池”以用于恢复。

如果其中一项任务失败,则C ++ bad_alloc将展开堆栈,并通过RAII释放一些堆栈/堆内存。然后,恢复功能将尽可能地抢救(将任务的初始数据保存在磁盘上,以供以后尝试使用),并可能注册任务数据以供以后尝试。

我确实相信使用C ++强/无担保人可以帮助进程在内存不足的情况下生存,即使这类似于内存交换(例如,缓慢,有些无响应等),但当然,这是仅理论。我只需要在尝试模拟这个主题之前变得更聪明(即创建一个C ++程序,并使用一个内存有限的自定义new / delete分配器,然后尝试在那些压力很大的条件下做一些工作)。

好...


0

内存不足通常意味着您必须退出正在执行的操作。但是,如果您对清理很小心,它可能会使程序本身保持运行状态并能够响应其他请求。最好让程序说“对不起,没有足够的内存来做”比说“对不起,内存不足,正在关闭”。


0

内存不足可能是由于空闲内存耗尽或试图分配一个不合理的大块(例如一个演出)引起的。在“耗尽”情况下,内存不足是系统的全局问题,通常会影响其他应用程序和系统服务,并且整个系统可能会变得不稳定,因此明智的做法是忘记并重新启动。在“不合理的大块”情况下,实际上不会出现短缺,并且可以继续进行。问题是您无法自动检测到您所处的情况。因此,使错误不可恢复并针对遇到此错误的每种情况找到解决方法更安全-使程序使用更少的内存或在某些情况下只需修复调用内存分配的代码中的错误。


0

这里已经有很多好的答案。但是我想从另一个角度做出贡献。

通常,几乎所有可重用资源的消耗都应可恢复。原因是程序的每个部分基本上都是一个子程序。仅仅因为一个子程序无法在此时间点完成,并不意味着程序的整个状态都是垃圾。仅仅因为停车场里满是汽车,并不意味着您就把汽车丢了。您可能需要等待一段时间才能获得免费的摊位,或者开车去更远的商店购买Cookie。

在大多数情况下,有另一种方法。使错误恢复是不可恢复的,有效地消除了很多选择,而且我们谁都不想让任何人为我们决定我们可以做什么和不能做什么。

磁盘空间也是如此。这实际上是相同的推理。与您对堆栈溢出的暗示是不可恢复的相反,我会说这是任意限制。没有充分的理由说明您不应该抛出异常(弹出很多帧),然后使用另一种效率较低的方法来完成工作。

我的两分钱:-)


0

如果您真的内存不足,那么您将注定要失败,因为您无法释放任何东西。

如果内存不足,但是垃圾回收器之类的东西可以启动并释放一些内存,那么您还没有死。

另一个问题是碎片化。尽管您可能没有内存不足(碎片),但是您仍然可能无法分配想要拥有的巨大块。


0

我知道您要求提供论据,但我只能反对。

无论如何,我看不到在多线程应用程序中实现这一目标。您如何知道哪个线程实际上造成了内存不足错误?一个线程可以不断分配新的内存,并具有gc根到99%的堆,但是第一个失败的分配发生在另一个线程中。

一个实际的例子:每当我在Java应用程序(在JBoss服务器上运行)中发生OutOfMemoryError时,就不像一个线程死了,服务器的其余部分继续运行:不,有几个OOME,杀死了多个线程(有些其中是JBoss的内部线程)。我没有看到我作为程序员可以从中恢复的工作,甚至看不到JBoss从中恢复的工作。实际上,我什至不确定您是否可以:VirtualMachineErrorjavadoc建议在抛出此类错误后JVM可能“损坏”。但是也许这个问题更针对语言设计。


0

当没有更多的内存可以动态分配时,uClibc具有一个8字节左右的内部静态缓冲区用于文件I / O。


0

使它成为可恢复的错误的有力论据是什么?

在Java中,一个令人信服的说法是使它成为可恢复的错误,因为Java允许在任何时候发出OOM信号,包括在结果可能导致程序进入不一致状态时发出信号。因此,无法从OOM可靠地回收;如果捕获到OOM异常,则不能依赖任何程序状态。请参见 无投掷VirtualMachineError保证


0

我正在研究SpiderMonkey,这是Firefox中使用的JavaScript VM(以及gnome和其他一些软件)。内存不足时,您可能需要执行以下任何操作:

  1. 运行垃圾收集器。我们不会一直运行垃圾收集器,因为它会破坏性能和电池,因此,当您遇到内存不足的错误时,可能已经积累了一些垃圾。
  2. 可用内存。例如,摆脱一些内存缓存。
  3. 杀死或推迟不必要的任务。例如,从内存中卸载一些长时间未使用的标签。
  4. 记录内容以帮助开发人员解决内存不足错误。
  5. 显示错误信息,让用户知道发生了什么。
  6. ...

所以是的,有很多原因可以手动处理内存不足错误!


-1

我有这个:

void *smalloc(size_t size) {
  void *mem = null; 
  for(;;) {
   mem = malloc(size);
   if(mem == NULL) {
    sleep(1);
   } else 
     break;
  }
  return mem;
}

其中已经节省了几次系统。仅仅因为您现在的内存不足,并不意味着系统的其他部分或系统上运行的其他进程有一些内存,它们很快就会退还给您。在尝试这种技巧之前,您最好非常小心,尽管如此,您可以完全控制程序中分配的每个内存。


如果另一个进程正在用尽所有内存而不将其还给操作系统,该怎么办?那么您将永远坐在循环中,没有比这更明智的事情了。如果可以记录某些内容会更好吗?
马特

1
当然。以上仅是示例,您可以记录一些内容,可以尝试20次,而不是无限循环,等等
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.