当程序终止前没有释放malloc分配的内存,会发生什么?

654
我们都被教导必须释放每个分配的指针。然而我有点好奇,如果不释放内存会有什么实际代价。在一些明显的情况下,比如在循环或线程执行的一部分中调用 malloc() 时,释放内存非常重要以避免内存泄漏。但是考虑以下两个示例:
首先,如果我的代码类似于这样:
int main()
{
    char *a = malloc(1024);
    /* Do some arbitrary stuff with 'a' (no alloc functions) */
    return 0;
}

这里的真正结果是什么?我的想法是,进程会死亡,然后堆空间也会消失,因此错过调用free并没有什么损失(但是,我认识到为了闭包、可维护性和良好的实践,还是有必要的)。我的想法正确吗?

其次,假设我有一个类似于shell的程序。用户可以像这样声明变量:aaa = 123,并将其存储在一些动态数据结构中以供以后使用。显然,你会使用一些调用某些*alloc函数的解决方案(哈希表、链表等)。对于这种类型的程序,从malloc调用后永远不释放它们是没有意义的,因为这些变量在程序执行期间始终存在,并且我无法找到好的方法(静态分配空间的话)来实现这一点。拥有大量已分配但仅在进程结束时释放内存的做法是糟糕的设计吗?如果是,那么替代方案是什么?


34
以下是翻译的结果: 以下人们经常说一个好的现代操作系统可以进行清理,但如果代码运行在内核模式下(例如出于性能原因),该怎么办? 内核模式程序(例如Linux中)是否被沙箱化了? 如果没有,我认为你需要手动释放所有内容,即使在类似abort()的异常终止之前。 - SO Stinks
11
@Dr.PersonPersonII,是的,运行在内核模式下的代码通常需要手动释放所有资源。 - zwol
6
我想补充一下,free(a)并不能真正释放内存!它只是重置了libc实现的malloc中一些指针,这些指针跟踪内存页(通常称为“堆”)中可用的内存块。当你的程序终止时,该内存页才会被释放,而不是在此之前。 - Marco Bonelli
2
@MarcoBonelli 部分正确。如果malloc()分配的内存来自“正常”的sbrk堆,并且位于其末尾,则会调用sbrk()来减少内存映像。如果malloc()通过mmap()分配了内存,则在free()中取消映射该内存。 - glglgl
2
@SOStinks 内核“程序”不像用户空间程序那样退出。内核代码“存在”的等效方式是系统关闭,在这种情况下,您不需要释放它。只是在内核中,您通常运行的时间很长,以至于几乎没有情况下您不需要释放内存。 - forest
显示剩余4条评论
20个回答

450
几乎所有现代操作系统在程序退出后会回收所有分配的内存空间。我能想到的唯一例外可能是像Palm OS这样的操作系统,其中程序的静态存储和运行时内存基本相同,因此不释放可能会导致程序占用更多的存储空间(这里只是猜测)。
因此,通常情况下没有害处,除了比需要更多存储空间的运行时成本。当然,在您给出的示例中,您希望保留变量的内存空间,直到其被清除为止。
然而,及时释放不再需要的内存并在程序退出时释放所有剩余内存被认为是良好的编程风格。这更多地是关于了解您正在使用的内存,并考虑是否仍然需要它的练习。如果您不跟踪,可能会出现内存泄漏。
另一方面,类似于在退出时关闭文件的忠告具有更加具体的结果-如果不这样做,则写入它们的数据可能无法刷新,或者如果它们是临时文件,则在完成后可能不会被删除。此外,应在使用完毕后提交数据库处理程序的事务并将其关闭。同样,如果您正在使用C++或Objective C之类的面向对象语言,则在完成后未释放对象将意味着析构函数永远不会被调用,责任类可能无法清理任何资源。

24
可能最好也要提到并非所有人都在使用现代操作系统,如果有人使用你的程序(且它仍在不支持内存恢复的操作系统上运行),那么就会GG。 - user105033
154
请问您认为以下这句话中哪部分有问题呢?“然而,释放不再需要的内存,并在程序退出时释放任何仍存在的内容被视为良好的编程风格。” - Paul Tomblin
34
如果你有一个内存存储器,在程序退出之前需要一直使用,而且你不是在原始操作系统上运行,那么在程序退出之前释放内存只是一种风格选择,而不是缺陷。 - Paul Tomblin
41
“@Paul -- 只是同意EvilTeach的观点,不释放内存被认为是不好的做法,它不释放内存是错误的。你的措辞让这看起来像是与手帕颜色相匹配一样重要。实际上,这就像穿裤子一样重要。” - Heath Hunnicutt
13
最后一段话是完全错误的。在C语言中,通过exit或从main返回时的隐式exit正常终止程序会关闭并刷新所有打开的文件。 - R.. GitHub STOP HELPING ICE
显示剩余24条评论

