当realloc由于内存问题失败时,如何处理?

25

问题已经说得很清楚了,这里是一个示例:

typedef struct mutable_t{
    int count, max;
    void **data;
} mutable_t;


void pushMutable(mutable_t *m, void *object)
{
    if(m->count == m->max){
        m->max *= 2;
        m->data = realloc(m->data, m->max * sizeof(void*));
    }
    // how to handle oom??
    m->data[m->count++] = object;
}

我该如何处理内存耗尽而不会将所有数据都设为NULL?

编辑 - 假设有一些可以做的事情,例如释放某些内存或至少告诉用户“你不能这样做 - 你的内存已用完”。理想情况下,我希望保留先前分配的内存。


1
高度依赖应用程序... 但有一件事是确定的,OOM非常关键。 - jldupont
请始终检查动态分配的内存,以确保它们都已正确的被分配和释放。 - jldupont
仅仅是为了补充一下这里的几个答案,对于如何处理失败的realloc()(在你的情况下),一个想法是执行m->max /= 4; m->max *= 3;,然后再尝试调用realloc(),看看我们是否仍然可以挤出更多的字节。你甚至可以尝试几次,每次使用越来越小的大小,但在某些时候这样做就不值得了。 - Chris Lutz
3
如果 (!m->data) { log("请升级到 64 位版本"); abort(); } - Hans Passant
8个回答

33

标准技术是引入一个新变量来保存 realloc 的返回值。如果成功,您只会覆盖输入变量:

tmp = realloc(orig, newsize);
if (tmp == NULL)
{
    // could not realloc, but orig still valid
}
else
{
    orig = tmp;
}

9
然后呢?你不是为了好玩才试图增加数组大小,你实际上有一个需要的理由。 - Blindy
3
@Blindy - 这次操作失败了。根据应用程序的逻辑,决定如何进行恢复(也许这是一个服务器,会失败一个请求但继续处理其他请求)。但这看起来像是低级库代码,不应该在应用程序上强制执行 out-of-memory 策略。 - R Samuel Klatchko
我需要释放_tmp_吗? - andreaconsole
6
@andreaconsole - 如果tmp为NULL,则无所谓(在所有现代的malloc实现中free(NULL)都是可以的)。如果tmp不为NULL,则需要在正确的时机释放它。代码中,首先用malloc(size)分配了一块大小为size的内存区域orig,然后使用realloc(orig, newsize)扩展或缩小该内存块,得到新的指针tmp。如果tmp为NULL,则说明内存不足,需要释放原来的内存块orig并退出程序;否则,将tmp赋值给orig,继续进行后续操作。最后,需要在程序结束前释放orig指向的内存块。 - R Samuel Klatchko
所以,由于这是一个指针,我总是需要释放_orig_,对吗? - andreaconsole
显示剩余2条评论

11

realloc()失败时,应采取的策略取决于您的应用程序。这个问题太笼统了,无法回答所有可能的情况。

一些其他注意事项:

永远不要做以下事情:

a = realloc(a, size);

如果realloc()失败,您将失去原始指针,并且realloc()不会释放原始内存,因此会导致内存泄漏。相反,请执行以下操作:
tmp = realloc(a, size);
if (tmp)
    a = tmp;
else
    /* handle error */

我想提出的第二点是次要的,可能不那么关键,但了解它仍然很好:增加要分配的内存因子f是有好处的。假设你首先使用malloc()分配n个字节。然后你需要更多的内存,所以你使用大小为n×frealloc()。然后你需要更多的内存,所以你需要n×f2个字节。如果你希望realloc()使用前两个内存块中的空间,你需要确保n×f2 ≤ n + n×f。解决这个方程,我们得到f≤(sqrt(5)+1)/2=1.618黄金分割)。我大多数时候使用1.5的因子。


你有关于内存分配算法的更多资料吗? - Jori
你不觉得尝试分配一个巨大但不必要的内存会有风险吗?我有几个包含 10^9 元素的数组,可能需要对其中两个进行 realloc。代码已经占用了 10% 的内存,我担心 realloc 会失败。我在考虑 realloc(old_size + 1000),但我知道这样做通常会导致多次调用 realloc。这样做会有什么问题吗?(现在应该不会出现这种情况,但将来呢...) - GRquanti

