在分配的内存上,是否有时可以不使用free()?

83
我正在学习计算机工程,学了一些电子课程。我从两位教授(这些课程的教授)那里听说过,可以避免使用free()函数(在使用malloc()calloc()等之后),因为分配的内存空间可能不会再被用来分配其他内存。 也就是说,例如,如果您分配了4个字节,然后释放了它们,您将有一个不太可能再次分配的4个字节的空间:您将拥有一个“空洞”。
我认为这很疯狂:你不能有一个真正的程序,在堆上分配内存而不释放它。但是我没有足够的知识来解释为什么对于每个malloc()都必须有一个free()是如此重要。
那么,是否有情况可以在不使用free()的情况下使用malloc()?如果没有,我该如何向我的教授解释?
答:一般情况下,每个malloc()都必须有一个相应的free()来释放已分配的内存,否则会导致内存泄漏。在某些情况下,比如在进程结束时,操作系统会自动回收堆上的内存,但这并不是一个可靠的做法。因此,为了确保程序的正确性和健壮性,在堆上分配内存后必须使用free()释放它们。

12
他们并不是“错误的”——他们对于非常小的孤立自由地区的分割有一个有效的(尽管有限)观点,并且可能比你所报告的要更加仔细地陈述了这一观点。 - Chris Stratton
18
@Marian :我曾经有一位教授声称,在 C 和 C++ 中,需要在与分配内存相同的 .c/.cxx 文件中定义的函数中释放已分配的内存……这些人有时似乎严重患有缺氧症,因为他们生活在象牙塔中太高了。 - PlasmaHH
4
有相当数量的非玩具程序不会释放内存,在进程退出时让操作系统清理所有内存比(繁琐地)保留大量簿记以便自行处理要快得多。 - Donal Fellows
2
正如@mfro所指出的那样,free()不会将内存返回给操作系统,但仍会标记为malloc()重用。这将推迟对mmap()/sbrk()的未来调用,并节省系统内存。一旦整个页面未使用,它们将被分页。然而,长期运行的进程应该解决这个问题并实际将内存返回给操作系统(使用mmap()brk()更容易,但重新启动进程要容易得多)。这就是开发和运营需要理解其影响的地方。 - Henk Langeveld
9
不要毫无怀疑地接受你听到的内容。我曾经遇到很多老师、演讲者和校正者是错误或过时的。一定要非常精确地分析他们所说的话。我们的民族通常非常精确,可能会说正确的话,但某些人如果只擅长于日常用语,很容易理解错误或以错误的优先级理解。例如,我记得在学校有一位老师问:“你做作业了吗?”我回答:“没有。”虽然我是正确的,但是老师觉得这样回答很冒犯,因为我省去了找借口的时间,这是他意料之外的。 - Sebastian Mach
显示剩余8条评论
11个回答

100

简单来说,只需阅读任何半认真实现malloc()/free()的源代码即可了解。我的意思是处理调用工作的实际内存管理器,这可能位于运行时库、虚拟机或操作系统中。当然,并非所有情况下都能轻松获取源代码。

确保内存不会碎片化,通过将相邻的空洞合并成更大的空洞非常非常普遍。更严格的分配器使用更严密的技术来确保这一点。

因此,让我们假设您进行了三次分配和释放,并按以下顺序在内存中布置块:

+-+-+-+
|A|B|C|
+-+-+-+

个别分配的大小并不重要。然后你释放第一个和最后一个,即A和C:

+-+-+-+
| |B| |
+-+-+-+

当您最终释放B时,您(至少在理论上)最终得到:

+-+-+-+
| | | |
+-+-+-+

可以被整理成只有

+-+-+-+
|     |
+-+-+-+

即一个较大的空闲块,不会留下任何碎片。

参考资料,如所请求:


1
你能给我提供一些参考资料吗? - Nick
4
值得一提的是,虚拟地址空间并不直接代表物理内存。物理内存的碎片问题可以由操作系统解决,而进程未释放的虚拟内存也不会被实际释放。 - lapk
@PetrBudnik,虚拟内存一对一映射到物理内存的情况很少见,操作系统会考虑页面映射,并能够轻松地进行换页。 - ratchet freak
3
非常挑剔的评论:虽然概念讲解得很清楚,但实际的例子选择有点不太恰当。如果有人查看dlmalloc等源代码并感到困惑:特定大小以下的块始终是2的幂,并按照此进行合并/分裂。因此,我们可能会得到一个8字节块和一个4字节块,但没有12字节块。这在至少在桌面上是分配器的一种标准方法,尽管嵌入式应用程序可能会更加谨慎地处理它们的开销。 - Voo
只是为了澄清@Voo的答案,编译器分配2的幂次方字节数而不是您要求的值的原因是为了减少碎片化。我不完全确定大多数编译器在没有设置-O标志的情况下会做什么,但是如果设置了它,它肯定会分配一个2的幂次方而不是您要求的数量。请参见https://dev59.com/LHA75IYBdhLWcg3wo6pe - Hoffmann
显示剩余2条评论

43
其他答案已经很好地解释了实现malloc()和free()的程序确实会将零散块合并成更大的可用空间。但即使没有这种情况发生,放弃使用free()仍然是不明智的。
问题是,你的程序刚刚分配(并想要释放)那4个字节的内存。如果它将长时间运行,很可能需要再次分配只有4个字节的内存。因此,即使这4个字节永远不会合并成更大的连续空间,它们仍然可以被程序本身再次使用。

7
没错。问题是,如果free被调用的次数足以对性能产生影响,那么它很可能也被调用了足够多次,以至于不使用它将大大减少可用内存。很难想象一个嵌入式系统的情况,其中由于free而导致性能持续下降,但只有有限次调用malloc。这是一种相当罕见的用例,因为嵌入式设备通常需要对数据进行一次性处理,然后重置。 - Jason C

10

这纯属无稽之谈,例如malloc有很多不同的实现,有些尝试使堆更有效率,例如Doug Lea的或者这个


9
你的教授是否有使用POSIX技术?如果他们习惯于编写许多小型、极简的Shell应用程序,那么我可以想象这种方法可能并不太糟糕 - 在操作系统闲暇时一次性释放整个堆比释放一千个变量要快。如果你期望你的应用程序运行一两秒钟,你甚至可以完全不进行释放。
当然,这仍然是一个不好的做法(性能改进应该始终基于分析而非模糊的直觉),而且这不是你应该在没有解释其他约束条件的情况下告诉学生的事情,但我可以想象很多微型管道Shell应用程序都是以这种方式编写的(如果不是直接使用静态分配)。如果你正在从中受益,那么要么你正在极端低延迟条件下工作(在这种情况下,你如何才能负担得起动态分配和C++? :D),要么你正在做某些非常非常错误的事情(例如通过一个接一个地分配一千个整数来分配整数数组,而不是单个内存块)。

让操作系统在结束时释放所有内容不仅能提高性能,而且需要更少的逻辑来使其正常工作。 - hugomg
@missingno 换句话说,它让你摆脱了内存管理的复杂性 :) 然而,我认为这是反对非托管语言的一个论点 - 如果你的原因是复杂的释放逻辑而不是性能,那么使用一个为你处理它的语言/环境可能更好。 - Luaan

5

你提到他们是电子学教授。他们可能习惯于编写固件/实时软件,需要能够准确计时某些执行操作。在这些情况下,知道你有足够的内存分配并且不释放和重新分配内存,可以给出一个更容易计算的执行时间限制。

在某些方案中,硬件内存保护也可以用于确保例程在其分配的内存中完成或在应该非常特殊的情况下生成陷阱异常。


10
这是一个好观点。然而,如果这种情况下他们不会使用malloc和类似的函数,我会期望他们会依靠静态分配(或者分配一个大块内存,然后手动处理内存)。 - Luaan

2

从程序员的角度来看,我认为问题陈述如果字面理解是无意义的,但从操作系统的角度来看,它确实有一定的真实性。

malloc()最终将调用mmap()或sbrk(),从操作系统中获取一个页面。

在任何非平凡的程序中,即使您释放了大部分分配的内存,这个页面在进程生命周期内被归还给操作系统的机会非常小。所以free()的内存大多数情况下仅对同一进程可用,而不是其他进程。


