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

时间:2008-12-02 11:55:03

标签: language-agnostic exception memory memory-management error-recovery

我已经编程很长时间了,我看到的程序在内存耗尽时会尝试清理并退出,即优雅地失败。我不记得上次我看到一个人真正尝试恢复并继续正常运作。

如此多的处理依赖于能够成功分配内存,特别是在垃圾收集语言中,似乎内存不足错误应归类为不可恢复。 (不可恢复的错误包括堆栈溢出等。)

使其成为可恢复错误的令人信服的理由是什么?

24 个答案:

答案 0 :(得分:36)

这实际上取决于你正在建设什么。

网络服务器失败一个请求/响应对然后继续进行进一步的请求并非完全不合理。你必须确定单一故障不会对全球状态产生不利影响 - 但这将是一个棘手的问题。鉴于失败在大多数托管环境(例如.NET和Java)中导致异常,我怀疑如果在“用户代码”中处理异常,则可以在将来的请求中恢复 - 例如如果一个请求尝试分配10GB内存并且失败,那不应该损害系统的其余部分。如果系统在尝试将请求移交给用户代码时内存不足,那么 - 这种事情可能会更糟糕。

答案 1 :(得分:17)

在库中,您希望有效地复制文件。当你这样做时,你会发现使用少量大块复制比复制大量小块更有效(比如,通过复制15个1MB块比复制15'000复制15MB文件更快1K块。

但代码适用于任何块大小。因此,虽然使用1MB块可能会更快,但如果您为复制了大量文件的系统进行设计,那么捕获OutOfMemoryError并减少块大小可能是明智之举。

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

最后,对于图像处理,您希望尽可能多地将图像加载到内存中。同样,OOM处理程序允许您在不事先知道用户或操作系统将为您的代码授予多少内存的情况下实现该功能。

[编辑]请注意,我在这里假设您已经为应用程序提供了固定数量的内存,并且此数量小于不包括交换空间的总可用内存。如果你可以分配这么多的内存,那么它的一部分必须被换掉,我的几条评论就没有意义了。

答案 2 :(得分:9)

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

答案 3 :(得分:8)

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

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

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

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

我不是100%肯定,但我非常确定'Code Complete'(任何受人尊敬的软件工程师必读)都涵盖了这一点。

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

答案 4 :(得分:5)

我认为,与许多事情一样,这是成本/收益分析。你可以尝试从malloc()失败中尝试恢复 - 虽然它可能很难(你的处理程序最好不要因为它要处理的内存短缺而犯错)。

您已经注意到最常见的情况是清理并且优雅地失败。在这种情况下,已经确定优先流产的成本低于恢复中的开发成本和性能成本的组合。

我相信你能想到你自己的例子,终止程序是一个非常昂贵的选择(生命支持机器,飞船控制,长期运行和时间关键的财务计算等) - 尽管第一行防御当然是为了确保程序具有可预测的内存使用量,并且环境可以提供该内存。

答案 5 :(得分:5)

我正在研究一个为IO缓存分配内存以提高性能的系统。然后,在检测到OOM时,需要将其中的一部分恢复,以便业务逻辑可以继续,即使这意味着更少的IO缓存和略低的写入性能。

我还使用了一个嵌入式Java应用程序,它试图通过强制垃圾收集来管理OOM,可选地释放一些非关键对象,如预取或缓存数据。

OOM处理的主要问题是:

1)能够在其发生的地方重新尝试,或者能够从更高点回滚并重新尝试。大多数当代程序过于依赖语言而无法真正管理它们最终的位置以及如何重新尝试操作。如果操作的上下文没有被设计为保留

,通常会丢失操作的上下文

2)能够实际释放一些内存。这意味着一种资源管理器,它知道哪些对象是关键的,哪些不是,并且系统能够在以后成为关键时重新请求已发布的对象

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

此外,底层操作系统必须在OOM方面表现出可预测性。例如,如果启用了内存过量使用,则Linux不会。许多支持交换的系统会比将OOM报告给违规应用程序更快死亡。

而且,当你的进程不是创造了这种情况的时候就是这种情况,所以如果违规进程继续泄漏,释放内存也无济于事。

由于这一切,它通常是采用这种技术的大型嵌入式系统,因为它们可以控制操作系统和内存以实现它们,以及实现它们的纪律/动机。