9

这是一个比较敏感的话题,因为在这个问题上有两种不同的观点:

  1. 检测到OOM(内存溢出),并让函数返回错误代码。
  2. 检测到OOM并尽快崩溃进程。

个人而言,我支持第二种观点。除非是一些特殊类型的应用程序,否则OOM是致命的。确实,完美编写的代码可以处理OOM,但很少有人能够理解如何编写安全的代码以应对没有内存的情况。更少的人会真正去做,因为几乎不值得花费这样的努力。

我不喜欢将OOM的错误代码传递给调用函数,因为这相当于告诉调用者“我失败了,你无能为力”。相反,我更喜欢尽快崩溃,以便生成的转储文件尽可能地具有指导性。


1
关于OOM失败,有一些事情可能会发生。虽然并不多,但在某些情况下是可能的。(在大多数应用程序中,应该使用malloc()realloc()的包装器来处理内存失败的错误消息,但对于少数有更好解决方案的应用程序而言,它们不这样做)。 - Chris Lutz
@Chris,确实如此,有些产品(例如SQL Server)在这方面非常出色。然而,这些产品是罕见的例外。要做到正确无误需要极高的纪律性、执行力和理解力。以至于人们很少尝试去做到完美无缺。 - JaredPar
1
@JaredPar,你的意思是因为大多数人无法正确处理错误,所以你甚至不应该关心错误,而是让应用程序崩溃和烧毁,可能会破坏用户的数据吗?问题在于OOM发生在用户机器上的运行时。您无法控制这些机器的内存大小和交换文件的硬盘空间。然后再加上内存泄漏...此外,测试您的应用程序是否可以处理它非常容易。使用一个包装器来随机返回NULL的malloc/realloc。 - Secure
1
@Secure,我的意思是尽快失败是获得可操作的错误报告的绝佳方式。在我的职位上,我处理了很多Watson错误。快速失败的代码路径会产生非常可操作的数据,并且通常会导致错误被修复。试图处理像OOM这样的情况的代码路径几乎总是1)处理不正确或2)将其传递给无法处理该情况的代码。两者都会导致崩溃并产生非常不可操作的错误,因为崩溃发生在最初的真正问题之后很远的地方。 - JaredPar

3

当使用realloc时,第一个规则是不要将realloc的返回值赋值给与其传递的相同指针。这样会导致内存泄漏。

m->data = realloc(m->data, m->max * sizeof(void*)); 

realloc函数有缺陷。如果realloc函数执行失败,它将返回空指针,但是它不会释放旧的内存。上述代码将使你的m->data变成null指针,而之前由m->data指向的旧内存块很可能会成为内存泄漏(如果你没有其他引用它的地方)。

应该先将realloc函数的返回值存储在一个单独的指针中。

void **new_data;
...
new_data = realloc(m->data, m->max * sizeof(void*)); 

然后您可以检查成功/失败,并在成功的情况下更改 m->data 的值

if (new_data != NULL)
  m->data = new_data;
else
  /* whatever */;

