为什么要使用垃圾回收器?

18

可能是重复的问题:
C++中的垃圾收集 - 为什么使用?

你好,我读了一些关于垃圾回收器的文章,但有一件事我仍然不明白 - 为什么要使用垃圾回收?

我来尝试解释一下我的想法:

垃圾回收器应该会在没有需要时释放动态分配的内存,对吧?那么,如果你用 C 语言编写程序,你知道是否需要某个内存块,如果不需要,就可以直接销毁它。

那么,既然只需要明智地管理内存分配和释放,为什么还要使用垃圾回收呢?或者是我漏掉了什么重要的东西吗?谢谢。


26
一切都在于“明智决策”。 - mouviciel
7
因为程序员很懒。 - ericvg
一个相关的线程:https://dev59.com/rHE85IYBdhLWcg3w02_z - Péter Török
13
如果我足够聪明,在所有情况下都能正确地管理内存,那么我会用令人惊叹的洛杉矶修辞和深刻的见解来回答问题,而不是发表这条评论。总体情况下,内存管理非常困难。 - David Thornley
2
@b-gen-jack-o-neill:我觉得你已经回答了自己的问题。这不是必要的。一个足够小心的程序员可以手动管理资源,并且可以做得很好。当然,如果你完成了一个资源,立即释放它可能是有优势的(有时候不释放反而更高效!)。但是不用担心这个问题让开发者有更多时间处理更重要的问题。个人而言,我更喜欢C++风格的“智能指针”,它是手动和GC之间的一个不错的折中方案,因为它确定了何时会释放资源。 - Evan Teran
显示剩余4条评论
22个回答

19

为了更高的生产力。换句话说,程序员可以专注于编写与特定问题相关的独特代码块。


13
为了避免错误。无论你如何小心地释放内存,你总会犯一个错误,或者编写一个需要复杂内存引用模式的程序,这将使出错的可能性更大。
任何可能存在的东西,经过足够长的时间,最终都会成为现实,除非专门投入额外的精力监视内存消耗,否则你最终会使用手动方法泄漏内存。这种额外的工作会从编码向程序的主要目的转移时间,而这主要目的可能不是管理内存。
此外,即使您的程序不会泄漏内存,垃圾收集通常也比许多非垃圾收集方法更有效地处理内存。大多数人不会使用new块创建对象以避免多次调用new,也不会重新访问和清理未使用的new对象缓存。大多数手动垃圾收集方法集中于在块边界上释放内存,这可能导致垃圾存留的时间比它需要的时间更长。
每增加一项手动垃圾收集的功能都会使你离自动垃圾收集更近一步。除了手动调用free之外,不使用任何工具来收集垃圾将难以进行扩展。你要么会花费大量时间检查内存分配/回收,要么不会投入足够的精力来避免内存泄漏。
无论哪种情况,自动垃圾收集都可以解决这个问题,让你回到程序的主要目的。

5
在一个(非平凡的)程序中处理垃圾,手动写起来需要很长时间(a),手动处理时很难正确完成(b),而且有很好的自动解决方案(c)。在编程中,垃圾回收是应该自动化的事情之一! - Thomas Padron-McCarthy
3
@Steve:“然而,带有垃圾回收功能的语言仍然可能泄漏内存......”实际上,就C/C++而言,带有垃圾回收功能的语言从来不会发生内存泄漏,相反,人们重新定义了什么是内存泄漏,然后在新的方面应用相同的术语。在C/C++中,你遇到的情况是,你无法访问已泄漏的内存,除非随机访问所有内存(如果你能找到它的话,很可能就无法将其强制转换回正确的数据结构)。在Java中,内存不会泄漏,相反,糟糕的程序员永远不停止使用不需要的内存(他们称之为“内存泄漏”)。 - Edwin Buck
1
@Edwin:你所说的“可怜的程序员永远不会停止需要使用不必要的内存”的问题仍然被称为内存泄漏,这是有充分理由的。确保一个永远不会再次使用的引用被置空与确保一个永远不会再次使用的指针被释放是同样的问题。当你无法可靠地管理所有生命周期时,失败的情况和症状是不同的,但这并不意味着基本问题根本上不同。如果不能可靠地管理生命周期是一个“可怜的程序员”问题,那么任何需要GC的人都是一个可怜的程序员。 - user180247
2
@Steve,解引用!=释放内存。故障模式非常不同。如果我意外地释放了其他人正在使用的内存,程序将崩溃。如果我意外地解引用了其他人正在使用的内存,程序将继续执行。重复使用术语来表示其他含义是不可取的,就像版权侵权==盗窃误解一样。盗窃要求您非法剥夺某人的财产,从而防止他们使用它,而不是您可能已经剥夺了他们将来可能获得的利润(将其出售给您)。 - Edwin Buck
1
这是意思不同,还是只是你个人想要的意思范围不在其中?据我所知,没有权威机构来决定词语的确切含义。顺便说一下 - 如果你不小心不调用文件关闭函数,当程序尝试再次打开同一个文件时,它可能会失败。在C++中,析构函数通常可以确保及时清理 - “通常”是因为有时候调用它们是程序员的责任。正如我所说,故障情况和症状是不同的 - 为其中一个问题命名不能证明它整体上更糟糕。 - user180247
显示剩余2条评论

