当您在malloc之后不释放时,真正发生了什么?


538

多年来,这一直困扰着我。

我们都在学校里受教(至少在我以前是这样),您必须释放分配的每个指针。但是,对于不释放内存的实际成本,我有点好奇。在某些明显的情况下,例如在malloc循环或线程执行的一部分内调用when时,释放非常重要,这样就不会发生内存泄漏。但是,请考虑以下两个示例:

首先,如果我有这样的代码:

int main()
{
    char *a = malloc(1024);
    /* Do some arbitrary stuff with 'a' (no alloc functions) */
    return 0;
}

真正的结果是什么?我的想法是该过程终止,然后无论如何堆空间都消失了,因此错过该调用不会有任何危害free(但是,我的确意识到无论如何对于关闭,可维护性和良好实践而言,保持它的重要性)。我的想法对吗?

其次,假设我有一个程序有点像shell。用户可以声明类似的变量,aaa = 123并将这些变量存储在某种动态数据结构中以备后用。显然,显然您会使用一些可调用* alloc函数的解决方案(哈希映射,链接列表等)。对于这种程序,调用后永远释放是没有意义的,malloc因为这些变量必须在程序执行期间始终存在,并且没有很好的方法(我可以看到)用静态分配的空间来实现。分配一堆但仅在进程结束时释放的内存是不好的设计吗?如果是这样,还有什么选择?


7
@NTDLS评级系统的魔力实际上有效了一次:6年了,“更当之无愧”的答案的确达到了顶峰。
zxq9

15
下面的人一直说,一个好的现代OS可以清除,但是如果代码以内核模式运行(例如,出于性能原因)怎么办?内核模式程序(例如在Linux中)是否已沙箱化?如果没有,我想您应该手动释放所有内容,甚至在任何异常终止之前,例如abort()。
Person Person II博士

3
@ Dr.PersonPersonII是的,以内核模式运行的代码通常必须手动释放所有内容。
zwol

1
我想补充一点free(a),实际上没有任何事情可以释放内存!它仅重置malloc的libc实现中的一些指针,这些指针跟踪大的映射内存页面(通常称为“堆”)中的可用内存块。该页面仍将仅在程序终止时才释放,而不是在此之前终止。
Marco Bonelli,

1
Free()可能会或可能不会实际释放内存。它可能只是将块标记为已释放,以供以后回收,或者可以将其链接到空闲列表中。它可能将其合并到相邻的空闲块中,或者可能会将其留作后续分配​​。全部都是实现细节。
约旦·布朗

Answers:


378

程序退出后,几乎每个现代操作系统都将恢复所有分配的内存空间。我能想到的唯一例外可能是类似Palm OS的程序,其中程序的静态存储和运行时内存几乎是同一件事,因此不释放可能会导致程序占用更多的存储空间。(我只是在这里推测。)

因此,通常来说,这没有什么害处,除了拥有超出所需存储空间的运行时成本。当然,在您给出的示例中,您想要保留一个变量的内存,直到清除该变量为止。

但是,这是一种很好的样式,可以在不再需要时释放内存,并在程序退出时释放仍然可用的任何内容。这更多的是了解您正在使用的内存,并思考是否仍需要它。如果不跟踪,则可能会发生内存泄漏。

另一方面,在退出时关闭文件的类似建议会产生更具体的结果-如果不这样做,则写入您的数据可能不会被刷新,或者如果它们是临时文件,则可能不会完成后将其删除。同样,数据库句柄应提交它们的事务,然后在完成后关闭它们。同样,如果您使用的是面向对象的语言(例如C ++或Objective C),则在完成处理后不释放对象将意味着将永远不会调用析构函数,并且类负责的任何资源也可能无法清除。


16
如果不是有人使用您的程序(如果该程序仍在无法恢复内存的操作系统上运行)运行了GG,那么也许并不是所有人都在使用现代操作系统。
user105033

