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

我已经编程很长时间了,我看到的程序,当它们用完内存时,会尝试清理和退出,也就是说,优雅地失败。我已经不记得上一次看到有人试图恢复并继续正常工作是什么时候了。

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

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

9298 次浏览

一般情况下,是无法恢复的。

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

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

这真的取决于你在建造什么。

一个 web 服务器失败一个请求/响应对,然后继续处理更多请求,这并非完全不合理。然而,你必须确保单个失败不会对全球状况产生不利影响——这将是棘手的一点。假设在大多数托管环境中失败会导致异常(例如。NET 和 Java)我怀疑,如果这个异常是在“用户代码”中处理的,那么对于将来的请求,这个异常是可以恢复的——例如,如果一个请求试图分配10GB 的内存,但是失败了,那么这个异常应该不会损害系统的其他部分。但是,如果系统在试图将请求传递给用户代码时耗尽了内存,那么这种情况可能会更糟糕。

这个问题很难回答。乍一看,似乎没有更多的记忆意味着“不走运”,但是,你也必须看到,一个人可以摆脱许多记忆相关的东西,如果一个人真的坚持。让我们从另一个角度来看待函数 strtok,它一方面没有内存方面的问题。然后从 Glib 库中取出相应的 g _ string _ split,它在很大程度上依赖于内存分配,就像基于 Glib 或 GObject 的程序中的几乎所有内存一样。可以肯定地说,在更动态的语言中,内存分配的使用要比在更不灵活的语言中多得多,尤其是 C 语言。但让我们看看其他选择。如果你只是结束程序,如果你用完了内存,甚至仔细开发的代码可能停止工作。但是如果您有一个可恢复的错误,您可以对它做一些事情。因此,使其可恢复的参数意味着人们可以选择以不同的方式“处理”这种情况(例如,为紧急情况放置一个内存块,或者降级到内存扩展较少的程序)。

所以最有说服力的理由是。如果你提供一种恢复的方法,一个可以尝试恢复,如果你没有选择一切都取决于总是得到足够的内存..。

问候

I think that like many things, it's a cost/benefit analysis. You 可以 program in attempted recovery from a malloc() failure - although it may be difficult (your handler had better not fall foul of the same memory shortage it's meant to deal with).

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

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

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

但是代码可以处理任何块大小。因此,虽然使用1MB 的块可能会更快,但如果您设计的系统中有很多文件被复制,那么捕获 OutOfMemory 错误并减小块大小可能是明智的,直到成功。

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

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

[编辑]注意,我在这里工作的假设是,你给应用程序一个固定的内存量,这个量比不包括交换空间的可用内存总量要小。如果你可以分配如此多的内存,以至于它的一部分必须被交换掉,我的一些评论就不再有意义了。

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

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

面向对象管理的主要问题包括:

1)能够在它发生的地方重试,或者能够从更高的位置回滚并重试。大多数当代的程序过于依赖于语言,而不能真正管理它们最终到达哪里,以及如何重试操作。通常操作的上下文将会丢失,如果它没有被设计来保存的话

2)能够真正释放一些记忆。这意味着一种资源管理器,它知道哪些对象是关键的,哪些不是,系统能够在以后对象变得关键时重新请求释放的对象

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

此外,底层操作系统必须在面向对象方面具有可预测的行为。例如,如果启用了内存过量提交,Linux 就不会这样做。许多启用了交换的系统会比向违规应用程序报告 OOM 更快死亡。

而且,在这种情况下,并不是您的进程创造了这种情况,因此如果违规进程继续泄漏,释放内存并没有帮助。

由于所有这些原因,通常是大型嵌入式系统采用这种技术,因为它们可以控制操作系统和内存来启用它们,并且有纪律/动机来实现它们。

只有当您捕获并正确处理它时,它才是可恢复的。

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

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

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

malloc()在大多数系统上失败时,那是因为您已经用完了地址空间。

如果大部分内存是通过缓存获取的,或者通过 mmap 的区域获取的,那么您可以通过释放缓存或取消 mmap 来回收一部分内存。然而,这实际上需要您知道您使用该内存的目的是什么——正如您已经注意到的,要么大多数程序不知道,要么它不起作用。

如果您对自己使用 setrlimit()(可能是为了防止不可预见的攻击,也可能是 root 对您进行了攻击) ,那么您可以放松错误处理程序中的限制。我经常这样做——如果可能的话,在提示用户并记录事件之后。

另一方面,捕获堆栈溢出有点困难,而且不可移植。我为 ECL编写了一个 posxish 解决方案,并描述了一个 Windows 实现,如果您要走这条路线的话。几个月前进入了 ECL 系统,但如果你感兴趣,我可以找到原始补丁。

来自 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;
}

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

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

我现在很困惑。

在工作中,我们有很多应用程序在一起工作,内存不足。尽管问题在于要么让捆绑变成64位(这样就可以超出我们在普通 Win32操作系统上的2 Go 限制) ,要么减少内存使用,但“如何从 OOM 中恢复”这个问题仍然困扰着我。

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

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

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

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

好吧..。

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

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

