75

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

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

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

4

24 回答 24

37

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

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

于 2008-12-02T12:04:54.207 回答
17

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

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

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

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

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

于 2008-12-02T12:16:20.613 回答
9

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

于 2008-12-02T16:08:36.493 回答
8

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

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

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

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

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

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

于 2009-01-03T22:37:51.073 回答
5

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

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

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

于 2008-12-02T12:08:28.087 回答
5

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

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

OOM处理的主要问题是:

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

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

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

此外,底层操作系统在 OOM 方面的行为必须是可预测的。例如,如果启用了内存过量使用,Linux 就不会。许多启用交换的系统会在向有问题的应用程序报告 OOM 之前死掉。

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

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

于 2008-12-02T12:18:56.567 回答
4

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

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

但是,在多线程应用中很多情况下,后台线程(包括系统/3rd-party库创建的)也可能发生OOE。几乎无法预测,您可能无法恢复所有线程的状态。

于 2008-12-02T12:38:31.410 回答
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;
}

这是一个不错的论据,可以为一般的垃圾收集器添加注册/注销自收集/压缩对象的支持。

于 2008-12-02T15:26:12.540 回答
1

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

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

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

于 2008-12-02T12:03:27.270 回答
1

尤其是在垃圾收集环境中,如果您在应用程序的高级别捕获 OutOfMemory 错误,则可能会引用很多内容超出范围并且可以回收以归还内存。

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

于 2009-01-03T23:28:15.670 回答
1

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

我只是举这个例子来说明它是可以做到的。

处理OOM的问题真的取决于你的程序和环境。

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

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

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

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

所以,是的,OOM 是可恢复的,但它可能非常困难,特别是在 Java 等现代环境中,它有大量不同质量的 3rd 方库。

于 2009-01-03T23:33:38.810 回答
1

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

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

如果大部分内存被缓存或 mmap'd 区域占用,您可以通过释放缓存或取消映射来回收其中的一部分。然而,这确实需要您知道您使用该内存的目的 - 正如您已经注意到的那样,大多数程序都没有,或者它没有任何区别。

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

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

于 2008-12-02T13:05:09.233 回答
1

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

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

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

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

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

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

于 2013-08-05T19:02:26.717 回答
0

这是一个困难的问题。乍一看,似乎没有更多的记忆意味着“运气不好”,但是,您还必须看到,如果一个人真的坚持,一个人可以摆脱许多与记忆相关的东西。让我们以其他方式破坏函数 strtok ,一方面它在内存方面没有问题。然后从 Glib 库中获取对应的 g_string_split,它在很大程度上依赖于内存分配,就像 glib 或基于 GObject 的程序中的几乎所有内容一样。可以肯定地说,在更动态的语言中,内存分配比在更不灵活的语言中使用得更多,尤其是 C。但让我们看看替代方案。如果你在内存不足的情况下结束程序,即使是仔细开发的代码也可能停止工作。但是如果你有一个可恢复的错误,你可以做一些事情。所以论据,

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

问候

于 2008-12-02T12:05:09.887 回答
0

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

于 2009-01-03T23:35:41.253 回答
0

现在只是让我感到困惑。

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

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

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

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

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

出色地...

于 2008-12-11T21:35:45.063 回答
0

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

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

于 2012-01-04T18:24:58.820 回答
0

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

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

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

于 2009-05-18T19:50:57.753 回答
0

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

于 2009-02-03T08:06:09.427 回答
0

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

于 2010-01-13T15:31:16.700 回答
0

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

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

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

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

我的两分钱:-)

于 2009-02-21T19:26:26.110 回答
0

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

我无论如何都看不到在多线程应用程序中实现这一点。你怎么知道哪个线程实际上是造成内存不足错误的原因?一个线程可以不断地分配新内存,并且 gc-root 占堆的 99%,但第一次分配失败发生在另一个线程中。

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

于 2009-11-09T21:33:08.317 回答
0

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

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

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

于 2019-11-19T13:10:12.913 回答
-1

我有这个:

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

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

于 2009-08-07T13:40:57.750 回答