79
我真的认为这个答案是错误的。一个人应该总是在使用完资源后重新分配资源,无论是文件句柄/内存/互斥体。通过养成这种习惯,在构建服务器时不会犯那种错误。某些服务器预计将全天候运行24x7。在这种情况下,任何形式的泄漏都意味着您的服务器最终将耗尽该资源并以某种方式挂起/崩溃。一个简短的实用程序,泄漏不是那么糟糕。任何服务器,任何泄漏都是死亡。帮个忙。自己清理一下。这是个好习惯。
EvilTeach

120
“但是,在不再需要内存时释放内存,并在程序退出时释放您仍然拥有的任何东西,这被认为是一种很好的样式。” 那你认为错了吗?
Paul Tomblin,2009年

24
如果您有一个需要存储的内存,直到程序退出时为止,并且您没有在原始操作系统上运行,那么在退出之前释放内存是一种风格选择,而不是缺陷。
Paul Tomblin'1

30
@Paul-只是同意EvilTeach,释放内存不被认为是好的样式,不释放内存是不正确的。您的措辞使这看起来和配戴与领带一样的手帕一样重要。实际上,这是在穿裤子的水平上。
Heath Hunnicutt

110

是的,您是对的,您的示例没有任何危害(至少在大多数现代操作系统上没有)。进程退出后,操作系统将恢复由进程分配的所有内存。

来源:分配和GC神话(PostScript警告!)

分配误区4:非垃圾收集程序应始终释放其分配的所有内存。

真相:频繁执行的代码中省略的重分配会导致泄漏增加。它们很少被接受。但是在程序退出之前保留分配的内存最多的程序通常会在没有任何介入释放的情况下执行得更好。如果没有免费的内存,则Malloc易于实现。

在大多数情况下,在程序退出之前释放内存是没有意义的。 操作系统仍会收回它。自由会触碰并翻页死物;操作系统不会。

结果:请小心“泄漏检测器”,它会统计分配情况。一些“泄漏”是好的!

也就是说,您应该真正避免所有内存泄漏!

第二个问题:您的设计还可以。如果您需要存储某些内容,直到应用程序退出,那么可以通过动态内存分配来完成。如果您不预先知道所需的大小,则不能使用静态分配的内存。


3
可能是因为,在我阅读本文时,问题是泄漏的内存实际上正在发生什么,而不是这个特定示例是否正确。不过,我不会投反对票,因为这仍然是一个很好的答案。
DevinB,2009年

3
可能有(早期是Windows,早期是Mac OS)并且仍然存在,要求进程在退出之前释放内存的操作系统,否则将无法回收空间。
皮特·柯坎

除非您担心内存碎片或内存不足,否则这完全没问题-这样做太多,应用程序性能将消失。除了困难的事实,还应始终遵循最佳实践和良好的习惯养成。
NTDLS

1
我有一个可以接受的答案,目前的答案是-11,所以他甚至都没有参加比赛。
Paul Tomblin,2009年

8
我认为通过说“由于检漏器”来解释需要free()来存储内存是错误的。这就像在说:“您必须在游戏街上缓慢行驶,因为警察可能会使用高速摄像机在等您。”
塞巴斯蒂安·马赫

57

=== 将来的校对代码重用如何?===

如果您编写代码来释放对象,那么您将代码限制为仅在可以依靠关闭的进程来释放内存的情况下才可以安全使用,即少量使用项目或“扔掉” [1]项目)...,您知道流程何时结束。

如果您确实编写了释放所有动态分配的内存的代码,那么将来您将对代码进行验证,并让其他人在更大的项目中使用它。


[1]关于“一次性”项目。“丢弃”项目中使用的代码具有不被丢弃的方式。您知道已经过去了十年的下一件事,并且您的“丢弃”代码仍在使用中)。

我听到一个故事,说某人只是为了好玩而编写一些代码,以使其硬件更好地工作。他说:“ 只是一种爱好,不会变得又大又专业 ”。几年后,许多人都在使用他的“爱好”代码。


