为什么C和C ++之类的语言没有Java的垃圾回收?[关闭]


57

好吧,我知道在C ++中有诸如malloc / free和在C ++中用于内存管理的new / using-destructor之类的东西,但是我想知道为什么这些语言没有“新更新”,使用户能够是否可以选择手动管理内存,或让系统自动进行内存管理(垃圾回收)?

有点新奇的问题,但仅在CS中使用了大约一年。


9
这个学期,我们在iPhone开发中有一个模块。在为Android编写应用程序编码2年之后,这个问题困扰了大部分班级。直到现在,我们才能看到Java实际上已经为我们节省了多少个小时,而不必跟踪讨厌的内存管理错误并且不必编写样板代码。
siamii 2011年

7
@NullUserException,因为它没有指定回收内存的方法,而回收内存几乎意味着GC。
温斯顿·埃韦特

1
@ bizso09:您看过ARC吗?获得系统支持的引用计数时,无需慢速/肥胖/不确定性GC:developer.apple.com/technologies/ios5
JBRWilkinson 2011年

3
这个相当好的问题的答案充满了宗教废话。
abatishchev 2015年

1
在C和C ++中,可以使用一个指针,将其强制转换为int并向其添加一个数字。稍后从int减去数字并将结果转换回指针。您将获得与以前相同的指针。祝您实施GC时好运,因为它的地址仅存储在也具有另一个值的变量中时,它不收集该内存。我知道这个例子很愚蠢,但是XOR链表使用了类似的东西。我会将其发布为答案,但问题已关闭。
玛丽安·斯派尼克

Answers:


72

垃圾收集需要用于跟踪分配和/或引用计数的数据结构。这些会在内存,性能和语言复杂性方面造成开销。C ++被设计为“接近金属”,换句话说,它在权衡性能与便捷功能之间取舍了更高的性能。其他语言则使折衷方式有所不同。这是选择语言时要考虑的因素之一,因此您偏爱于此。

就是说,C ++中有许多引用计数方案,它们相当轻量级且性能很好,但是它们存在于商业和开源库中,而不是语言本身的一部分。管理对象生命周期的引用计数与垃圾回收不同,但是它解决了许多相同类型的问题,并且更适合C ++的基本方法。


26
第二个问题是GC是不确定的。在程序“删除”对象之后,该对象可能会或可能不会仍在内存中。引用计数生命周期是确定性的,当删除最后一个引用时,将删除内存。这不仅对内存效率有影响,而且对调试也有影响。常见的编程错误是“僵尸”对象,即理论上已删除的参考内存。GC更有可能掩盖这种影响,并产生断断续续且极难追踪的错误。
kylben 2011年

22
-现代gc既不跟踪分配也不计数引用。他们从当前堆栈中的所有内容构建图形,然后压缩并擦除其他所有内容(简化),而GC通常会降低语言的复杂性。甚至性能收益也值得怀疑。
乔尔·科洪

13
,, @ kylben,将自动GC嵌入该语言的全部目的在于,它不可能引用僵尸对象,因为GC仅释放了无法引用的对象!当您在手动内存管理中犯错时,您将遇到正在谈论的那种难以追踪的错误。

14
-1,GC不计算引用。另外,根据您的内存使用情况和分配方案,GC可能会更快(内存使用量会有所增加)。因此,有关性能的争论也是谬论。实际上,只有接近金属的点才是有效点。
deadalnix

14
Java和C#都没有使用引用计数:相比之下,基于引用计数的GC方案是非常原始的,并且比现代垃圾收集器差很多(主要是因为复制副本时,它们需要进行内存写操作来更改引用计数!)
mikera

44

严格来说,C语言根本没有内存管理。malloc()和free()不是语言中的关键字,而只是从库中调用的函数。由于malloc()和free()是C标准库的一部分,并且将由C的任何符合标准的实现提供,因此这种区别现在可能是很古怪的,但是在过去并不总是如此。

