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

Is "Out Of Memory" A Recoverable Error?

提问人:Walter Bright 提问时间:12/2/2008 最后编辑:Walter Bright 更新时间:11/8/2023 访问量:9789

问:

我已经编程了很长时间,我看到的程序,当它们耗尽内存时,会尝试清理并退出,即优雅地失败。我不记得我最后一次看到一个人真正尝试恢复并继续正常运行是什么时候了。

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

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

与语言无关的 异常 内存管理 错误恢复

评论

0赞 KarolDepka 10/31/2010
对不起,如果上面的评论很粗鲁。但是,例如,Java GUI 应用程序可以很好地处理 OutOfMemoryError(我不是 Java 的忠实粉丝,只是注意到我的经验)——例如,terminated 仅来自单个用户操作的请求。
0赞 KarolDepka 10/31/2010
此外,有时让单个线程死亡,但让程序的其余部分运行是件好事 - 这有时会自然发生。
0赞 KarolDepka 10/31/2010
此外,故障有时可以被控制为仅破坏单个事件的处理(例如,在 GUI 事件调度循环/线程中)。
1赞 Alexander Malakhov 4/4/2013
以下是 Walter Bright 对 OOM 不可恢复原因的进一步讨论

答:

1赞 Mike G. 12/2/2008 #1

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

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

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

评论

0赞 Walter Bright 12/2/2008
通过能够将某些分配标记为“弱”,这意味着分配器可以在需要更多内存时释放它们,这难道不是更普遍、更好地处理吗?
0赞 Mike G. 12/18/2008
可能,尽管您可能需要另一个名称,因为通常“弱”用于引用而不是分配,并且以这种方式重载可能会导致混淆。而且,采用您的方法不能像缓存清理(或其他 oom 处理程序)那样具有选择性
39赞 Jon Skeet 12/2/2008 #2

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

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

0赞 Friedrich 12/2/2008 #3

这是一个难题。乍一看,似乎没有更多的记忆意味着“运气不好”,但是,你还必须看到,如果一个人真的坚持,可以摆脱许多与记忆有关的东西。让我们以其他方式损坏的函数 strtok 为例,一方面,它在内存方面没有问题。然后以 Glib 库中的对应g_string_split为例,它在很大程度上依赖于内存的分配,就像基于 glib 或 GObject 的程序中的几乎所有内容一样。可以肯定地说,在更动态的语言中,内存分配比在更不灵活的语言中更常用,尤其是 C。但是,让我们看看替代方案。如果在内存不足时结束程序,即使是精心开发的代码也可能会停止工作。但是,如果您遇到可恢复的错误,则可以采取一些措施。因此,使其可恢复的论点意味着人们可以选择以不同的方式“处理”这种情况(例如,为紧急情况留出内存块,或降级为内存占用较少的程序)。

所以最令人信服的理由是。如果您提供了一种恢复方法,则可以尝试恢复,如果您没有选择,则完全取决于始终获得足够的内存...

问候

5赞 slim 12/2/2008 #4

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

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

我相信你可以想到你自己的例子,在这些情况下,终止程序是一个非常昂贵的选择(生命支持机器、宇宙飞船控制、长时间运行和时间紧迫的财务计算等)——尽管第一道防线当然是确保程序具有可预测的内存使用,并且环境可以提供内存。

评论