8
因“小型项目”而被否决。有许多大型项目非常有意地在退出时释放内存,因为如果您知道目标平台,那会浪费时间。海事组织,一个更准确的例子就是“孤立的项目”。例如,如果您要创建将包含在其他应用程序中的可重用库,则没有明确定义的退出点,因此您不应泄漏内存。对于独立的应用程序,您将始终确切地知道进程何时结束,并且可以有意识地做出决定,将清理工作卸载到OS(必须以两种方式进行检查)。
丹·贝查德

昨天的应用程序是今天的库函数,明天的应用程序将链接到一个寿命很长的服务器,该服务器将调用它数千次。
Adrian McCarthy

1
@AdrianMcCarthy:如果一个函数检查静态指针是否为空,malloc()如果初始化为空,则将其初始化,如果指针仍然为空,则终止,即使free从未调用过,也可以安全地使用该函数任意次数。我认为,区分仅会浪费有限且可预测的存储空间的内存泄漏(可能会耗尽无穷数量的存储空间)可能是值得的。
超级猫

@supercat:我的评论是谈论代码随时间的变化。当然,泄漏有限数量的内存不是问题。但是总有一天,有人会想要更改该函数,以使其不再使用静态指针。如果代码没有规定能够释放指向的内存,那将是一个艰难的改变(或者更糟的是,该改变将是不好的,并且最终会导致无限的泄漏)。
Adrian McCarthy

1
@AdrianMcCarthy:将代码更改为不再使用静态指针可能会需要将指针移到某种“上下文”对象中,并添加代码来创建和销毁此类对象。如果指针总是null在没有分配的情况下存在,并且在存在分配的情况下为非空,那么让代码释放分配并将指针设置null为销毁上下文时将很简单,特别是与需要完成的所有其他操作相比将静态对象移到上下文结构中。
supercat

52

您是正确的,没有造成任何伤害,退出时更快

造成这种情况的原因有很多:

  • 所有台式机和服务器环境都只需在exit()上释放整个内存空间。他们不知道程序内部的数据结构,例如堆。

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

  • 更重要的是,在exit()之前完成操作会浪费时间。在退出时,仅释放内存页面和交换空间。相比之下,一系列的free()调用将消耗CPU时间,并可能导致磁盘分页操作,缓存未命中和缓存逐出。

关于possiblility未来的代码重用justifing的确定性毫无意义的OPS的:这是一个考虑因素,但它无疑不是敏捷的方式。亚尼!


2
我曾经在一个项目中工作过,在该项目中我们花了很短的时间试图了解程序的内存使用情况(我们需要为其提供支持,但并未编写)。根据经验,我奇特同意您的第二点。但是,我想听听您(或某人)提供更多的证据证明这是真的。
user106740

3
没关系,找到了答案: stackoverflow.com/questions/1421491/…。谢谢你这么!
user106740

@aviggiano,称为YAGNI。
v.oddou

YAGNI原理有两种作用:永远不需要优化关闭路径。过早的优化等等。
Adrian McCarthy

26

我完全不同意说OP是正确的或没有危害的每个人。

每个人都在谈论现代和/或旧式操作系统。

但是,如果我只是在没有操作系统的环境中怎么办?那里什么都没有?

想象一下,现在您正在使用线程样式的中断并分配内存。在C标准ISO / IEC:9899中,内存的生存期表示为:

7.20.3内存管理功能

1连续调用calloc,malloc和realloc函数分配的存储的顺序和连续性未指定。如果分配成功,则返回的指针将进行适当对齐,以便可以将其分配给指向任何类型对象的指针,然后将其用于访问分配的空间中的此类对象或此类对象的数组(直到明确释放该空间为止) 。分配对象的生命周期从分配一直到释放。[...]

因此,不必认为环境正在为您完成释放工作。否则,它将被添加到最后一句话:“或者直到程序终止。”

因此,换句话说:不释放内存不仅仅是不好的做法。它产生不可移植且不符合C规范的代码。至少可以将其视为“正确的,如果以下情况:[...]受环境支持”。

但是在根本没有操作系统的情况下,没有人为您做这项工作(我知道您通常不会在嵌入式系统上分配和重新分配内存,但是在某些情况下,您可能想要这样做。)