在应用程序开始时或在关键块开始时,预先分配该量的内存。如果检测到内存不足,请释放保护内存并执行恢复。这种策略仍然可能失败,但总体而言是物有所值的。

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

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

附言。您可以扩展您的应用程序框架来帮助实现这个策略,但是请不要在库中实现这样的策略(好的库不会在没有应用程序同意的情况下做出全局决策)

特别是在垃圾收集环境中,如果您在应用程序的高级别上捕获 OutOfMemory 错误,很多内容都已经超出了作用域,可以回收以返回内存。

在单个过度分配的情况下,应用程序可以继续完美地工作。当然,如果你有一个渐进的内存泄漏,你只会再次遇到这个问题(更有可能比以后更早) ,但它仍然是一个好主意,给应用程序一个机会,优雅地下降,保存未保存的变化,在一个 GUI 应用程序的情况下,等等。

是的,OOM 是可恢复的。作为一个极端的例子,Unix 和 Windows 操作系统在大多数情况下都能很好地从 OOM 条件下恢复。应用程序失败,但操作系统存活(假设有足够的内存让操作系统在第一时间正常启动)。

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

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

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

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

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

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

因此,是的,OOM 是可恢复的,但是它可能非常困难,特别是在 Java 这样的现代环境中,而且它有大量各种质量的第三方库。

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

内存不足可能是由于空闲内存消耗或试图分配一个不合理的大块(比如1G)造成的。在“消耗”的情况下,内存短缺对系统来说是全局性的,通常会影响到其他应用程序和系统服务,整个系统可能会变得不稳定,所以最好忘记并重新启动。在“不合理的大块”情况下,实际上并没有出现短缺,继续下去是安全的。问题是你不能自动发现你在哪个案子里。因此,更安全的做法是让错误不可恢复,并为遇到的每个错误找到解决办法——让程序使用更少的内存,或者在某些情况下仅仅修复调用内存分配的代码中的错误。

这里已经有很多很好的答案,但我想从另一个角度提供一些建议。

几乎所有可重用资源的消耗通常都是可恢复的。其理由是,程序的每一部分基本上都是一个子程序。仅仅因为一个子程序不能在这个特定的时间点完成到它的结束,并不意味着程序的整个状态都是垃圾。停车场到处都是车,并不意味着你要把你的车扔进垃圾桶。要么你等一会儿,等一个摊位空出来,要么你开车到更远的商店去买你的饼干。

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

磁盘空间也是如此。理由是一样的。与您暗示的堆栈溢出是不可恢复的相反,我认为它是任意的限制。没有充分的理由说明您不能抛出异常(弹出大量框架) ,然后使用另一种效率较低的方法来完成工作。

我的建议是: -)

如果你真的失去了记忆,你就注定要失败,因为你再也无法释放任何东西了。

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

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

我有这个:

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

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

我知道你想要争论,但我只能看到反对的理由。

我认为在多线程应用程序中无论如何都不可能实现这一点。如何知道哪个线程实际上要对内存不足错误负责?一个线程可以不断地分配新内存,并且99% 的堆都有 gc 根,但是第一个失败的分配发生在另一个线程中。

一个实际的例子: 当我在 Java 应用程序(运行在 JBoss 服务器上)中发生 OutOfMemory 错误时,并不是一个线程死亡,而服务器的其余部分继续运行: 不,有几个 OOME,杀死了几个线程(其中一些是 JBoss 的内部线程)。我不知道我作为一个程序员能做些什么来从中恢复——甚至不知道 JBoss 能做些什么来从中恢复。事实上,我甚至不确定您是否可以: 虚拟机错误表明,在抛出这样的错误之后,JVM 可能会“中断”。但也许这个问题更多的是针对语言设计。

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

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

在 Java 中,使 没有成为可恢复错误的一个令人信服的理由是,Java 允许在 任何时发出 OOM 信号,包括在程序进入不一致状态的时候。因此,从 OOM 进行可靠的恢复是不可能的; 如果捕获了 OOM 异常,则无法在任何程序状态下进行 真的恢复。你看 无抛出虚拟机错误保证

这个问题被标记为“语言不可知论”,但是如果不考虑语言和/或底层系统,就很难回答。(我看到她的一些哈恩

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

例如,如果您调用一个试图分配一个巨大数组的函数,如果无法分配该数组,大多数语言都不会定义该行为。(在 Ada 中,这引发了一个 Storage_Error异常,至少在原则上是这样,应该可以处理这个问题。)

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

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

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

我正在开发用于 Firefox (以及 gnome 和其他一些应用程序)的 JavaScript 虚拟机 SpiderMonkey。当你失去记忆的时候,你可能想做以下任何一件事:

  1. 运行垃圾收集器。我们不会一直运行垃圾收集器,因为它会影响性能和电池,所以当您超出内存错误时,一些垃圾可能已经积累起来了。
  2. 释放内存。例如,去掉一些内存中的缓存。
  3. 取消或推迟不必要的任务。例如,从内存中卸载一些很长时间没有使用的标签。
  4. 记录日志以帮助开发人员解决内存不足错误。
  5. 显示一个不错的错误消息,让用户知道发生了什么。
  6. ...

因此,是的,有很多原因需要手动处理内存不足错误!