您为什么要使用没有标准的内存管理语言?这可以回溯到C的起源,称为“便携式程序集”。在许多情况下,硬件和算法可以受益于甚至需要专门的内存管理技术。据我所知,没有办法完全禁用Java的本机内存管理并将其替换为您自己的。在某些高性能/最小资源情况下,这是完全不可接受的。C提供了几乎完全的灵活性,可以准确地选择程序要使用的基础结构。付出的代价是C语言在编写正确的无错误代码方面几乎没有帮助。


2
+1是一个很好的总体答案,尤其是“付出的代价是C语言在编写正确的
无错误

2
C确实具有内存管理功能-但是它可以正常工作,因此人们几乎没有注意到它。有静态内存,寄存器和堆栈。在开始分配堆外之前,您还可以花钱。是堆分配弄乱了事情。至于Java,每个人都可以编写自己的Java运行时-有很多选择,包括所谓的“系统的Java”。.NET几乎可以完成C的所有工作-它仅落后于C ++的本机功能(例如,仅在.NET中管理类)。当然,您还拥有C ++。NET,它具有C ++的所有功能以及.NET的所有功能。
a安

1
@Luaan我想说的是“内存管理”的非常宽泛的定义:“直到您开始从堆中进行分配,否则您会变得很好,很花俏。这是堆分配使事情搞砸了”,这就像说一辆汽车在一架非常好的飞机,却无法飞行。
Charles E. Grant

1
@ CharlesE.Grant好吧,纯函数式语言可以使用这种内存管理来完成所有工作。仅仅因为堆分配在某些用例中是一个很好的折衷,并不意味着它是所有语言/运行时的基准。并不是因为内存管理简单,直接,隐藏在幕后就不再是“内存管理”。设计静态内存分配仍然是内存管理,对堆栈和其他可用资源的充分利用也是如此。
a安

“仅符合标准的实现”不是真的,仅适用于符合标准的主机环境实现。某些平台/标准库(大多数用于8位或16位嵌入式微控制器)不提供malloc()free()。(例如用于PIC的MLAP编译器)
12431234123412341234123

32

真正的答案是,建立安全,有效的垃圾回收机制的唯一方法是为不透明引用提供语言级别的支持。(或者相反,缺少对直接内存操作的语言级别支持。)

Java和C#可以做到这一点,因为它们具有无法操作的特殊引用类型。这使运行时可以自由执行诸如在内存中移动分配的对象之类的事情,这对于高性能GC的实现至关重要。

出于记录,没有现代的GC实现使用引用计数,因此这完全是个麻烦。现代GC使用世代集合,其中新分配的处理方式基本上与栈分配在C ++等语言中的处理方式相同,然后将所有仍在运行的新分配的对象定期移至单独的“幸存者”空间,并整代生成的对象立即被释放。

这种方法各有利弊:支持GC的语言中的堆分配与不支持GC的语言中的堆分配一样快,缺点是需要执行清除操作的对象才能被销毁需要一个单独的机制(例如C#的using关键字),否则它们的清除代码将不确定地运行。

请注意,高性能GC的一个关键是必须为特殊类的引用提供语言支持。C没有这种语言支持,而且永远不会;因为C ++具有运算符重载,所以它可以模拟GC的指针类型,尽管必须谨慎进行。实际上,当Microsoft发明了可以在CLR(.NET运行时)下运行的C ++方言时,他们不得不为“ C#样式引用”(例如Foo^)发明新的语法,以将其与“ C ++样式引用”区分开。 (例如Foo&)。

C ++的功能以及C ++程序员经常使用的是智能指针,它实际上只是一种引用计数机制。我不会认为引用计数是“真正的” GC,但它确实提供了许多相同的好处,但代价是比手动内存管理或真正的GC速度慢,但具有确定性销毁的优势。

归根结底,答案实际上归结为一种语言设计功能。C做出了一个选择,C ++做出了使它与C向后兼容的选择,同时仍然提供了足以满足大多数目的的替代方案,而Java和C#做出了与C不兼容但对于C来说也足够好的选择大多数目的。不幸的是,没有万灵药,但是熟悉其中的不同选择将帮助您为当前尝试构建的任何程序选择正确的选择。


4
这是对问题的实际答案
coredump

1
对于C ++部分,现在您应该看一下std :: unique_ptr和std :: move :)
Niclas Larsson