因此,以普通的普通C语言(OP被标记)来说,这只是在产生错误且不可移植的代码。


4
一个反驳的观点是,如果您是嵌入式环境,那么作为开发人员的您首先将在内存管理方面更加严格。通常,这实际上是预先预先分配静态固定内存,而不是完全使用任何运行时malloc / realloc。
John Go-Soco

1
@lunarplasma:尽管您说的话并没有错,但这并不能改变语言标准所陈述的事实,任何反对/遵循标准的人,甚至可能是常识,都在编写有限的代码。我可以理解是否有人说“我不必在乎”,因为在很多情况下都可以。但是至少应该知道为什么他不必在乎。尤其是只要问题与该特殊情况无关就不要忽略它。而且由于OP在理论(学校)方面一般都在问C。说“不需要”是不行的!
dhein

2
在大多数没有操作系统的环境中,没有办法可以“终止”程序。
超级猫

@supercat:正如我之前写的:您是对的。但是,如果有人问起有关教学原因和学校方面的问题,那么就说“在大多数情况下都不需要考虑它”是不正确的。语言的措辞和行为给出定义是有原因的,并且仅仅因为大多数环境都会为您处理它,所以您不能说没有必要在意。那就是我的意思。
dhein

2
-1用于引用C标准,而大多数情况下它在没有操作系统的情况下不适用,因为没有运行时来提供标准命令所必需的功能,尤其是在内存管理和标准库功能方面(显然也没有)以及运行时/ OS)。

23

一旦确定已完成,通常我就释放每个分配的块。今天,我程序的入口点可能是main(int argc, char *argv[]),但是明天它可能被foo_entry_point(char **args, struct foo *f)输入为函数指针。

所以,如果发生这种情况,我现在有一个漏洞。

关于第二个问题,如果我的程序接受a = 5之类的输入,我会为a分配空间,或者在后续的a =“ foo”上重新分配相同的空间。这将一直分配到:

  1. 用户键入“取消设置”
  2. 输入了我的清理功能,用于处理信号或用户键入“退出”

我想不出有任何现代 OS在进程退出后不会回收内存。再说一次,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
是的,我曾经有一个队友告诉我:“我永远不需要在main()中调用free()” <shudders>
Tim Post

free() is cheap除非您有十亿个具有复杂关系的数据结构,否则必须逐个释放,否则遍历该数据结构以尝试释放所有内容可能最终会显着增加关闭时间,尤其是如果该数据结构的一半已经分页出来到磁盘上,没有任何好处。
Lie Ryan

3
@LieRyan如果您有十亿,从字面上十亿的结构来看,您肯定会遇到其他需要专门考虑的问题-超出了此特定答案的范围:)
Tim Post

13

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

话虽这么说,人们仍然坚持认为最好在退出前释放所有内容的一个原因是内存调试器(例如Linux上的valgrind)将未释放的块检测为内存泄漏,并且如果您还存在“真实的”内存泄漏,它将变成如果最后还得到“伪造”结果,则更难发现它们。


1
Valgrind难道不能很好地区分“泄漏”和“仍可到达”吗?
Christoffer

11
-1(表示“完全正常”)不好的编码习惯是留下分配的内存而不释放它。如果将这些代码提取到库中,则将导致整个地方的内存泄漏。
DevinB,2009年

5
+1补偿。请参阅compie的答案。freeexit当时被认为是有害的。
R .. GitHub停止帮助ICE

11

如果您使用的是已分配的内存,那么您就没有做错任何事情。当您编写函数(而不是main)来分配内存而不释放内存,并且也无法将其提供给程序的其余部分时,这将成为问题。然后,您的程序将在分配了该内存的情况下继续运行,但无法使用它。您的程序和其他正在运行的程序被剥夺了该内存。

编辑:说其他正在运行的程序被剥夺了内存并不是100%准确。操作系统始终可以让他们使用它,而代价是将程序换出到虚拟内存(</handwaving>)。但是,要点是,如果程序释放了未使用的内存,则不太可能需要进行虚拟内存交换。