答案 6 :(得分:4)

只有抓住它并正确处理它才能恢复。

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

但是,在多线程应用程序的许多情况下,OOE也可能发生在后台线程上(包括由系统/第三方库创建)。 预测几乎是不可能的,你可能无法恢复所有线程的状态。

答案 7 :(得分: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;
}

通常,添加对自动收集/压缩对象的注册/取消注册的支持是一个不错的理由。

答案 8 :(得分:1)

特别是在垃圾收集环境中,可能会引用如果您在应用程序的高级别捕获OutOfMemory错误,很多东西已经超出范围并且可以回收以返回内存。

在单次过度分配的情况下,应用程序可能能够完美地继续工作。当然,如果你有一个逐渐的内存泄漏,你只会再次遇到问题(更有可能早一点),但是让应用程序有机会正常下降仍然是一个好主意,保存未保存的更改GUI应用程序等的案例

答案 9 :(得分:1)

这个问题被标记为“语言不可知”,但如果不考虑语言和/或底层系统,很难回答。 (我看到几个toher的避风港

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

例如,如果您调用一个尝试分配大型数组的函数,那么如果无法分配该数组,大多数语言都不会定义该行为。 (在Ada中,至少在原则上会引发Storage_Error异常,应该可以处理它。)

另一方面,如果你有一个试图分配内存并且能够报告失败的机制(比如C的malloc()或C ++的new),那么是的,肯定的有可能从那次失败中恢复过来。至少在malloc()new的情况下,失败的分配除了报告失败之外不会执行任何操作(例如,它不会破坏任何内部数据结构)。

尝试恢复是否有意义取决于应用程序。如果应用程序在分配失败后无法成功,那么它应该做任何清理并终止。但是,如果分配失败仅仅意味着无法执行某个特定任务,或者如果任务仍然可以用较少的内存执行得更慢,那么继续操作是有意义的。

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

答案 10 :(得分:1)

是的,OOM是可以恢复的。作为一个极端的例子,Unix和Windows操作系统在大多数情况下从OOM条件中恢复得非常好。应用程序失败,但操作系统仍然存在(假设有足够的内存供操作系统首先正确启动)。

我只引用这个例子来证明它可以完成。

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

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

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

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

这就像Java这样的东西很容易令人沮丧。您无法覆盖分配器。因此,虽然您可以在自己的代码中捕获OOM异常,但没有任何迹象表明您正在使用的某些库正确捕获,或者甚至正确地处理OOM异常。创建一个永远被OOM异常破坏的类是微不足道的,因为某个对象被设置为null并且“永远不会发生”,并且它永远不可恢复。

所以,是的,OOM是可以恢复的,但它可能非常难,特别是在像Java这样的现代环境中,它有各种各样质量的第三方库。

答案 11 :(得分:1)

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

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

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

答案 12 :(得分:1)

这取决于你的内存不足你的意思。

malloc()在大多数系统上失败时,这是因为你的地址空间不足。

如果大部分内存是通过缓存或mmap'd区域获取的,那么您可以通过释放缓存或解压缩来回收其中一些内存。然而,这确实需要你知道你正在使用那个记忆的内容 - 并且你已经注意到大多数程序没有,或者它没有什么区别。

如果你在自己身上使用setrlimit()(为了防止不可预见的攻击,或许root可能会对你做过),你可以在错误处理程序中放宽限制。我经常这样做 - 在可能的情况下提示用户并记录事件。

另一方面,捕获堆栈溢出有点困难,并且不便携。我为ECL写了一个posixish解决方案,并描述了一个Windows实现,如果你要走这条路。几个月前它被检查了ECL,但如果你有兴趣,我可以挖掘原始的补丁。

答案 13 :(得分:0)

现在让我感到困惑。

在工作中,我们有一组应用程序协同工作,内存不足。虽然问题是要么使应用程序包变为64位(因此,能够超出我们在普通Win32操作系统上的2个Go限制),和/或减少我们对内存的使用,这个问题“如何从OOM中恢复“不会放弃我的头脑。

当然,我没有解决方案,但仍然在为C ++搜索一个(主要是因为RAII和例外)。

也许应该优雅地恢复的进程应该在原子/可回滚的任务中分解其处理(即仅使用提供强/非运行异常保证的函数/方法),并为恢复目的保留“缓冲区/内存池”

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

我确实相信使用C ++强/非保密可以帮助一个过程在低可用内存条件下生存,即使它可能类似于内存交换(即缓慢,有些不相应等),但当然,这只是理论。在尝试模拟这个问题之前,我只需要更加智能化(即创建一个C ++程序,使用内存有限的自定义新/删除分配器,然后尝试在这些压力条件下做一些工作)。

嗯...

答案 14 :(得分:0)

内存不足通常意味着你必须放弃你正在做的事情。但是,如果您对清理很谨慎,它可以让程序本身运行并能够响应其他请求。最好让节目说“对不起,没有足够的记忆去做”,而不是说“对不起,内存不足,关机。”

答案 15 :(得分:0)

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

答案 16 :(得分:0)

这里已有很多好的答案。但我想以另一种观点做出贡献。

一般来说,几乎任何可重用资源的消耗都应该是可以恢复的。原因是程序的每个部分基本上都是一个子程序。仅仅因为一个子在这个时间点无法完成它的结束,并不意味着程序的整个状态都是垃圾。仅仅因为停车场满是汽车并不意味着你把车丢了。您要么等待一个展位免费,要么开车到更远的商店购买您的饼干。

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

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

我的两分钱: - )