2
  1. 了解应用程序框架如何处理OOM。许多框架根本无法处理OOM。大多数情况下,除非框架在某个地方非常明确和明确地说明它能够在没有空闲RAM的情况下正常运行,否则框架在无空闲RAM条件下将无法正常运行。如果框架不能处理OOM并且是多线程的(现在许多框架都是这样),在许多情况下,OOM将是进程终止的结束。即使它不是多线程的,它可能仍然接近崩溃。无论您退出进程还是框架退出进程可能都是无关紧要的;可预测的立即退出可能只比在不远的将来的某个半随机点上崩溃好一点。

  2. 如果您正在为一个明确定义的操作集使用单独的专用子内存池(即不是您通常的malloc),这些操作仅受OOM的内存使用限制(即当前操作在子内存池中因OOM而被回滚或清除,而不是整个进程或主内存池),并且该子池也不被应用程序框架使用,或者如果您的框架和整个应用程序的其余部分都设计为在无空闲RAM条件下维护有意义的状态和持续操作(在内核模式和某些类型的系统编程中很少见,但并非没有),则返回错误代码而不是崩溃进程可能是正确的。

  3. 理想情况下,处理的大部分内存分配(甚至更理想的是所有分配)应尽早地在处理中分配,最好是在适当开始之前,以最小化数据完整性丢失的问题和/或如果失败,则需要回滚编码的数量。实际上,在许多情况下,为了节省项目的编程成本和时间,保护数据完整性的应用程序依赖于数据库事务,并要求用户/支持人员在发生内存不足错误时检测GUI崩溃(或服务器崩溃)并重新启动应用程序,而不是编写以处理和回滚任何数以千计的潜在OOM情况的最佳方式。然后,努力将应用程序暴露于过载情况的限制范围内,这可能包括对数据大小和同时连接和查询的额外验证和限制。

  4. 即使您检查可用RAM的剩余量,通常其他代码也会像您一样分配或释放内存,从而改变您的内存检查基础,可能导致OOM。因此,在分配之前检查可用的空闲RAM通常不是解决确保应用程序在可用RAM限制内运行并维护数据完整性的问题的可靠解决方案。

  5. 最好的情况是知道您的应用程序在所有可能的情况下需要多少内存,包括任何框架开销,并将该数字保持在可用于您的应用程序的RAM量内,但是系统通常非常复杂,外部依赖关系决定数据大小,因此实现这一点可能是不现实的。

当然,酸度测试是通过高运行时间和不频繁的数据损坏、丢失或崩溃来满足用户需求。在某些情况下,

关于realloc:

检查realloc的返回值-将其放入临时变量中。仅在请求的新大小大于0时关心它是否为NULL。在其他情况下,将其放入非临时变量中:

例如

    void* temp = realloc(m->data, m->max * sizeof(void*));
    if (m->max!=0&&temp==NULL) { /* crash or return error */ }
    m->data =(void**)temp;

编辑

在(1)中将“大多数情况”更改为“很多情况”。

我认识到您说如果无法分配内存,则应假定“可以做些什么”。但是,内存管理是一个非常全局的考虑因素!


2

这完全是你的问题!以下是一些标准:

  • 你请求那块内存是有原因的。如果它不可用,你的程序工作是否注定失败?如果是前者,你需要用错误消息终止你的程序;否则,你可以以某种方式显示错误消息并继续运行。

  • 是否有可能用时间来换取空间?你能否使用一种占用更少内存的算法回复你尝试的任何操作?这听起来像是很多工作,但实际上这是在没有足够内存的情况下继续程序操作的可能性。

  • 如果你的程序在没有这些数据和足够内存的情况下继续独自摇摇晃晃,那么这样做是否会出错?如果是,你应该用错误消息终止程序。比盲目地处理错误数据而不是杀死程序要好得多。


1

realloc 还可能出现另一个微妙的错误。由于返回 NULL 指针而导致的内存泄漏相当出名(但很少会遇到)。 我的程序偶尔会因为 realloc 调用而崩溃。我有一个动态结构,它通过类似于这个 realloc 的方式自动调整大小:

m->data = realloc(m->data, m->max * sizeof(void*)); 

我犯的错误是没有检查m->max == 0,这导致了内存区域被释放。并且使我的m->data指针变得陈旧。
我知道这有点离题,但这是我使用realloc遇到的唯一真正问题。

我刚刚发现的有趣事情(即在2016年)是,我当时使用的stdlib没有正确遵循标准,因为在调用长度为0的realloc()时需要返回NULL。这本来不会引发第一个错误。令人着迷的是,我非常清楚地记得那个错误,在一个非常古老(当时已经如此)的Solaris机器上发生于2004年左右。 - Patrick Schlüter

1
我遇到了一个问题。配置是:操作系统:win7(64);IDE:vs2013;Debug(Win32)。当我的realloc返回null时,由于内存不足,我有两个解决方案:
1. 更改项目属性,启用大地址。 2. 将解决方案平台从Win32更改为x64。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接