10

因为我们已经不再生活在80年代初了。当你想要创建一个惊人的应用程序时,关注最低层任务既浪费开发者的时间又令人厌烦。


5
我在 C++ 中不使用垃圾回收器,也不会浪费时间担心内存。 - GManNickG
3
是的,我认识很多不太在意记忆力的人。但这也是他们自己的问题,最后得由我们来收拾残局。 - sigfpe
1
@GMan 智能指针现在在C++中非常普遍,是一种垃圾回收形式。 - Andres Jaan Tack
1
智能指针不仅是垃圾回收的一种形式,在大多数情况下它们是一种较差的形式,比起现代适当的垃圾回收系统更慢且使用更多资源。 - JUST MY correct OPINION
2
@Andres @user:vector不需要任何麻烦,也不用担心。如果你想称之为垃圾回收,那也可以,但这是一个相当广泛的定义。 - GManNickG
@GManNickG说:“向量不需要麻烦,也不用担心。”它是最简单的集合类型。但当你需要一个持久化的集合时会发生什么呢?你不再知道其共享内部的生命周期,因此你只能采用最低公共分母并深度复制所有内容,以使手动内存管理变得可控。或者你可以采用垃圾回收技术... - J D

8
因为我们还不够聪明。

如果我们没有更好的事情要做,或许我们会有时间去学习。+1 - Andres Jaan Tack

8
当我编写程序时,我喜欢专注于我的问题领域,而不是无关的实现细节。例如,如果我正在编写一个web服务器,我的问题领域是网络连接、协议、数据传输/接收等,而不是内存分配和释放。如果我正在编写一个视频游戏,我的问题领域是图形性能和可能的AI,同样不包括内存分配和释放。
任何时候花费在问题领域之外的事情都是浪费时间,这些时间本可以用来解决我的问题领域。换句话说,专注于低级细节会导致我的实际工作质量——我真正要解决的问题——受到影响。
此外,你所说的“你只需要在内存分配/释放方面聪明一点”只突出了两种可能性(至少我能想到的):
1.你非常缺乏经验。 2.你已经——很可能是下意识地——限制了你的设计,以使它们符合你对内存管理的简单假设。
在现实世界的软件中,内存管理是一项非常复杂的任务。任何规模较大的现代软件的复杂数据结构都会导致难以确定任何给定动态分配内存块的“活性”和“所有权”。如果引入了线程或更糟糕的是,带有任何形式的共享状态的多处理(对称和非对称),这将变得更加复杂(可能是数量级)。
在未经管理的内存环境中编写的软件中最常见的错误与动态分配内存的管理不善有关,这并非偶然。
那么为什么要使用垃圾回收?因为在处理动态分配内存的各种问题时,你并不像你想象的那么聪明。真的,你不是。无论你认为自己有多聪明。如果你认识到自己不是那么聪明,你会限制你的设计,以使你的内存管理足够简单易懂。但如果你傲慢地认为自己可以处理任何问题,你只会让你的用户面临你的垃圾、容易崩溃和/或占用内存的软件。

1
+1 绝对正确。特别是关于“你非常缺乏经验”的那部分。我发现,在方法论和工具选择方面保守(比如“zomg 管理的东西?谁需要这些现代化的东西,反正我自己来管理内存,因为我太牛逼了”这种态度)往往只是无知和狭隘的表现。然而,在我的团队中不会出现这种情况。:-D - Turing Complete

5
考虑这样一种情况,一个特定的指针被两个不同的子系统使用。其中一个子系统可能已经完成了对变量的操作,程序员可能会认为:“我已经完成了这个操作,我将释放它”,却完全没有意识到另一个子系统仍然需要访问它。或者另一种错误,开发人员认为:“我不确定是否有其他子系统需要这个变量”(即使实际上并没有),导致内存泄漏。这种情况在复杂系统中经常出现。