@NiclasLarsson:我不确定我是否理解你的观点。您是在说std::unique_ptr“对不透明引用的语言级别支持”吗?(这不是我想要的那种支持,除非在C ++中也删除了对直接内存操作的支持,否则我也不认为这是足够的。)我的回答中确实提到了智能指针,我会考虑std:unique_ptr使用智能指针。 ,因为它实际上确实进行引用计数,所以它仅支持引用数量为零或一的特殊情况(并且std::move是引用计数更新机制)。
丹尼尔·普赖登

std::unique_ptr没有引用计数,std::move与引用完全没有关系(因此对性能没有影响)。不过,我明白你的意思了,std::shared_ptr引用计数也隐含地更新了std::move:)
Niclas Larsson

2
@ Mike76:在分配方面,GC分配器的工作速度与堆栈分配一样快,并且GC可以同时释放数千个对象。无论您使用引用计数实现方式做什么,分配和释放都永远不会比malloc和快free。因此,是的,GC可以快得多。(请注意,我说的是“可以”-当然,每个程序的确切性能受许多因素影响。)
Daniel Pryden

27

因为,当使用C ++的功能时,没有必要。

赫伯·萨特(Herb Sutter):“ 多年来我都没有写过删除。

请参阅编写现代C ++代码: 21 年以来C ++的发展

这可能会让许多经验丰富的C ++程序员感到惊讶。


有趣。我今天的阅读材料。
surfasb 2011年

ah,一个视频。但是从来没有少过,很有趣。
surfasb 2011年

2
有趣的视频。最好是21分钟和55分钟。不幸的是,WinRT调用仍然看起来像是C ++ / CLI缓冲。
gbjbaanb

2
@ dan04:是的。但是,如果用C编写,您将得到所需的东西。
DeadMG

6
管理智能指针仅比确保在垃圾回收环境中没有不必要的引用要严格得多。因为GC无法读懂您的想法,所以它也不是魔术。
陶Szelei

15

“全部”垃圾收集器是一个定期运行的过程,用于检查内存中是否有未引用的对象以及是否将其删除。(是的,我知道这太过简单了)。这不是语言的属性,而是框架的属性。

有为C和C ++编写的垃圾收集器- 例如,这个

之所以没有将其“添加”到该语言中,一个原因可能是因为现有代码量巨大,因为他们使用自己的代码来管理内存,因此它们永远不会使用它。另一个原因可能是用C和C ++编写的应用程序类型不需要与垃圾回收过程相关的开销。


1
但是将来编写的程序将开始使用垃圾收集器,不是吗?
黑暗圣堂武士

5
尽管垃圾回收理论上独立于任何编程语言,但要为C / C ++编写有用的GC却非常困难,甚至不可能制作出一个万无一失的GC(至少与Java一样安全)-Java可以拉它的原因之所以关闭,是因为它在受控的虚拟化环境中运行。相反,Java语言是为GC设计的,您将很难编写不执行GC的Java编译器。
tdammers,2011年

4
@tdammers:我同意垃圾收集需要这种语言的支持。但是,重点不是虚拟化和受控环境,而是严格的键入。C和C ++的类型很弱,因此它们允许诸如将指针存储在整数变量中,从偏移量重构指针之类的事情,以及阻止收集器能够可靠地判断可达对象的事情(C ++ 11禁止后者至少允许保守的收藏家)。在Java中,您始终知道什么是引用,因此即使将其编译为本机,也可以精确地收集它。
日1

2
@ThorbjørnRavnAndersen:我可以编写一个有效的C程序,以一种无垃圾收集器无法找到它们的方式存储指针。如果然后将垃圾回收器连接到mallocfree,则会破坏我的正确程序。
Ben Voigt

2
@ThorbjørnRavnAndersen:不,free直到完成后我才打电话。但是您提议的在我明确调用之前不会释放内存的垃圾收集器根本不是free垃圾收集器。
Ben Voigt

12

C是在垃圾回收几乎是一个选项的时代设计的。它还适用于通常无法进行垃圾收集的用途-裸机,具有最少内存和最少运行时支持的实时环境。请记住,C是第一个unix的实现语言,它在具有64 * K *字节内存的pdp-11上运行。C ++最初是对C的扩展-已经做出了选择,并且很难将垃圾回收移植到现有的语言上。这种东西必须在底层构建。