1赞 Walter Bright 12/2/2008
如果由于内存不足错误而终止程序是不可想象的,那么您就遇到了一个非常严重的设计问题,可能需要自定义解决方案(例如静态分配所有数据或预先分配数据)。
0赞 slim 12/2/2008
嗯,是的,但有些程度是不可想象的,你可以在恢复策略中投入一定程度的努力。例如,我敢肯定,在开发游戏机时,这是一个非常常见的考虑因素。
0赞 slim 12/2/2008
对于某些应用程序来说,一个可能就足够了的简单策略:void *my_malloc(size_t nbytes) { for(int i=0;i<MALLOC_RETRIES) { void *p = malloc(nbytes); if(null != p) return p; sleep(5);
18赞 Aaron Digulla 12/2/2008 #5

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

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

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

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

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

评论

8赞 Walter Bright 12/2/2008
在具有虚拟内存的系统上,探测可以分配多少内存意味着将分配分页到磁盘的内存,这是磁盘缓冲区的悲观化。
1赞 Prof. Falken 1/13/2010
防御中的数据点:uClibc 有一个 8 字节左右的内部静态缓冲区,用于当没有更多内存要动态分配时,用于文件 I/O。
5赞 n-alexander 12/2/2008 #6

我正在开发一个为 IO 缓存分配内存以提高性能的系统。然后,在检测到 OOM 时,它会收回其中的一些内容,以便业务逻辑可以继续,即使这意味着更少的 IO 缓存和略低的写入性能。

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

OOM 处理的主要问题是:

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

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

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

此外,基础操作系统在 OOM 方面的行为必须可预测。例如,如果启用了内存过量使用,则 Linux 不会。许多支持交换的系统会比向有问题的应用程序报告 OOM 更早死亡。

而且,在某些情况下,不是您的进程造成了这种情况,因此,如果有问题的进程继续泄漏,释放内存无济于事。

正因为如此,通常是大型嵌入式系统采用这种技术,因为它们可以控制操作系统和内存来启用它们,并且有实施它们的纪律/动机。

评论

0赞 Walter Bright 12/2/2008
因此,您认为从 OOM 中恢复是使用自定义内存分配方法而不是标准库方法所做的事情?
0赞 n-alexander 12/4/2008
您也可以使用标准方法执行此操作,前提是您知道要发布的内容,并且无需新内存即可执行此操作。我们使用标准的 Java 内存管理器做到了这一点,并取得了一些成功。
4赞 Dennis C 12/2/2008 #7

只有当您抓住它并正确处理它时,它才能恢复。

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

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

1赞 geocar 12/2/2008 #8

这取决于你所说的内存不足是什么意思。

当大多数系统出现故障时,这是因为您的地址空间已经用完了。malloc()

如果大部分内存被缓存或 mmap 区域占用,则可以通过释放缓存或取消映射来回收部分内存。然而,这确实需要你知道你用内存做什么 - 正如你所注意到的,大多数程序要么没有,要么它没有区别。

如果你对自己使用了(也许是为了防止不可预见的攻击,或者可能是root对你做的),你可以放宽错误处理程序中的限制。我非常频繁地这样做 - 在尽可能提示用户并记录事件之后。setrlimit()

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

3赞 Robert Jacques 12/2/2008 #9

不。 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;
}

这是向垃圾回收器添加对注册/取消注册自收集/压缩对象的支持的一个不错的论据。

10赞 Ben Hinkle 12/3/2008 #10

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

0赞 paercebal 12/12/2008 #11

现在让我感到困惑。

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

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

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

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

我确实相信使用 C++ strong/nothrow guanrantees 可以帮助进程在低可用内存条件下生存,即使它类似于内存交换(即缓慢、有点无响应等),但当然,这只是理论。在尝试模拟之前,我只需要在这个主题上变得更聪明(即创建一个 C++ 程序,使用内存有限的自定义新建/删除分配器,然后尝试在这些压力条件下做一些工作)。

井。。。

8赞 Ifeanyi Echeruo 1/4/2009 #12

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

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

在应用程序的开头或关键块的开头,预先分配该内存量。如果检测到内存不足的情况,请释放防护内存并执行恢复。该策略仍然可能失败,但总的来说物有所值。

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

我不是 100% 确定,但我很确定“代码完成”(任何受人尊敬的软件工程师都需要阅读)涵盖了这一点。

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

评论

0赞 Jason Baker 1/4/2009
这在 Code Complete 中有所提及。如果我没记错的话,这种技术被称为“降落伞”。
0赞 Raedwald 1/5/2012
不幸的是,这在 Java 中无法可靠地完成,因为 JVM 被允许在任何时候抛出 OOM,而不仅仅是在你有一个 .因此,如果您捕获到 OOM,它可能已在某个点被抛出,使您的程序处于不一致的状态。请参阅 stackoverflow.com/questions/8728866/...new
1赞 Michael Borgwardt 1/4/2009 #13

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

在单次过度分配的情况下,应用程序可能能够继续完美运行。当然,如果你有一个逐渐的内存泄漏,你只会再次遇到问题(更有可能是迟早的),但给应用程序一个优雅地关闭的机会仍然是一个好主意,在GUI应用程序的情况下保存未保存的更改,等等。

1赞 Will Hartung 1/4/2009 #14

是的,OOM 是可恢复的。举个极端的例子,Unix 和 Windows 操作系统在大多数时候都能很好地从 OOM 条件下恢复。应用程序失败,但操作系统会幸存下来(假设操作系统首先有足够的内存来正确启动)。

我引用这个例子只是为了表明这是可以做到的。

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

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

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

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

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

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

0赞 Loren Pechtel 1/4/2009 #15

内存不足通常意味着你必须放弃你正在做的任何事情。但是,如果您小心清理,它可能会使程序本身正常运行并能够响应其他请求。最好让程序说“对不起,内存不足”,而不是说“对不起,内存不足,正在关闭”。

0赞 sharptooth 2/3/2009 #16

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

0赞 Zuu 2/22/2009 #17

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

一般来说,几乎所有可重用资源的枯竭都应该是可恢复的。理由是程序的每个部分基本上都是一个子程序。仅仅因为一个子不能在这个时间点完成它,并不意味着程序的整个状态都是垃圾。仅仅因为停车场停满了汽车并不意味着你把你的车扔掉了。要么你等一会儿,让摊位空闲,要么你开车去更远的商店买饼干。

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

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

我的两分钱:-)