答案 17 :(得分:0)

如果你真的没有记忆,那你注定要失败,因为你不能再释放任何东西了。

如果你的内存不足,但垃圾收集器之类的东西可以启动并释放一些内存,你还没死。

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

答案 18 :(得分:0)

我知道你要求提供论据,但我只能看到反对的论点。

我还没有看到在多线程应用程序中实现这一点。你怎么知道哪个线程实际上是造成内存不足错误的原因?一个线程可以不断地分配新内存并将gc-roots置于99%的堆中,但第一个失败的分配发生在另一个线程中。

一个实际的例子:每当我在Java应用程序中运行OutOfMemoryError(在JBoss服务器上运行)时,它就不会像一个线程死掉而服务器的其余部分继续运行:不,有几个OOME,杀了几个线程(其中一些是JBoss的内部线程)。我不认为我作为程序员可以做些什么来从中恢复 - 甚至是JBoss可以从中恢复的东西。事实上,我甚至不确定你是否可以:javadoc for VirtualMachineError表明在抛出这样的错误后JVM可能会被“破坏”。但也许问题更多地针对语言设计。

答案 19 :(得分:0)

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

答案 20 :(得分:0)

  

使其成为可恢复错误的令人信服的理由是什么?

在Java中,使其成为可恢复错误的一个引人注目的论点是因为Java允许在任何时间发出OOM信号,包括结果可能是您的程序进入不一致状态。因此,不可能从OOM进行可靠的重建;如果您捕获OOM异常,则无法依赖任何程序状态。看到 No-throw VirtualMachineError guarantees

答案 21 :(得分:0)

这是一个棘手的问题。乍一看,似乎没有更多的记忆意味着“运气不好”但是,你必须看到,如果一个人真的坚持,可以摆脱许多与记忆相关的东西。让我们采取其他方式破坏函数strtok,一方面对内存的东西没有问题。然后从Glib库中获取对应的g_string_split,这在很大程度上取决于内存的分配,因为几乎所有内容都在glib或基于GObject的程序中。人们可以肯定地说,在更动态的语言中,内存分配更多地用于更不灵活的语言,尤其是C.但是让我们看看替代方案。如果您在内存不足时结束程序,即使仔细开发的代码也可能停止工作。但是如果你有一个可恢复的错误,你可以做些什么。因此,使其可恢复的论点意味着人们可以选择以不同的方式“处理”这种情况(例如,将紧急情况下的内存块放在一边,或者将内存块降级为内存较少的程序)。

所以最引人注目的原因是。如果你提供一种恢复方法,可以尝试恢复,如果你没有选择,所有都取决于总是获得足够的内存......

此致

答案 22 :(得分:0)

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

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

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

答案 23 :(得分:-1)

我有这个:

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

已经保存了几次系统。仅仅因为你现在没有内存,并不意味着系统的其他部分或系统上运行的其他进程有一些内存,他们很快就会回复。在尝试这些技巧之前,你最好非常小心,并且可以控制你在程序中分配的每个内存。