9

我没有确切的引号,但Bjarne和Herb Sutter都说了一些话:

C ++不需要垃圾收集器,因为它没有垃圾。

在现代C ++中,您使用智能指针,因此没有垃圾。


1
什么是智能指针?
黑暗圣堂武士

11
如果就这么简单,那么没人会实施任何GC。
deadalnix

7
@deadalnix:是的,因为没有人实现过分复杂,缓慢,肿或不必要的东西。所有软件一直都是100%有效的,对不对?
扎克(Zach)

5
@deadalnix-用于内存管理的C ++方法比垃圾收集器新。RAII是Bjarne Stroustrup为C ++发明的。析构函数清理是一个较旧的想法,但是确保异常安全的规则很关键。我不知道什么时候该想法本身何时被首次描述,但是第一个C ++标准是在1998年完成的,Stroustrups“ C ++的设计和演进”直到1994年才发布,并且例外是C ++的相对较新的添加-我相信,在1990年出版了《带注释的C ++参考手册》之后。GC于1959年为Lisp发明。
Steve314,2011年

1
@deadalnix-您是否知道至少一个Java VM使用了一个引用计数GC,可以(几乎)使用C ++风格的RAII和一个智能指针类来实现该计数-正是因为它比现有的VM对多线程代码更有效?参见www.research.ibm.com/people/d/dfb/papers/Bacon01Concurrent.pdf。在实践中,您在C ++中看不到此原因的一个原因是通常的GC收集-它可以收集循环,但是在存在循环的情况下无法选择安全的析构函数顺序,因此无法确保可靠的析构函数清除。
Steve314,2011年

8

您问为什么没有更新这些语言以包括可选的垃圾收集器。

可选垃圾回收的问题在于,您不能混合使用不同模型的代码。也就是说,如果我编写的代码假定您使用的是垃圾收集器,则无法在已关闭垃圾收集的程序中使用它。如果这样做,它将泄漏到各处。


6

您能想象用带有垃圾回收的语言编写设备处理程序吗?GC运行时,有多少位可能下降?

还是操作系统?在启动内核之前,如何启动运行的垃圾回收?

C被设计用于接近硬件任务的底层。问题?它是一种很好的语言,因此对于许多更高级别的任务也是不错的选择。语言沙皇意识到了这些用法,但是他们需要优先支持设备驱动程序,嵌入式代码和操作系统的要求。


2
C对高水平有好处吗?我在键盘上哼了一声。
DeadMG

5
好吧,他确实说过“许多更高层次的任务”。他可能在数着巨魔(一,二,很多...)。而且他实际上并没有说出什么话。除了笑话,这是真的-有证据表明许多重要的高级项目已经在C语言中成功开发。许多这些项目现在可能有更好的选择,但是一个可行的项目是有力的证据而不是猜测已经。
2011年

有一些托管的操作系统,它们运行良好。实际上,当您使整个系统处于托管状态时,使用托管代码所造成的性能下降甚至更低,在现实情况下要比非托管代码更快。当然,这些都是“研究OS”-除了在托管OS中制作完全虚拟化的非托管OS外,几乎没有其他方法可以使其与现有非托管代码兼容。微软确实建议在某个时候将它们替换为Windows Server,因为越来越多的服务器代码是在.NET上编写的。
罗安

6

对于这个问题的简短而无聊的回答是,需要为编写垃圾收集器的人们提供一种非垃圾收集语言。同时要允许对内存布局进行非常精确的控制在顶部运行GC 的语言在概念上并不容易。

另一个问题是为什么C和C ++没有垃圾收集器。好吧,我知道C ++有很多,但是它们并没有真正流行,因为它们被迫处理最初不是由GC-ed设计的语言,而仍然使用C ++的人这个年龄的确不是那种错过GC的人。

另外,与其将GC添加到旧的非GC版本的语言中,反而更容易地创建一种在支持GC的同时具有大多数相同语法的新语言。Java和C#是很好的例子。