129

没错,你说得对。你的例子并不会造成任何伤害(至少在大多数现代操作系统上是这样)。当进程退出时,操作系统将回收进程分配的所有内存。

来源:Allocation and GC Myths(后缀名为PostScript)

分配迷思4:非垃圾回收程序应该总是释放它们分配的所有内存。

真相:在经常执行的代码中省略释放内存可能导致内存泄漏增长。它们很少被接受。但是,保留大部分已分配内存直到程序退出的程序通常表现更好,而无需任何干预释放操作。如果没有释放,则malloc更容易实现。

在大多数情况下,在程序退出之前释放内存是毫无意义的。操作系统将重新获取它。Free将触及并分页死亡对象;操作系统不会。

结果:小心使用“泄漏检测器”来计算分配量。有些“泄漏”是好的!

尽管如此,你真的应该尽量避免所有内存泄漏!

第二个问题:你的设计是可以的。如果需要在应用程序退出之前存储某些东西,则使用动态内存分配是可以的。如果无法提前确定所需大小,则无法使用静态分配的内存。


3
可能是因为我所阅读的问题实际上是关于泄漏内存正在发生什么,而不是这个特定示例是否可行。我不会投反对票,因为这仍然是一个很好的答案。 - DevinB
3
可能早期的Windows和Mac OS(现在可能仍然存在),需要进程在退出前释放内存,否则该空间将无法回收。 - Pete Kirkham
14
我认为用“因为泄漏检测器”的原因来解释需要free()内存是错误的。这就像说“你必须在游戏街上慢行驶,因为警察可能会用速度照相机等着你。” - Sebastian Mach
6
事实上,一个一次性的小泄漏即使在长期运行的程序中也不是问题(强调“一次性”部分)。然而,最好仍然按照最佳实践进行清理,以便验证器不会出现错误提示。这并不是因为让验证器停止报错本身有用,而是因为如果您的验证输出中有大量“可接受”的失败,则更难找到不可接受的失败。 - Jordan Brown
1
引用的论文是以UNIX系统为基础编写的。论文中关于操作系统内存回收的声明是不可移植的!ISO/IEC 9899定义了malloc()函数,但并没有要求程序终止(如通过exit()abort()_Exit())隐式释放内存。告诉人们不调用free()是可以接受的,在Linux、Windows和大多数(所有?)Unix系统上都是无关紧要的。但这仍然是错误的建议:在嵌入式/IoT和复古领域中,还有许多其他托管环境,不调用free()会产生不良后果。 - Christian Hujer
显示剩余2条评论

73

=== 未来的保护和代码可重用性如何处理? ===

如果您编写释放对象的代码,则只能在可以依赖进程关闭时内存被释放才能安全使用代码,也就是小型一次性项目或“一次性”[1]项目中使用……您知道进程何时结束。

如果您编写释放所有动态分配内存的代码,则可以保护代码,并让他人在更大的项目中使用它。


[1] 关于“一次性”项目。在“一次性”项目中使用的代码可能不会被丢弃。下一件事情,你会发现已经过去了十年,你的“一次性”代码仍然在使用)。

我听说过一个有关某个人的故事,他为了让硬件工作得更好而编写了一些代码。他说“只是一种爱好,不会成为大规模专业的”。多年后,许多人正在使用他的“爱好”代码。