2
你的教授并没有错,但他们可能有些误导或过于简化。内存碎片化会影响性能和内存的有效使用,所以有时你需要考虑它并采取措施避免它。一个经典的技巧是,如果你分配了许多相同大小的东西,可以在启动时抓取一个大小为它们倍数的内存池,并完全在内部管理其用法,从而确保你没有在操作系统级别上出现碎片化(并且你内部内存映射器中的空洞将恰好适合下一个相同类型的对象)。
也有整个第三方库专门处理这种事情,有时这是可接受性能和运行缓慢之间的区别。`malloc()`和`free()`执行起来需要相当长的时间,如果你经常调用它们,就会开始注意到。
因此,通过避免仅仅朴素地使用`malloc()`和`free()`,你可以避免碎片化和性能问题 - 但归根结底,你应该始终确保你`malloc()`的所有内容都被`free()`,除非你有非常好的理由不这样做。即使使用内部内存池,一个好的应用程序也会在退出前释放池内存。是的,操作系统会清理它,但如果应用程序的生命周期稍后改变,很容易忘记那个池仍然存在...
当然,长时间运行的应用程序需要非常认真地清理或回收它们分配的所有资源,否则它们最终会耗尽内存。

2
从不同的角度来看待此前评论者和回答者的问题,有可能您的教授曾经使用过静态分配内存的系统(即:在编译程序时分配内存)。
静态分配是指像下面这样进行操作:
define MAX_SIZE 32
int array[MAX_SIZE];

在许多实时和嵌入式系统(最有可能被EE或CE遇到)中,通常最好完全避免使用动态内存分配。因此,很少使用malloc、new及其删除对应项。除此之外,近年来计算机内存已经猛增。
如果您有512 MB可用,并且您静态分配1 MB,则在软件爆炸之前,您大约需要通过511 MB的内存。假设您有511 MB可滥用,如果您每秒malloc 4个字节并且不释放它们,那么您将能够运行近73小时,然后才会耗尽内存。考虑到许多计算机每天都会关闭,这意味着您的程序永远不会耗尽内存!
在上述示例中,泄漏为每秒4个字节,即每分钟240个字节。现在想象一下,你降低了这个字节/分钟比率。比率越低,您的程序就可以长时间运行而没有问题。如果您的malloc不频繁,那么这是一个真实的可能性。
如果您知道自己只需要malloc一次,并且那个malloc永远不会再次出现,那么它就很像静态分配,尽管您不需要预先知道分配的内容的大小。例如:假设我们再次有512 MB。我们需要malloc 32个整数数组。这些是典型的整数-每个整数4个字节。我们知道这些数组的大小永远不会超过1024个整数。在我们的程序中没有其他内存分配。我们有足够的内存吗?32 * 1024 * 4 = 131,072. 128 KB-所以是的。我们有足够的空间。如果我们知道我们永远不会再分配任何内存,那么我们可以安全地malloc这些数组而不释放它们。但是,这也可能意味着如果您的程序崩溃,则必须重新启动机器/设备。如果您启动/停止程序4096次,则会分配所有512 MB。如果您有僵尸进程,则可能永远无法释放内存,即使发生崩溃之后。
避免痛苦和痛苦,把这个口号作为唯一的真理:malloc应始终与free关联。new应始终具有delete。

2
在大多数嵌入式和实时系统中,一个只能持续 73 小时就会导致故障的定时炸弹将是一个严重的问题。 - Ben Voigt
典型的整数??? 整数至少是16位数字,在小型微芯片上通常为16位。一般来说,sizeof(int)等于2的设备比等于4的设备更多。 - ST3

1

我很惊讶居然没有人引用这本书

这个可能最终不成立,因为记忆体可以变得足够大,以至于电脑的生命期内不可能用尽自由记忆。例如,一年中有约3×1013微秒,因此如果我们每微秒增加一个单元,我们需要大约1015个单元的记忆体才能建造出一台机器,其工作时间在30年内不会耗尽内存。那么多的内存在今天看来似乎是荒谬地大,但它并非物理上不可能。另一方面,处理器的速度越来越快,未来的计算机可能会在单个存储器上并行运行大量处理器,因此可能比我们所假设的更快地用完内存。

http://sarabander.github.io/sicp/html/5_002e3.xhtml#FOOT298

因此,实际上,许多程序可以完全不必费心地释放任何内存。


1

我知道有一种情况,显式释放内存比没有作用还糟糕。也就是说,当你需要在进程生命周期的最后才能使用所有数据时。换句话说,在程序终止之前才能释放它们。由于任何现代操作系统在程序死亡时会负责释放内存,因此在这种情况下调用free()是不必要的。事实上,它可能会减慢程序的终止,因为它可能需要访问内存中的多个页面。


如果您想验证程序没有泄漏内存,执行此操作仍然是有用的。 - Jeremy Friesner

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