1
在developer.se或SO的某个地方,有人声称有人在从事自引导垃圾收集的东西的工作-IIRC基本上是使用GC语言实现VM的,而引导子集用于实现GC本身。我忘记了名字。当我查看它时,结果发现它们基本上从未实现过从无GC子集到工作GC级别的飞跃。原则上这可能的,但是AFAIK在实践中从未实现过-当然,这是用艰苦的方式做事的情况。
Steve314

@ Steve314:如果您记得在哪里找到它,我很乐意看到!
hugomg 2011年

找到了!请参阅对stackoverflow.com/questions/3317329/…的注释,指的是Klein VM。找到它的一部分问题-该问题已解决。
Steve314,2011年

顺便说一句-我似乎无法以@missingno开头我的评论-有什么用?
Steve314

@ steve314:弄清楚了该主题的答案,我已经收到所有评论的通知。在这种情况下,执行@ -post是多余的,并且SE不允许这样做(尽管不要问我为什么)。(但真正的原因是因为我的电话号码丢失)
hugomg 2011年

5

有很多问题,包括...

  • 尽管GC是在C ++之前(可能是在C之前)发明的,但是在GC被广泛接受之前,已经实现了C和C ++。
  • 没有底层的非GC语言,您将无法轻松实现GC语言和平台。
  • 尽管对于在典型时标等中开发的典型应用程序代码,GC显然比非GC更为有效,但存在一些问题,即需要付出更多的开发工作才能取得良好的平衡,而且专用内存管理的性能将超过通用GC。此外,即使没有任何额外的开发工作,C ++通常也比大多数GC语言更有效。
  • GC并不比C ++风格的RAII普遍安全。RAII允许自动清除内存以外的资源,这主要是因为它支持可靠且及时的析构函数。由于参考循环的问题,这些方法无法与常规GC方法结合使用。
  • GC语言具有自己独特的内存泄漏类型,特别是与永远不会再次使用的内存有关,但存在的引用从未被清空或覆盖过。需要明确地做,这是原则上比需要没有什么不同deletefree明确。GC方法仍然有一个优势-没有悬而未决的参考-静态分析可以捕获某些情况,但是同样,对于所有情况都没有一种完美的解决方案。

基本上,部分是关于语言的时代,但是无论如何,非GC语言总会有一个地方-即使这是一个小众市场。严重的是,在C ++中,缺少GC并不是什么大问题-内存的管理方式不同,但并非不受管理。

微软托管的C ++至少具有在同一应用程序中混合使用GC和非GC的能力,可以混合使用每种优点,但是我没有经验说这在实践中的效果如何。

代表服务链接到我的相关答案...


4

垃圾回收从根本上与用于开发具有DMA功能的硬件的驱动程序的系统语言不兼容。

指向对象的唯一指针很可能会存储在某些外设的硬件寄存器中。由于垃圾收集器对此一无所知,因此会认为该对象不可访问并进行收集。

对于压缩GC,此参数具有双重意义。即使您小心地维护对硬件外围设备使用的对象的内存引用,当GC重新放置该对象时,它也不知道如何更新外围设备配置寄存器中包含的指针。

因此,现在您需要混合使用固定的DMA缓冲区和GC管理的对象,这意味着您同时具有这两种缺点。


可以说这两个都是缺点,但每个缺点的实例较少,并且优点相同。显然,要处理更多种类的内存管理会很复杂,但是通过为代码中的每个过程选择合适的方法也可以避免复杂性。我想这不太可能,但是在理论上存在差距。我之前曾猜测过将GC和非GC混合使用相同的语言,但不是针对设备驱动程序-更多是针对具有大部分GC应用程序,但具有一些手动内存管理的低级数据结构库。
Steve314,2011年

@ Steve314:您不是说记住要手动释放哪些对象和记住释放所有内容一样麻烦吗?(当然,智能指针可以帮助解决这两个问题,因此,这都不是一个大问题)。对于手动管理的对象与收集的/可压缩的对象,您需要使用不同的池,因为当固定对象分散在各处时,压缩效果不佳。因此,很多额外的复杂性什么也没有做。
Ben Voigt