22
因“小项目”而被踩。其实有很多大项目故意不在退出时释放内存,因为如果你知道目标平台,这样做会浪费时间。在我看来,更准确的例子应该是“隔离项目”。例如,如果你正在制作一个可重用的库,它将被包含在其他应用程序中,那么就没有明确定义的退出点,所以你不应该泄漏内存。对于独立应用程序,你总是会准确地知道进程何时结束,并可以做出有意识的决定将清理卸载到操作系统(无论如何都必须进行检查)。 - Dan Bechard
8
昨天的应用程序是今天的库函数,而明天它将被链接到一个长期运行的服务器中,被调用数千次。 - Adrian McCarthy
2
@AdrianMcCarthy:如果一个函数检查一个静态指针是否为空,如果是,则使用malloc()进行初始化,并在指针仍为空的情况下终止,这样的函数即使从未调用free也可以安全地使用任意次数。我认为区分可能会使用无限量存储空间的内存泄漏和只能浪费有限且可预测存储空间的情况可能是值得的。 - supercat
@supercat:我的评论是关于代码随时间变化的。当然,泄漏有限量的内存不是问题。但总有一天,有人会想要更改该函数,使其不再使用静态指针。如果代码没有为能够释放指向的内存做出规定,那么这将是一个困难的更改(或者更糟糕的是,更改将是错误的,你最终会得到一个无限泄漏)。 - Adrian McCarthy
2
更改代码以不再使用静态指针可能需要将指针移动到某种“上下文”对象中,并添加创建和销毁此类对象的代码。如果指针在没有分配时始终为null,并且在存在分配时为非空,则在销毁上下文时使代码释放分配并将指针设置为null将是简单明了的,特别是与需要将静态对象移动到上下文结构中的所有其他操作相比。 - supercat

61

你是正确的,没有任何伤害,并且直接退出更快

有各种原因:

  • 所有桌面和服务器环境都只是在exit()时释放整个内存空间。它们不知道程序内部数据结构,比如堆。

  • 几乎所有的free()实现都不会将内存返回给操作系统。

  • 更重要的是,在exit()之前这样做是浪费时间。在exit()时,内存页面和交换空间被简单地释放。相比之下,一系列的free()调用将消耗CPU时间,并可能导致磁盘分页操作、缓存未命中和缓存驱逐。

关于未来代码重用的可能性是否可以证明无意义的操作是正当的考虑,但这可以被认为不是敏捷软件开发的做法。YAGNI!


3
我曾参与一个项目,我们花了很短的时间来理解程序的内存使用情况(我们需要支持这个程序,而不是自己编写它)。基于我的经验,我可以故事性地赞同你提到的第二点。然而,我想听听你(或其他人)能否提供更多证据证明这是正确的。 - user106740
4
没关系,找到答案了:https://dev59.com/5nM_5IYBdhLWcg3wUxjF。 感谢 Stack Overflow! - user106740
8
YAGNI原则有双重作用:你永远不需要优化关闭路径。过早的优化等等。 - Adrian McCarthy
你可以提到在退出程序之前释放内存不仅浪费运行时间,而且还浪费开发人员和潜在的测试时间。 - Peter - Reinstate Monica
“敏捷”论点是错误的。这也是敏捷的一种表现:在两个或更多大致相等的选项中,选择使未来变化更容易的路径。而且这条路上可能会在exit()之前调用free():当程序发展时,exit()不一定会始终位于程序末尾。因此,“敏捷”论点是“情况而定”。 - Christian Hujer
@ChristianHujer: 关键是这些并不是在其他情况下大致等价的。 显式释放所有内容后退出的成本要高得多,因此“可能的未来使用”不能成为额外费用的充分理由。 - Chris Dodd

46

我完全不同意那些说OP是正确的或者没有害处的人。

大家都在谈论现代或传统的操作系统。

但如果我处于一个根本没有操作系统的环境中呢?

想象一下,现在你正在使用线程样式的中断和分配内存。 在C标准ISO/IEC:9899中,内存的生命周期被规定为:

7.20.3 内存管理函数

1 调用calloc、malloc和realloc函数分配存储空间时,分配的存储空间的顺序和连续性是未指定的。 如果分配成功,则返回的指针适当地对齐,以便可以将其分配给任何类型的对象的指针,然后用于访问在分配的空间中分配的该对象或这些对象的数组,直到显式释放该空间为止。 分配对象的生命周期从分配开始直到释放为止。[...]

因此,并没有规定环境会自动释放内存。 否则,最后一句话会加上:“或直到程序终止。”

因此,换句话说: 不释放内存不仅是不良实践,还会产生不可移植且不符合C规范的代码。 至少在支持 '...' 的环境中可以看作是“正确的”。

但在没有任何操作系统的情况下,没有人会替你完成这项工作 (我知道通常在嵌入式系统上不会分配和重新分配内存,但也有可能需要这样做)。

因此,在一般的纯C中(就像OP被标记的那样), 这简单地产生了错误和不可移植的代码。


