为什么在垃圾回收语言中不允许使用free()函数?

17
我在维基百科上阅读了C#的文章,并发现:

托管内存不能被显式释放;相反,它会自动进行垃圾回收。

为什么在具有自动内存管理的语言中,人工管理甚至不被允许?我可以看出,在大多数情况下,这是不必要的,但在内存紧缺且不想依赖于智能垃圾回收器的情况下,手动管理是否有用?
11个回答

10

自动内存管理的编程语言旨在提供实质性的内存安全保证,这是任何手动内存管理所不能提供的。

它们可以防止以下问题:

  • 重复调用free()
  • 在没有拥有该内存的情况下,调用free()指针,导致其他地方的非法访问
  • 对不是分配函数的返回值的指针(例如,栈中某个对象或数组中间的地址)调用free()
  • 引用已经被释放的内存的指针

另外,自动内存管理可将活动对象移动到一个集中的区域,从而提高缓存性能,因此也可以获得更好的性能。


1
在 .NET 中,错误地调用 Dispose 可能会导致相同的问题。 - Andrey
@Andrey:在.NET中,对Wizbang调用Dispose可能会将其转换为无用的Wizbang对象,该对象不能再执行任何特定于Wizbang的操作,但它仍然是一个有效的Object,并且像GetTypeEquals等方法应该继续工作。只要任何引用仍然指向Wizbang,所有对它的引用都将成为指向死亡Wizbang的引用。相比之下,在手动分配系统中,对存在其他副本的指针调用free可能会导致这些其他副本变成指向完全不同的东西的引用。 - supercat

5
垃圾回收通过保证内存分配从不别名来强制执行内存分配器的类型安全。也就是说,如果当前有一个内存片段被视为类型T,那么内存分配器可以通过垃圾回收来保证只要该引用仍然存在,它将始终指向T类型。更具体地说,这意味着内存分配器永远不会将该内存作为不同类型返回。
现在,如果内存分配器允许手动使用free()并使用垃圾回收,它必须确保您free()的内存没有被其他任何人引用;换句话说,您传递给free()的引用是该内存的唯一引用。大多数情况下,鉴于对free()的任意调用,这种做法的成本过高,因此大多数使用垃圾回收的内存分配器都不允许这样做。
这并不是说不可能做到;如果您可以表示单一参考者类型,则可以手动管理它。但是在这一点上,最好要么停止使用GC语言,要么不必担心它。

这是我在回答中提到的内存安全性的良好描述。谢谢。 - Phil Miller
我接受了这个答案,因为它提供了一个坚实的例子,手动free()可能实际上会干扰GC的工作。就我所理解的而言,其他答案只是列出了使用free()违背了拥有GC的意图的方式。 - Sundar R
@sundar,谢谢。我实际上从未考虑过GC强制类型安全,直到我在一份有关D语言的演示文稿中读到了类似的内容。 - MSN

1

调用GC.Collect几乎总是比使用显式的free方法更好。只有在指针/对象引用从未被引用时,才有意义地调用free。这是一种容易出错的情况,因为有可能您会为错误类型的指针调用free

当运行时环境为您进行引用计数监控时,它知道哪些指针可以安全释放,哪些不行,因此让GC决定哪些内存可以释放避免了一类丑陋的错误。人们可以想象一个同时具有GCfree的运行时实现,在这个实现中,显式调用free来释放单个内存块可能比运行完整个GC.Collect要快得多(但不要期望手动释放每个可能的内存块比GC更快)。但我认为C#、CLI(以及其他具有垃圾收集器的语言,如Java)的设计者已经决定在这里优先考虑健壮性和安全性而不是速度。


1
GC环境实际上可以比等效的手动环境具有更高的性能,因为分配通常只是指针的移动。移动收集器还可以在程序运行时改善引用的局部性。 - Phil Miller
此外,JVM和CLR中的垃圾收集器不是基于引用计数的,因为这无法处理循环结构。它们从根集追踪对象并标记或移动已到达的对象,处理永远无法到达的对象。 - Phil Miller
@Novelocrat:你说JVM和CLI的话是对的,我应该说“引用监控”,而不是“引用计数”。然而,我故意写了“为单个内存块调用free”,而不是“为复制整个GC行为调用free”。 - Doc Brown

1
在允许手动释放对象的系统中,分配例程必须搜索已释放内存区域的列表以找到一些可用内存。在基于垃圾收集的系统中,任何立即可用的空闲内存都将位于堆的末尾。通常,系统忽略堆中未使用的内存区域比尝试分配它们更快且更容易。

0

有趣的是,你可以通过 System.GC 访问垃圾回收器——尽管从我所读到的一切来看,强烈建议您允许 GC 自行管理。

曾经有第三方供应商建议我使用以下两行代码来处理 DLL 或 COM 对象等垃圾回收问题:

// Force garbage collection (cleanup event objects from previous run.)
GC.Collect();         // Force an immediate garbage collection of all generations
GC.GetTotalMemory(true);

说实话,除非我确切地知道底层发生了什么,否则我不会费心去使用System.GC。在这种情况下,第三方供应商的建议“修复”了我处理其代码时遇到的问题。但我不禁想知道,这是否实际上是他们破损代码的一种变通方法...