2
如果在所有GC的高级代码和选择退出GC的低级代码之间没有明确的区分,那就不是了。几年前,我主要在研究D时提出了这个想法,它允许您选择退出GC,但不允许您选择退出。例如,一个B +树库。容器作为一个整体应该是GC,但是数据结构节点可能不是-相对于通过分支节点进行GC递归搜索,仅对叶节点进行自定义扫描更为有效。但是,该扫描确实需要将包含的项目报告给GC。
Steve314

关键是,这是一个包含的功能。治疗B +树节点特殊WRT内存管理是把他们当作特殊WRT无异 B +树节点。它是一个封装的库,应用程序代码不需要知道GC行为已被绕过/特殊情况。除此之外,至少在当时,这在D中是不可能的-就像我说的那样,没有办法选择退出并向GC报告所包含的项目作为潜在的GC根源。
Steve314,2011年

3

因为C&C ++是相对低级的语言,通常用于通用目的,例如,甚至可以在嵌入式系统中具有1MB内存的16位处理器上运行,而这不能用gc浪费内存。


1
“嵌入式系统”?在C标准化(1989年)时,它需要能够处理具有1 MB内存的PC
2011年

我同意,我在举一个最新的例子。
Petruza 2011年

1MB ??? schmoley,谁会需要那么多RAM?</ billGates>
Mark K Cowan

2

在C ++和C中有垃圾收集器。不确定在C中如何工作,但是在C ++中,您可以利用RTTI动态发现对象图并将其用于垃圾收集。

据我所知,没有垃圾收集器就无法编写Java。稍加搜索就发现了这个

Java和C / C ++之间的主要区别在于,在C / C ++中,选择始终是您的选择,而在Java中,设计常常使您无选择。


而且专用的垃圾收集器可以更好地实现,更有效并且更适合该语言。:)
最大

不,您不能使用RTTI来动态发现C / C ++中的对象图:是普通的旧数据对象破坏了一切。普通的旧数据对象中根本没有存储任何RTTI信息,这将使垃圾回收器可以区分该对象中的指针和非指针。更糟糕的是,指针不必在所有硬件上都完全对齐,因此,给定一个16字节的对象,可以在9个可能的位置存储一个64位的指针,其中只有两个不会重叠。
cmaster

2

在性能和安全性之间进行权衡。

无法保证您的垃圾将用Java收集,因此它可能浪费了很长时间,而扫描未引用的对象(即垃圾)也比显式删除或释放未使用的对象要花费更长的时间。

优点当然是,人们可以构建一种没有指针或没有内存泄漏的语言,因此人们更有可能产生正确的代码。

有时,这些辩论可能会有轻微的“宗教”优势-警告!


1

这是GC的固有问题的列表,这些问题使它无法在诸如C的系统语言中使用:

  • GC必须在其管理的对象的代码级别以下运行。内核中根本没有这样的级别。

  • GC必须不时停止托管代码。现在考虑如果对内核执行该操作会发生什么情况。当GC扫描所有现有的内存分配时,计算机上的所有处理将停止一毫秒。这将终止所有创建在严格的实时要求下运行的系统的尝试。

  • GC需要能够区分指针和非指针。也就是说,它必须能够查看存在的每个内存对象,并能够生成可在其中找到其指针的偏移量列表。

    此发现必须是完美的:GC必须能够追踪其发现的所有指针。如果取消引用误报,则可能会崩溃。如果未能发现误报,则很可能会破坏仍在使用的对象,从而使托管代码崩溃或无声地破坏其数据。

    绝对需要将类型信息存储在现有的每个对象中。但是,C和C ++都允许不包含类型信息的普通旧数据对象。

  • GC本质上是一项缓慢的业务。曾经与Java交往过的程序员可能没有意识到这一点,但是如果不使用Java来实现,程序的速度可能会提高几个数量级。导致Java变慢的因素之一是GC。这就是诸如Java之类的GCed语言无法用于超级计算的原因。如果您的机器每年耗费一百万美元的电力,那么您甚至不想为垃圾回收支付10%的费用。

C和C ++是为支持所有可能的用例而创建的语言。而且,如您所见,垃圾收集排除了许多这些用例。因此,为了支持这些用例,不能对C / C ++进行垃圾收集。

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.