12
反驳意见是,如果你处于嵌入式环境中,作为开发者,你会更加注重内存管理。通常情况下,你会提前静态分配固定的内存空间,而不是在运行时进行任何的内存动态分配或重新分配操作。 - John Go-Soco
5
虽然你所说的并不是错误的,但这并不能改变语言标准陈述的事实,任何违反或进一步推广它的人,即使通过常识,都会产生限制性的代码。我可以理解如果有人说“我不必关心它”,因为有足够的情况是可以接受的。但是那个人至少应该知道为什么他不必关心它,特别是在问题与特殊情况无关时,不能忽略它。由于OP在整体上涉及C的理论(学校)方面提问,所以说“你不需要”是不可以的! - dhein
7
在大多数没有操作系统的环境中,程序无法“终止”,因为没有可用的手段。 - supercat
6
在没有操作系统的情况下,引用C标准大部分并不适用,因为没有运行时来提供标准所要求的功能,特别是关于内存管理和标准库函数(这些也显然随着运行时/操作系统一起缺失)。 - user3160514
2
@Nax:你能告诉我标准确切地规定了这些原则是强制性要求吗?事实上,标准只是说明了由什么环境提供什么机制以及谁的工作是实现它。像内存管理之类的东西甚至都没有被讨论过。即使有,标准也明确考虑到了操作系统的缺失,因为它针对每个特性都说明了编译器、操作系统是否需要实现特定的功能。这样写是为了确保即使在没有操作系统的情况下,标准也是适用的。 - dhein
显示剩余3条评论

26
我通常会在确认不再需要分配的块后释放每个已分配的块。今天,我的程序入口点可能是main(int argc, char *argv[]),但明天它可能是foo_entry_point(char **args, struct foo *f)并作为函数指针进行类型定义。
所以,如果发生这种情况,我现在就会有一个泄漏。
关于你的第二个问题,如果我的程序像a=5这样输入,我会为a分配空间,或者在后续的a="foo"上重新分配相同的空间。这将保留分配,直到:
1.用户键入“unset a” 2.我的清理函数被调用,无论是服务信号还是用户键入“quit”
我想不出任何现代操作系统不会在进程退出后回收内存。然而,free()很便宜,为什么不清理呢?正如其他人所说,像valgrind这样的工具非常适合发现您确实需要担心的泄漏。即使您示例的块被标记为“仍可访问”,当您尝试确保没有泄漏时,这只是额外的噪音。
另一个谬误是“如果它在main()中,我就不必释放它”,这是不正确的。考虑以下内容:
char *t;

for (i=0; i < 255; i++) {
    t = strdup(foo->name);
    let_strtok_eat_away_at(t);
}

如果这个程序在分叉/守护进程之前运行(理论上永久运行),那么你的程序已经泄露了大小不确定的t 255次。
一个好的、写得好的程序应该总是在自己之后进行清理。释放所有内存,刷新所有文件,关闭所有描述符,取消链接所有临时文件等等。这个清理函数应该在正常终止时被调用,或者在收到各种致命信号时被调用,除非你想留下一些文件,以便在崩溃时检测并恢复。
实际上,当你转移到其他事情时,请对那些必须维护你的东西的可怜灵魂友好一点,把它交给他们“valgrind clean” :)

1
free() 很便宜,除非你有数十亿个具有复杂关系的数据结构需要逐个释放,遍历数据结构尝试释放所有内容可能会显著增加关闭时间,特别是如果其中一半数据结构已经分页到磁盘上,而没有任何好处。 - Lie Ryan
6
如果你拥有10亿个结构,那么你肯定会有其他需要特殊考虑的问题 - 超出了这个特定答案的范围 :) - Tim Post

16

当你退出时,不释放内存是完全可以的;malloc()从称为“堆”的内存区域分配内存,进程的整个堆在进程退出时被释放。

话虽如此,人们仍然坚持在退出前释放所有内存的一个原因是,内存调试器(例如Linux上的valgrind)会将未释放的块检测为内存泄漏,如果你还有“真正”的内存泄漏,那么如果你也得到“假”的结果,就更难以发现它们了。


1
Valgrind难道不会很好地区分“泄漏”和“仍然可达”吗? - Christoffer
13
“完全没问题”这个说法是不好的编程习惯,因为分配的内存没有被释放。如果将这段代码提取成库,那么这将导致内存泄漏。 - DevinB
7
+1 表示补偿。请参考 compie 的回答。在程序退出时使用 free 可能会带来危害。 - R.. GitHub STOP HELPING ICE

14