2
可能是的。我唯一会考虑调用GC.Collect的情况是当我有一个巨大的、长寿命的对象被丢弃时。GC假设如果一个对象存在很长时间,你会再保留它一段时间,因此任何经过多次收集而幸存下来的东西都不会被收集,除非进行完整的收集。话虽如此,这是一个罕见的情况,需要手动执行。 - cHao
@Novelocrat:我从未说过它是。我只是说:“有趣的是……” - Pretzel
@cHao:看起来你理解程序的“底层”运作,并且知道何时使用GC(显然,不滥用它…) :-) - Pretzel

0

我不能说这是“答案”,但我想到的一个可能原因是,如果你可以使用free,你可能会意外地双倍释放指针/引用,甚至更糟糕的是,在释放后继续使用。这违背了使用c#/java等语言的主要目的。

当然,解决这个问题的一个可能方法是,让你的free通过引用来接收参数,并在释放后将其设置为null。但是,如果他们像这样传递一个r-valuefree(whatever()),该怎么办呢?我想你可以为r-value版本提供重载,但我甚至不知道c#是否支持这样的东西:-P。

最后,即使这样也是不够的,因为正如已经指出的那样,你可以有多个引用指向同一个对象。将其中一个设置为null并不能防止其他引用访问现在已被释放的对象。


1
将传递的指针置空并不是解决问题的方法。指针可以被复制,因此可能会被其他代码引用。请参见我的答案。 - Phil Miller
当然,我并没有试图暗示这是一个有效的解决方案。 - Evan Teran

0

如果你处于“不想依赖GC聪明”的情况下,那么很可能你选择的框架不正确。在.NET中,你可以稍微操纵GC(http://msdn.microsoft.com/library/system.gc.aspx),在Java中不确定。

我认为你不能称之为免费,因为你开始执行GC的一个任务。当GC以它认为最好的方式进行操作并在它决定时执行操作时,GC的效率可以得到保证。如果开发人员干预GC,它的整体效率可能会降低。


主要关注安全而非效率。 - Phil Miller
尽管垃圾回收通常可以提高性能,但很少有系统使用具有可证明属性的收集器,因为它们需要付出巨大的开销来维护这些保证。 - Phil Miller
@Novelocrat 我不同意关于安全性的观点。我认为这与垃圾回收的完整性有关。"凯撒的物归凯撒",让垃圾回收器完成它的工作。.net/java仍然会让你有可能自己给自己惹麻烦。 - Andrey

0

许多其他答案提供了关于垃圾回收如何工作以及在针对提供垃圾回收的运行时系统进行编程时应该如何思考的好解释。

我想添加一个技巧,当我在使用垃圾回收语言编程时,我尽量铭记在心的规则是:“尽早放弃指针是很重要的。” 通过放弃指针,我的意思是不再指向我将不再使用的对象。例如,在某些语言中,可以通过将变量设置为Null来实现这一点。这可以被视为给垃圾回收器的提示,表明它可以收集此对象,前提是没有其他指向它的指针。


2
我不会点踩,但在某些系统(例如.NET)中,JIT/GC合作来跟踪方法体内的活动引用。这意味着一旦变量不再被访问,它所指向的任何内容都可以进行回收(假设没有其他未解除的引用)。将变量设置为null实际上会延长变量的生命周期。 - Damien_The_Unbeliever
@Damien - 如果编译器或运行时在程序的静态单赋值表示上运行其分析,则对空值的赋值不会有问题。实际上,如果该赋值后支配了原始赋值,则它为后者的生命周期提供了硬边界。 - Phil Miller

0
为什么要使用free()呢?假设您有一大块内存需要释放。
一种方法是调用垃圾收集器,或者让系统在需要时运行。在这种情况下,如果无法访问该大块内存,则会被释放。(现代垃圾收集器非常智能。)这意味着,如果它没有被释放,仍然可以访问它。
因此,如果您可以使用free()来摆脱它,而不能使用垃圾收集器,则仍然可以访问该块(如果语言具有概念,则不通过弱指针),这意味着您留下了该语言等效的悬空指针。
语言可以防止双重释放或尝试释放未分配的内存,但它避免悬空指针的唯一方法是废除free(),或修改其含义,使其不再有用。

0
为什么在自动内存管理的语言中,甚至不允许手动管理?我可以看出,在大多数情况下,这是不必要的,但在内存紧张且不想依赖GC智能的情况下,手动管理会有所帮助,不是吗?
在绝大多数垃圾收集语言和虚拟机中,提供free函数是没有意义的,尽管您几乎总是可以使用FFI在托管VM之外分配和释放未管理的内存。
垃圾收集语言中缺少free有两个主要原因:
1. 内存安全性。 2. 没有指针。
关于内存安全性,自动内存管理背后的主要动机之一是消除由于不正确的手动内存管理而引起的错误类别。例如,使用手动内存管理时,使用相同的指针两次或使用不正确的指针调用free可能会破坏内存管理器自己的数据结构,并在程序后面(当内存管理器下次到达其损坏的数据时)导致非确定性崩溃。这在自动内存管理中不会发生,但是暴露free将再次打开这个问题。
关于指针,free函数会释放由指针指向的内存块并将其返回给内存管理器。垃圾回收语言和虚拟机使用更抽象的引用概念代替指针。大多数生产环境中的垃圾回收器都是移动式的,这意味着高级代码持有值或对象的引用,但底层内存位置可以在不知情的情况下被虚拟机移动。这用于压缩堆,防止碎片化并提高局部性。

因此,在具有垃圾回收功能时没有free的好理由。


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