我们都被教导必须释放每个分配的指针。不过,我有点好奇不释放内存的实际成本。在某些明显的情况下,例如在循环或线程执行的一部分中调用 malloc()
时,释放非常重要,因此不会出现内存泄漏。但请考虑以下两个示例:
首先,如果我有这样的代码:
int main()
{
char *a = malloc(1024);
/* Do some arbitrary stuff with 'a' (no alloc functions) */
return 0;
}
这里的真正结果是什么?我的想法是进程终止,然后堆空间无论如何都消失了,因此错过对 free
的调用并没有什么坏处(但是,我确实认识到无论如何都有它对于关闭、可维护性和良好实践的重要性)。我的这个想法是对的吗?
其次,假设我有一个有点像 shell 的程序。用户可以声明像 aaa = 123
这样的变量,这些变量存储在一些动态数据结构中供以后使用。显然,很明显您会使用一些会调用某些 *alloc 函数(散列图、链表等)的解决方案。对于这种程序,在调用 malloc
后释放是没有意义的,因为这些变量必须在程序执行期间始终存在,并且没有好的方法(我可以看到)通过静态分配来实现它空间。分配一堆内存但仅作为进程结束的一部分释放是不是糟糕的设计?如果是这样,有什么替代方案?
free(a)
实际上并没有真正释放内存!它只是重置了 malloc 的 libc 实现中的一些指针,这些指针跟踪一个大的 mmaped 内存页面(通常称为“堆”)内的可用内存块。该页面仍然只会在您的程序终止时被释放,而不是之前。
malloc()
的内存来自“正常”的 sbrk 堆,并且在其末尾,则调用 sbrk()
以减少内存映像。如果 malloc()
通过 mmap()
分配内存,则它在 free()
中未映射。
几乎每个现代操作系统都会在程序退出后恢复所有分配的内存空间。我能想到的唯一例外可能是 Palm OS,其中程序的静态存储和运行时内存几乎相同,因此不释放可能会导致程序占用更多存储空间。 (我只是在这里推测。)
因此,一般来说,它没有任何害处,除了存储空间超过您需要的运行时间成本。当然,在您给出的示例中,您希望保留可能使用的变量的内存,直到它被清除。
但是,当您不再需要它时立即释放内存并在程序退出时释放您仍然存在的任何东西,这被认为是一种很好的风格。这更像是一种了解您正在使用的内存的练习,并考虑您是否仍然需要它。如果你不跟踪,你可能有内存泄漏。
另一方面,在退出时关闭文件的类似警告会产生更具体的结果——如果你不这样做,你写给它们的数据可能不会被刷新,或者如果它们是临时文件,它们可能不会完成后被删除。此外,数据库句柄应该提交它们的事务,然后在你完成它们时关闭它们。同样,如果您使用的是 C++ 或 Objective C 等面向对象的语言,则在完成对象后不释放对象将意味着永远不会调用析构函数,并且该类负责的任何资源都可能不会被清理。
是的,您是对的,您的示例不会造成任何伤害(至少在大多数现代操作系统上不会)。一旦进程退出,您的进程分配的所有内存将由操作系统恢复。
来源:Allocation and GC Myths(PostScript 警报!)
分配误区 4:非垃圾回收程序应始终释放它们分配的所有内存。真相:在频繁执行的代码中省略释放会导致越来越多的泄漏。他们很少被接受。但是在程序退出之前保留大部分分配内存的程序通常在没有任何干预解除分配的情况下执行得更好。如果没有免费的,Malloc 更容易实现。在大多数情况下,在程序退出之前释放内存是没有意义的。无论如何,操作系统都会收回它。自由会在死物中触摸和翻页;操作系统不会。结果:小心计算分配的“泄漏检测器”。一些“泄漏”是好的!
也就是说,您真的应该尽量避免所有内存泄漏!
第二个问题:你的设计没问题。如果您需要在应用程序退出之前存储某些内容,那么可以使用动态内存分配来执行此操作。如果您事先不知道所需的大小,则不能使用静态分配的内存。
=== 未来的证明和代码重用呢? ===
如果您不编写代码来释放对象,那么您将代码限制为仅当您可以依赖被关闭的进程释放的内存时才能安全使用......即一次性使用项目或“丢弃”[1] 项目)......您知道该过程何时结束。
如果您确实编写了释放()所有动态分配的内存的代码,那么您将在未来对代码进行校对并让其他人在更大的项目中使用它。
[1] 关于“一次性”项目。 “一次性”项目中使用的代码有一种不会被丢弃的方式。接下来你知道十年过去了,你的“丢弃”代码仍在使用)。
我听说一个人写了一些代码只是为了好玩,以使他的硬件更好地工作。他说“just a hobby, won't be big and professional”。多年后,很多人都在使用他的“爱好”代码。
malloc()
初始化它,如果指针仍然为空则终止,即使 free
也可以安全地使用这样的函数任意次数永远不会被调用。我认为区分可能耗尽无限量存储的内存泄漏与只能浪费有限且可预测的存储量的情况可能是值得的。
null
,并且在存在分配时为非空,则在销毁上下文时让代码释放分配并将指针设置为 null
将很简单,尤其是与其他所有内容相比这需要将静态对象移动到上下文结构中。
你是对的,没有伤害,退出速度更快
这有多种原因:
所有桌面和服务器环境都只是在 exit() 上释放整个内存空间。他们不知道程序内部的数据结构,例如堆。
几乎所有的 free() 实现都不会将内存返回给操作系统。
更重要的是,在 exit() 之前完成是浪费时间。在退出时,内存页面和交换空间被简单地释放。相比之下,一系列 free() 调用会消耗 CPU 时间,并可能导致磁盘分页操作、缓存未命中和缓存驱逐。
关于未来代码重用的可能性,证明无意义操作的确定性:这是一个考虑因素,但可以说不是Agile方式。 YAGNI!
exit()
之前调用 free()
的路径:当程序发展时,exit()
不一定保留在程序的末尾。因此,“敏捷”论点是“视情况而定”。
我完全不同意所有说 OP 正确或没有害处的人。
每个人都在谈论现代和/或传统操作系统。
但是,如果我在一个根本没有操作系统的环境中呢?什么地方没有?
想象一下,现在您正在使用线程样式的中断并分配内存。在 C 标准 ISO/IEC:9899 中,内存的生命周期表示为:
7.20.3 内存管理函数 1 连续调用 calloc、malloc 和 realloc 函数分配的存储顺序和连续性是未指定的。如果分配成功,则返回的指针经过适当对齐,以便可以将其分配给指向任何类型对象的指针,然后用于访问已分配空间中的此类对象或此类对象的数组(直到空间被显式释放) .已分配对象的生命周期从分配延伸到解除分配。[...]
因此,不必认为环境正在为您完成释放工作。否则它将被添加到最后一句:“或者直到程序终止。”
换句话说:不释放内存不仅仅是不好的做法。它产生不可移植且不符合 C 的代码。至少可以将其视为“正确的,如果以下内容:[...],受环境支持”。
但是在您根本没有操作系统的情况下,没有人会为您完成这项工作(我知道通常您不会在嵌入式系统上分配和重新分配内存,但在某些情况下您可能想要这样做。)
因此,一般来说,普通的 C 语言(OP 被标记),这只是产生错误和不可移植的代码。
一旦我确定我完成了它,我通常会释放每个分配的块。今天,我的程序的入口点可能是 main(int argc, char *argv[])
,但明天它可能是 foo_entry_point(char **args, struct foo *f)
并输入为函数指针。
所以,如果发生这种情况,我现在有泄漏。
关于你的第二个问题,如果我的程序接受像 a=5 这样的输入,我会为 a 分配空间,或者在随后的 a="foo" 上重新分配相同的空间。这将一直分配到:
用户输入了“unset a”我的清理功能被输入,服务信号或用户输入“退出”
我想不出任何在进程退出后不回收内存的现代操作系统。再说一次,free() 很便宜,为什么不清理呢?正如其他人所说,像 valgrind 这样的工具非常适合发现您确实需要担心的泄漏。即使您示例的块将被标记为 'still reachable' ,当您试图确保没有泄漏时,它只是输出中的额外噪音。
另一个神话是“如果它在 main() 中,我不必释放它”,这是不正确的。考虑以下:
char *t;
for (i=0; i < 255; i++) {
t = strdup(foo->name);
let_strtok_eat_away_at(t);
}
如果这发生在分叉/守护进程之前(并且理论上永远运行),那么您的程序刚刚泄漏了 t 255 次的未确定大小。
一个好的,写得好的程序应该总是在自己之后清理。释放所有内存,刷新所有文件,关闭所有描述符,取消链接所有临时文件等。此清理功能应在正常终止或收到各种致命信号时达到,除非您想留下一些文件以便您可以检测崩溃并恢复。
真的,善待可怜的灵魂,当你继续做其他事情时,他们必须维护你的东西..把它交给他们'valgrind clean':)
free() is cheap
除非您有 10 亿个具有复杂关系的数据结构必须一个一个释放,否则遍历数据结构以尝试释放所有内容可能最终会显着增加您的停机时间,尤其是如果该数据结构的一半是已经分页到磁盘,没有任何好处。
退出时不释放内存是完全可以的; malloc() 从称为“堆”的内存区域分配内存,当进程退出时,进程的完整堆被释放。
话虽如此,人们仍然坚持在退出之前释放所有内容是好的一个原因是内存调试器(例如 Linux 上的 valgrind)将未释放的块检测为内存泄漏,如果您也有“真正的”内存泄漏,它变成如果您最后也得到“假”结果,则更难发现它们。
free
在 exit
时间被认为是有害的。
如果您正在使用已分配的内存,那么您没有做错任何事情。当您编写分配内存而不释放内存且不使其对程序的其余部分可用的函数(除 main 之外)时,这将成为一个问题。然后您的程序继续运行分配给它的内存,但无法使用它。您的程序和其他正在运行的程序被剥夺了该内存。
编辑:说其他正在运行的程序被剥夺了内存并不是 100% 准确的。操作系统总是可以让他们使用它,但代价是将您的程序换出到虚拟内存 (</handwaving>
)。但是,关键是,如果您的程序释放了它不使用的内存,那么虚拟内存交换就不太可能是必要的。
这段代码通常可以正常工作,但要考虑代码重用的问题。
您可能已经编写了一些不释放分配的内存的代码片段,它以这样的方式运行,然后自动回收内存。似乎没事。
然后其他人以每秒执行一千次的方式将您的代码段复制到他的项目中。那个人现在在他的程序中有一个巨大的内存泄漏。一般来说不是很好,对于服务器应用程序通常是致命的。
代码重用在企业中很常见。通常,公司拥有其员工编写的所有代码,每个部门都可以重复使用公司拥有的任何代码。因此,通过编写这种“看起来很无辜”的代码,您可能会令其他人头疼。这可能会让你被解雇。
这里的真正结果是什么?
你的程序泄露了内存。根据您的操作系统,它可能已被恢复。
大多数现代桌面操作系统确实会在进程终止时恢复泄漏的内存,因此很遗憾地忽略了这个问题(正如这里的许多其他答案所看到的那样。)
但是您依赖的是您不应该依赖的安全功能,并且您的程序(或函数)可能在下一次这种行为确实导致“硬”内存泄漏的系统上运行。
您可能在内核模式下运行,或者在不采用内存保护作为权衡的老式/嵌入式操作系统上运行。 (MMU占用die空间,内存保护需要额外的CPU周期,要求程序员自己清理也不算多)。
您可以以任何您喜欢的方式使用和重用内存,但请确保在退出之前释放所有资源。
OSTEP 操作系统本科课程在线教科书中实际上有一个部分准确地讨论了您的问题。
相关部分是第 6 页 Memory API chapter 中的“忘记释放内存”,其中给出了以下解释:
在某些情况下,似乎不调用 free() 是合理的。例如,你的程序是短暂的,很快就会退出;在这种情况下,当进程终止时,操作系统将清理其所有分配的页面,因此本身不会发生内存泄漏。虽然这肯定“有效”(参见第 7 页的旁注),但养成这种习惯可能是个坏习惯,所以要小心选择这样的策略
这段摘录是在介绍虚拟内存概念的背景下进行的。基本上在本书的这一点上,作者解释说操作系统的目标之一是“虚拟化内存”,也就是说,让每个程序相信它可以访问一个非常大的内存地址空间。
在幕后,操作系统会将用户看到的“虚拟地址”转换为指向物理内存的实际地址。
但是,共享物理内存等资源需要操作系统跟踪正在使用它的进程。因此,如果一个进程终止,那么操作系统的能力和设计目标就是回收该进程的内存,以便它可以重新分配并与其他进程共享内存。
编辑:摘录中提到的旁白复制如下。
旁白:为什么一旦你的进程退出就没有内存泄漏当你编写一个短暂的程序时,你可能会使用 malloc() 分配一些空间。程序运行并即将完成:在退出之前是否需要多次调用 free() ?虽然不这样做似乎是错误的,但在任何真正意义上都不会“丢失”任何记忆。原因很简单:系统中确实有两个级别的内存管理。第一级内存管理由操作系统执行,它在进程运行时将内存分配给进程,并在进程退出(或以其他方式死亡)时收回。第二级管理在每个进程内,例如在调用 malloc() 和 free() 时在堆内。即使您未能调用 free()(因此在堆中泄漏内存),操作系统也会在程序执行时回收进程的所有内存(包括代码页、堆栈以及此处相关的堆)运行完毕。无论您的地址空间中的堆处于何种状态,操作系统都会在进程终止时收回所有这些页面,从而确保即使您没有释放内存也不会丢失内存。因此,对于短期程序,内存泄漏通常不会导致任何操作问题(尽管它可能被认为是不良形式)。当您编写一个长时间运行的服务器(例如永远不会退出的 Web 服务器或数据库管理系统)时,内存泄漏是一个更大的问题,并且最终会在应用程序耗尽内存时导致崩溃。当然,内存泄漏是一个特定程序内部的一个更大的问题:操作系统本身。再次向我们展示:编写内核代码的人是所有工作中最艰巨的工作......来自操作系统的内存 API 章节的第 7 页:三个简单的部分 Remzi H. Arpaci-Dusseau 和 Andrea C. Arpaci-Dusseau Arpaci-Dusseau图书 2015 年 3 月(0.90 版)
不释放变量并没有真正的危险,但是如果将指向内存块的指针分配给不同的内存块而不释放第一个块,则第一个块不再可访问但仍占用空间。这就是所谓的内存泄漏,如果您经常这样做,那么您的进程将开始消耗越来越多的内存,从而从其他进程中夺走系统资源。
如果该过程是短暂的,您通常可以避免这样做,因为当该过程完成时,操作系统会回收所有分配的内存,但我建议养成释放所有您不再使用的内存的习惯。
您是对的,进程退出时会自动释放内存。有些人力求在进程终止时不进行大量清理,因为它将全部交给操作系统。但是,当您的程序运行时,您应该释放未使用的内存。如果你不这样做,如果你的工作集变得太大,你最终可能会耗尽或导致过度分页。
你在这方面是绝对正确的。在小型琐碎程序中,变量必须存在直到程序死亡,释放内存没有真正的好处。
事实上,我曾经参与过一个项目,其中程序的每次执行都非常复杂,但寿命相对较短,我决定只保持分配的内存,而不是因为释放错误而破坏项目的稳定性。
话虽如此,在大多数程序中,这并不是一个真正的选择,否则它会导致您耗尽内存。
如果您是从头开始开发应用程序,您可以就何时免费调用做出一些明智的选择。您的示例程序很好:它分配内存,也许您让它工作几秒钟,然后关闭,释放它声称的所有资源。
但是,如果您正在编写其他任何东西——一个服务器/长时间运行的应用程序,或者一个供其他人使用的库,您应该期望在您 malloc 的所有内容上调用 free。
暂时忽略务实的一面,遵循更严格的方法会更安全,并强迫自己释放所有你 malloc 的东西。如果您没有在编写代码时观察内存泄漏的习惯,那么您很容易就会出现一些泄漏。所以换句话说,是的——没有它你可以逃脱;不过请小心。
这取决于您正在从事的项目的范围。在你的问题的背景下,我的意思是你的问题,那么没关系。
为了进一步解释(可选),我从整个讨论中注意到的一些场景如下:
(1) - 如果您在嵌入式环境中工作,您不能依赖主操作系统为您回收内存,那么您应该释放它们,因为如果不注意内存泄漏,真的会导致程序崩溃。
(2) - 如果您正在从事一个不会向其他人透露的个人项目,那么您可以跳过它(假设您在主操作系统上使用它)或将其包含在“最佳实践”中清酒。
(3) - 如果您正在开发一个项目并计划将其开源,那么您需要对您的受众进行更多研究,并确定释放内存是否是更好的选择。
(4) - 如果您有一个大型库并且您的受众仅包含主操作系统,那么您不需要释放它,因为他们的操作系统会帮助他们这样做。同时,通过不释放,您的库/程序可能有助于提高整体性能,因为程序不必关闭每个数据结构,从而延长关闭时间(想象一下在离开前关闭计算机的非常缓慢的痛苦等待房子...)
我可以继续指定要学习的课程,但这最终取决于您希望通过您的程序实现什么。在某些情况下,释放内存被认为是一种很好的做法,但在某些情况下并不是那么好,因此它最终取决于您所处的具体情况并在正确的时间提出正确的问题。祝你好运!
如果程序在退出之前忘记释放几兆字节,操作系统将释放它们。但是,如果您的程序一次运行数周,并且程序内部的循环忘记在每次迭代中释放几个字节,那么您将发生严重的内存泄漏,除非您定期重新启动它,否则它将耗尽您计算机中的所有可用内存base => 如果程序用于一项非常大的任务,即使它最初不是为一个任务设计的,即使是很小的内存泄漏也可能是不好的。
正如其他人已经指出的那样,这取决于程序运行的操作系统环境,对于长时间运行的进程,释放内存并避免甚至非常缓慢的泄漏始终很重要。但是,如果操作系统处理一些东西,就像 Unix 可能永远做的那样,那么你不需要释放内存,也不需要关闭文件(当进程退出时,内核会关闭所有打开的文件描述符。)如果你的程序分配大量的内存,甚至可以毫不犹豫地退出。我发现当我退出 Firefox 时,它会花费几分钟!在许多进程中以千兆字节的内存进行分页。我猜这是由于必须在 C++ 对象上调用析构函数。这实际上很可怕。有些人可能会争辩说,这对于一致地保存状态是必要的,但在我看来,浏览器、编辑器和设计程序等长期运行的交互式程序(仅举几例)应该确保任何状态信息、首选项、打开的窗口/页面,文档等经常被写入永久存储,以避免在崩溃的情况下丢失工作。然后,当用户选择退出时,可以再次快速执行此状态保存,完成后,进程应该立即退出。
我认为您的两个示例实际上只是一个:free()
应该仅在流程结束时出现,正如您指出的那样,由于流程正在终止,所以这没有用。
但是,在第二个示例中,唯一的区别是您允许未定义数量的 malloc()
,这可能导致内存不足。处理这种情况的唯一方法是检查 malloc()
的返回码并采取相应措施。
不定期副业成功案例分享