11

该代码通常可以正常工作,但是要考虑代码重用的问题。

您可能已经编写了一些不能释放已分配内存的代码片段,该代码片段的运行方式是可以自动回收内存。似乎还好。

然后有人将您的代码段以每秒执行一千次的方式复制到他的项目中。该人现在在他的程序中存在巨大的内存泄漏。通常来说不是很好,通常对于服务器应用程序是致命的。

代码重用在企业中很典型。通常,公司拥有员工生产的所有代码,每个部门都可以重复使用公司拥有的任何代码。因此,通过编写这种“看起来很天真”的代码,您可能会引起其他人的头痛。这可能会让您被解雇。


2
可能值得一提的是,不仅有人复制了代码片段,而且编写的程序一旦被修改以重复执行它,就可能执行某些特定操作的可能性。在这种情况下,一次分配内存然后在不释放的情况下重复使用它会很好,但是为每个操作分配和放弃内存(不释放它)可能是灾难性的。
超级猫

7

真正的结果是什么?

您的程序泄漏了内存。根据您的操作系统,它可能已恢复。

大多数现代台式机操作系统的确会在进程终止时恢复泄漏的内存,令人遗憾的是,忽略该问题很普遍,如此处的许多其他答案所示。)

但是,您依赖的是不应使用的安全功能,并且您的程序(或功能)可能会在下次此行为确实导致“硬”内存泄漏的系统上运行。

您可能正在内核模式下运行,或者在未采用内存保护作为代价的老式/嵌入式操作系统上运行。(MMU占用了模具空间,内存保护花费了额外的CPU周期,而要求程序员自己清理也不是太多)。

您可以按自己喜欢的任何方式使用和重复使用内存,但是在退出之前,请确保已释放所有资源。


5

OSTEP在线教科书中实际上有一个针对操作系统本科课程的部分,它确切地讨论了您的问题。

相关部分是第6页上的Memory API章节中的“忘记释放内存”,其中给出了以下说明:

在某些情况下,似乎不调用free()是合理的。例如,您的程序寿命很短,将很快退出;在这种情况下,当进程终止时,操作系统将清理所有分配的页面,因此,不会发生内存泄漏。尽管这肯定是“有效的”(请参阅​​第7页的旁白),但养成这种习惯可能是个坏习惯,因此请谨慎选择这种策略

此摘录是在介绍虚拟内存的概念的上下文中。基本上,在这本书的这一点上,作者解释说,操作系统的目标之一是“虚拟化内存”,即让每个程序都相信它可以访问很大的内存地址空间。

在幕后,操作系统会将用户看到的“虚拟地址”转换为指向物理内存的实际地址。

但是,共享资源(例如物理内存)要求操作系统跟踪正在使用哪些进程。因此,如果进程终止,则回收操作系统的内存在操作系统的能力和设计目标之内,以便它可以重新分配内存并与其他进程共享。


编辑:摘录中提到的一旁复制如下。

旁白为什么程序退出后不会留下记忆

在编写短期程序时,您可以使用分配一些空间malloc()。该程序将运行并即将完成:是否需要free()在退出前多次调用?尽管这样做似乎是错误的,但实际上任何内存都不会“丢失”。原因很简单:系统中实际上有两个级别的内存管理。操作系统执行内存管理的第一级,操作系统在运行时将内存分配给进程,并在进程退出(或以其他方式终止)时将其收回。第二层管理在每个进程内,例如,在调用 malloc()free()。即使你没打电话free()(并因此导致堆中的内存泄漏),操作系统将在程序完成运行时回收进程的所有内存(包括代码,堆栈和相应的堆页面)。无论您的地址空间中的堆状态如何,当进程终止时,操作系统都会收回所有这些页面,从而确保即使您没有释放它也不会丢失任何内存。