0赞 robert.berger 5/19/2009 #18

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

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

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

-1赞 nos 8/7/2009 #19

我有这个:

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

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

评论

0赞 hookenz 8/30/2010
如果另一个进程耗尽了所有内存,并且没有将其返回给操作系统,该怎么办?然后你将永远坐在循环中,对正在发生的事情一无所知。如果可以记录一些东西不是更好吗?
1赞 nos 8/30/2010
确定。以上只是一个例子,你可以记录一些东西,你可以只尝试 20 次而不是无限循环,等等。
0赞 waxwing 11/10/2009 #20

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

无论如何,我都看不出在多线程应用程序中实现这一点。如何知道哪个线程实际导致了内存不足错误?一个线程可以不断分配新内存,并将 gc-root 连接到堆的 99%,但第一个失败的分配发生在另一个线程中。

举个实际的例子:每当我在 Java 应用程序(在 JBoss 服务器上运行)中发生 OutOfMemoryError 时,并不是说一个线程死了,服务器的其余部分继续运行:不,有几个 OOME,杀死了几个线程(其中一些是 JBoss 的内部线程)。我看不出作为一名程序员,我能做些什么来从中恢复过来,甚至看不出 JBoss 能做些什么来从中恢复过来。事实上,我什至不确定您是否可以:VirtualMachineError 的 javadoc 表明,在抛出此类错误后,JVM 可能会“损坏”。但也许问题更针对语言设计。

0赞 Prof. Falken 1/13/2010 #21

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

0赞 Raedwald 1/5/2012 #22

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

在 Java 中,使其成为可恢复错误的一个令人信服的论据是,因为 Java 允许在任何时候发出 OOM 信号,包括在结果可能是程序进入不一致状态的时候。因此,从 OOM 获得可靠的响应是不可能的;如果捕获到 OOM 异常,则不能依赖任何程序状态。请参阅 No-throw VirtualMachineError 保证

2赞 Keith Thompson 8/6/2013 #23

这个问题被标记为“与语言无关”,但如果不考虑语言和/或底层系统,就很难回答。

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

例如,如果调用一个函数来尝试分配一个巨大的数组,则大多数语言不会定义无法分配数组的行为。(在 Ada 中,这至少在原则上会引发一个例外,并且应该可以处理它。Storage_Error

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

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

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

0赞 Yoric 11/19/2019 #24

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

  1. 运行垃圾回收器。我们不会一直运行垃圾收集器,因为它会降低性能和电池电量,因此当您出现内存不足错误时,一些垃圾可能已经积累起来。
  2. 释放内存。例如,删除一些内存缓存。
  3. 终止或推迟非必要的任务。例如,从内存中卸载一些长时间未使用的选项卡。
  4. 记录内容以帮助开发人员解决内存不足错误。
  5. 显示一个半不错的错误消息,让用户知道发生了什么。
  6. ...

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