这段代码通常可以正常运行,但请考虑代码重用的问题。

您可能编写了一些代码片段,它没有释放分配的内存,但以这种方式运行,内存然后被自动回收。看起来没什么问题。

然后,其他人将您的片段复制到他的项目中,并以每秒执行一千次的方式运行。那个人现在在他的程序中有一个巨大的内存泄漏。总体上不是很好,对于服务器应用程序通常是致命的。

代码重用在企业中很常见。通常公司拥有其员工所产生的所有代码,每个部门都可以重复使用公司所拥有的任何东西。因此,编写这样“看似无害”的代码可能会给其他人带来潜在的头痛。这可能导致您被解雇。


4
值得注意的是,不仅有人复制代码片段的可能性,还有一种情况是某个旨在执行特定操作的程序被修改后进行重复操作。在这种情况下,分配内存一次并反复使用而无需释放是可以的,但是对于每个操作都分配并且没有释放内存则可能会造成灾难性后果。 - supercat

13
这里的真实结果是什么?
你的程序泄漏了内存。根据你的操作系统,它可能已经被恢复了。
大多数现代桌面操作系统在进程终止时会自动恢复泄漏的内存,所以人们往往忽视这个问题(正如其他答案中所看到的)。但是你依赖于一种不属于语言本身的安全特性,这是你不应该依赖的。你的代码可能在一个这种行为会导致“严重”内存泄漏的系统上运行,下一次就会出问题。
你的代码可能最终在内核模式下运行,或者在老旧/嵌入式操作系统上运行,这些操作系统没有采用内存保护作为一种权衡。 (MMU占用芯片空间,内存保护需要额外的CPU周期,要求程序员在退出之前清理资源并不过分)。
你可以随意使用和重复使用内存(和其他资源),但请确保在退出之前释放所有资源。
我想我会加上这个小小的历史宝藏,一个来自Amiga的Rom Kernel手册的截图,也就是官方的平台文档。

"In fact, Amiga programmers need to be careful with every system resource, not just memory. All system resources from audio channels to the floppy disk drives are shared among tasks. Before using a resource, you must ask the system for access to the resource. This may fail if the resource is already being used by another task. --- Once you have control of a resource, no other task can use it, so give it up as soon as you are finished. When your program exits, you must give everything back whether it's memory, access to a file, or an I/O port. You are responsible for this, the system will not do it for you automatically."


在应用程序可以使用DMA而操作系统不知道的平台上[在PC上使用未被操作系统考虑的硬件时很常见,或者在Amiga上使用硬件以实现比操作系统更好的图形和声音],当应用程序退出时,操作系统将内存保留分配会导致存储泄漏,可能导致内存耗尽,但释放即将被DMA操作写入的存储空间会导致内存损坏。 - supercat
我的观点是,在某些平台上,可能存在OS不知道的内存使用者。理想情况下,操作系统应该有单独的“分配将在退出时释放的内存”和“分配不能隐式释放的内存,原因操作系统可能无法知道”的调用,但我不知道Amiga是否具备这一功能,PC则肯定没有。 - supercat
2
在Amiga exec.library中,如果使用AllocMem()后没有调用Free(),则会导致内存“丢失”,直到重新启动系统。malloc和free将在底层使用这些内存。 - Richie
1
@Richie 这正是我在回答中所想的。某些操作系统会替你清理资源,但这并不改变你最初泄漏资源的事实。 - DevSolar
现代超级计算机操作系统也是Linux(参见https://top500.org/...),它们在进程退出或终止后也会清理内存。许多Linux开源软件(包括[GCC](https://gcc.gnu.org/)编译器或[RefPerSys](http://refpersys.org/)推理引擎...)并不费心释放所有通过`malloc`分配的(或在C++中通过堆内存`new`分配的)动态内存。 - Basile Starynkevitch
显示剩余3条评论

11

如果你正在使用你已经分配的内存,那么你就没有做错什么。当你编写(除了主函数之外的)分配内存但不释放并使其可供程序余下部分使用的函数时,这就成为一个问题。然后,你的程序将继续运行并分配了这些内存,但却无法使用它们。你的程序和其他正在运行的程序都无法使用该内存。

编辑:说其他正在运行的程序被剥夺了那部分内存并不完全准确。操作系统可以通过牺牲你的程序并将其换出到虚拟内存中来让其他程序使用它(</手势>)。但是,如果你的程序释放它没有使用的内存,则不太可能需要虚拟存储器交换。


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