因此,对于寿命短的程序,内存泄漏通常不会引起任何操作问题(尽管它可能被认为是较差的形式)。当您编写长时间运行的服务器(例如Web服务器或数据库管理系统,这些服务器永不退出)时,内存泄漏是一个更大的问题,当应用程序内存不足时,最终将导致崩溃。当然,内存泄漏是一个特定程序(操作系统本身)内更大的问题。再次向我们展示:编写内核代码的人工作最艰苦……

从第7页的“ 内存API”一章

操作系统:三本简单的书
Remzi H. Arpaci-Dusseau和Andrea C. Arpaci-Dusseau Arpaci-Dusseau书籍2015年3月(0.90版)


4

不释放变量并没有真正的危险,但是如果在不释放第一个块的情况下将指向一个内存块的指针分配给另一个内存块,则第一个块将不再可访问,但仍会占用空间。这就是所谓的内存泄漏,如果您定期执行此操作,则您的进程将开始消耗越来越多的内存,从而从其他进程中夺走系统资源。

如果该进程是短暂的,那么您通常可以避免这样做,因为在该进程完成时,操作系统会回收所有分配的内存,但是我建议您养成释放不再使用的所有内存的习惯。


1
我想对第一个陈述“没有危险”说-1,除了您然后对为什么会有危险给出一个深思熟虑的答案。
DevinB,2009年

2
随着危险的到来,它是相当不错的-我每天都会通过段错误来处理内存泄漏问题。
凯尔·克罗宁

1
非常正确,我们俩都
不愿意

2
@KyleCronin我会,而有段错误不是内存泄漏,因为两者都是严重的错误和段错误更容易被发现。通常,内存泄漏是“非常良性的”,因此往往不会引起注意或无法解决。我和我的RAM完全不同意。
Dan Bechard

@Dan作为开发人员,当然。作为用户,我将处理内存泄漏问题。我宁愿有个可以工作的软件,尽管有内存泄漏的问题,但要比不能工作的软件有效。
凯尔·克罗宁

3

您在这方面绝对正确。在琐碎的小型程序中,必须有一个变量,直到程序终止运行,释放内存并没有真正的好处。

实际上,我曾经参与过一个项目,该项目的每次执行都非常复杂,但是寿命相对较短,因此决定只是保持内存分配,而不会通过使分配错误而使项目不稳定。

话虽这么说,在大多数程序中这并不是一个真正的选择,否则它可能会导致您内存不足。


2

没错,进程退出时内存会自动释放。有些人力争在进程终止时不进行大量清理,因为它将全部放弃给操作系统。但是,在程序运行时,应释放未使用的内存。否则,如果工作集太大,最终可能会耗尽或导致过多的页面调度。


2

如果您是从头开始开发应用程序,则可以对何时免费拨打电话做出一些明智的选择。您的示例程序很好:它分配了内存,也许您让它工作了几秒钟,然后关闭了,释放了它声称的所有资源。

但是,如果您要编写其他任何东西-服务器/长时间运行的应用程序或供其他人使用的库,则应该期望对malloc的所有内容进行免费调用。

忽略实用主义的一秒钟,遵循更严格的方法会更安全,并强迫自己释放您分配的所有内容。如果您不习惯在每次编写代码时都注意内存泄漏,则可以轻松地引发一些泄漏。因此,换句话说,是的-没有它,您可以逃脱。不过请小心。


0

如果程序在退出之前忘记释放几兆字节,则操作系统将释放它们。但是,如果您的程序一次运行数周,并且程序内部的循环忘记每次迭代都释放几个字节,则可能会发生巨大的内存泄漏,除非您定期重新启动它,否则它将吞噬计算机中的所有可用内存。基础=>即使程序最初用于某个大型任务,即使很小的内存泄漏也可能是很糟糕的。


-2

我认为您的两个示例实际上只是一个例子:free()应该仅在流程结束时发生,正如您指出的那样,因为流程正在终止,所以这是没有用的。

但是,在第二个示例中,唯一的区别是您允许未定义数量的malloc(),这可能会导致内存不足。处理这种情况的唯一方法是检查的返回码malloc()并采取相应的措施。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.