1
正是我所想的。异常及其处理方式也增加了这种复杂性的混合物。 - James Westgate

4

我同意mouviciel的评论。但是垃圾收集器确实可以加快开发速度,因为开发人员不再需要担心内存泄漏,可以专注于程序的其他方面。

但请注意,如果您正在使用具有垃圾收集功能的语言进行编程,则非常明智地了解这一事实。在我看来,几乎必须要理解它的工作原理以及背后所做的事情。


4

这是一种反“愚蠢程序员”的机制。相信我,当代码变得非常复杂时,当以动态分配内存的方式思考时,我们都同样愚蠢。

在我短暂的程序员经验中,我花费了(累计)数日时间试图弄清楚为什么valgrind(或其他类似工具)报告内存泄漏,当一切看起来都是如此“明智编码”时。


4
当我读到第一句话时,你差点让我自动给你打个负分。但是当我读到第二句话时,我为你喝彩。 - JUST MY correct OPINION
实际上,垃圾回收器除了防止愚蠢/懒惰等问题外,还有很多其他用途。并行编程就是其中之一,许多算法在使用垃圾回收器时更容易实现和运行得更快,而有些算法在没有垃圾回收器的情况下几乎不可能实现。并行性是启用垃圾回收环境的一个重要优势。 - bestsss

3
这些天,大多数使用垃圾回收器的人都是在受控环境(比如Java虚拟机或.NET公共语言运行时)中使用。这些受控环境增加了一个额外的复杂性:它们限制了获取指向对象的指针的能力。例如,在CLR中,有一个指针的概念(可以通过托管的IntPtr或非托管的unsafe代码块使用),但是只有在有限的条件下才允许使用它们。在大多数情况下,您必须“固定”相应的对象在内存中,以便GC在您使用它们的指针时不会移动它们。
为什么这很重要?因为事实证明,允许更新指针并在内存中移动对象的受控分配器比malloc-style分配器更有效率得多。您可以做一些很酷的事情,比如分代垃圾回收,使堆分配像栈分配一样快速,您可以更轻松地分析应用程序的内存行为,并且,哦,是的,您还可以轻松地检测未引用的对象并自动释放它们。
因此,这不仅仅是关于提高程序员的生产力(尽管如果你问任何使用托管语言的人,他们都会证明它提高了他们的生产力),而且还涉及到启用全新的编程技术。
最后,在使用函数式编程语言(或以函数式风格编程)时,垃圾收集变得确实必要。事实上,第一个垃圾收集器是在1959年由麦卡锡发明的,作为Lisp语言开发的一部分。原因有两个:首先,函数式编程鼓励使用不可变数据结构,这些数据结构更容易进行收集;其次,在纯函数式编程中,没有分配函数;内存总是被分配为“堆栈”(函数本地)然后在被闭包捕获时移动到“堆”中。(这是一种过度简化,但可以说明问题。)
所以……如果你以命令式风格编程,并且足够聪明地处理好所有内存分配,那么你就不需要垃圾回收。但是,如果你想改变编程风格,利用最新的编程技术进步,你可能会有兴趣使用垃圾回收。

当然,没有人对除了最简单的内存管理场景以外的任何事情都“足够聪明”。但是,为超出程序员生产力场景的扩展加上+1。 - JUST MY correct OPINION

2

如果你使用C语言编写程序,你知道是否需要一些内存空间,如果不需要,你可以简单地销毁它。

至少在理论上是这样的。问题是这可能会大大复杂化代码。例如:

for (x in ComputeBigList()) ...

变成这样

var xs = ComputeBigList();

try {
   for(x in xs) ...
} finally {
   FreeMemory(xs);
}

缺乏垃圾回收器需要我们命名ComputeBigList的结果,将其存储在一个变量中,然后添加一个包含在finally中的删除语句,以确保它被删除。这就是C++粉丝应该指出C++的保证析构函数调用可以使这个过程更容易的地方。尽管如此,你仍需要考虑与引用计数相关的开销和额外的代码,假设你想让对象能够逃离它们创建的动态范围。(例如:我分配了一个对象,然后把它返回。) 垃圾回收器还有一个有用的功能是控制内存的使用方式。可重定位GC能够让你安排对象,使得它们可以更有效地访问。总的来说,GC为运行时提供了更多关于何时付出回收内存代价的灵活性。(显式释放和引用计数更新必须立即执行。)

1
值得注意的是,用于释放内存的析构函数会保留垃圾直到作用域结束,而跟踪收集器可以(并且确实)在此之前进行回收。 - J D

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