使用垃圾回收有哪些缺点?

19

现代大多数编程语言都内置垃圾回收(GC),例如Java、.NET语言、Ruby等。确实,GC在许多方面简化了应用程序开发。

我想知道使用具有GC的语言编写应用程序的限制/缺点。假设GC实现是最佳的,我只是想知道我们可能会被GC限制做出一些优化决策。


2
https://dev59.com/q3M_5IYBdhLWcg3wUxfF - Bertrand Marron
@tusbar 虽然这个问题的标题很泛泛,但它只是询问开发人员在使用无GC语言编程时可能会犯的错误。 - rpattabi
8个回答

26

在我看来,使用垃圾回收器的主要缺点是:

  1. 资源的非确定性清理。有时候,我们希望立即清理某些资源,但是使用垃圾回收器通常意味着强制进行全局清理或者等待垃圾回收器自行清理,这都会削弱开发者的一些控制权。

  2. 由于垃圾回收器的非确定性操作可能导致的潜在性能问题。当垃圾回收器进行回收时,通常会出现(小)卡顿等问题。这对于实时模拟或游戏等应用程序尤其具有问题。


18
+1 将GC添加到他在游戏中死亡的原因列表中 - corsiKa
@glowcoder:这不是很棒吗?我喜欢有东西可以为之承担责任... ;) - Reed Copsey
非确定性清理是GC问题常见的情况吗?还是特定于特定的GC实现? - rpattabi
@ragu.pattabi:从其本质上讲,GC往往几乎总是具有非确定性的清理。这实际上是GC的重点所在 - 您不必担心何时或如何释放内存,并将其留给收集器。 - Reed Copsey
使用垃圾回收器,通常意味着您不使用垃圾回收器来完成该操作。 - J D

14

来自一位C程序员的建议...关键在于成本效益和适当的使用

垃圾回收算法(如三色标记/标记清除)中通常存在着资源“丢失”与物理资源释放之间的显著延迟。在某些运行时中,垃圾回收甚至会暂停程序执行以执行垃圾回收。

作为一名长期从事C编程的人,我可以告诉你:

a) 手动free()垃圾回收很难 - 这是因为人工放置free()调用的错误率通常比GC算法高。

b) 手动free()垃圾回收需要时间 - 调试所花费的时间是否大于GC的毫秒级暂停?如果你正在编写游戏而不是嵌入式内核,使用垃圾回收可能是有利的。

但是,当你无法承受运行时劣势(正确的资源、实时约束)时,手动资源分配可能更好。虽然可能需要时间,但可以做到100%的效率。

试想一下,用Java编写操作系统内核?或者在.NET运行时上使用GC......只需看看JVM在运行简单程序时积累的内存量。我知道这样的项目存在......它们只会让我感到有些不适。

只要记住,现在我的Linux桌面配备了3GB RAM,与多年前512MB RAM时做的事情基本相同。唯一的区别是现在运行了Mono/jvm/Firefox等。

GC的商业案例很清楚,但它仍然经常让我感到不舒服。

好的书籍:

《编译原理》(最新版)《现代C编译器实现》


1
我同意这个观点,但是以“100%的效率”来执行手动资源分配通常会导致编写自己的“迷你GC”。这需要付出很多努力,并且经常会带来其他问题。(例如,在C语言中尝试防止内存碎片化非常具有挑战性...) - Reed Copsey
1
@Aiden:我之所以提到这个是因为我不得不使用压缩编写自己的自定义分配器。如果你的应用程序具有相对平坦的内存使用模式,那么这非常容易且更加高效,但如果不是,那就很麻烦了。 - Reed Copsey
1
@ragu.pattabi:实际上,在这个领域已经有一些研究了。有一些是严肃的,还有很多是业余爱好者(比如http://jos.sourceforge.net/)。大多数操作系统都有自己的内存管理例程,已经能够完成类似垃圾回收器对可执行文件的工作了... - Reed Copsey
1
@Reed Copsey - 我完全同意。我已经重写了一些C应用程序,因为最终我会将上下文传递给函数并采用面向对象的方法...当C应用程序获得那种数据“形状”时,我通常会选择Python并深入研究。我既不支持也不反对任何一种方法...每种方法都适用于特定的约束条件。 - Aiden Bell
1
@Reed,如果我说错了,请纠正我,但我们大多数时候编写分配器是为了避免碎片或在大小不固定(向量)的重新分配时避免复制数据。在这些情况下,如果您正在寻求性能,GC也将是一个巨大的痛苦。 - Ramadheer Singh
显示剩余5条评论

7
对于.NET,我看到了两个缺点。
1) 人们认为GC最懂,但事实并非总是如此。如果您进行某些类型的分配,您可能会在没有直接调用GC的情况下导致程序崩溃。
2) 大于85k的对象进入LOH或大对象堆。该堆当前从未被压缩,因此,当LOH未压缩足够以进行另一个分配时,您的程序可能会遇到内存不足异常。
这些错误都显示在我在此问题中发布的代码中: 如何使.NET积极进行垃圾回收?

大对象堆是很有趣的。这是.NET特定的东西吗? - rpattabi
1
@ragu.pattabi:是的。基本上,在.NET中,任何单个85k分配(即:大量结构数组)都将使用“传统”样式分配,并且不会与GC堆的其余部分紧凑。 - Reed Copsey
@Reed Copsey:很有趣。我看,研究特定的GC实现可能会指出它们特有的限制,尽管大多数限制是共通的。 - rpattabi
我在使用LOH和.Net时遇到了一些非常严重的问题,特别是在进行任何类型的COM互操作或构建运行数天的Windows服务模块时。我希望有一种解决方案可以强制清除.Net中的LOH。 - James
我想知道为什么大对象堆(LOH)分配的边界比4K小?将85K+对象填充到下一个4K会浪费最多20%的空间,然后可以使用页表压缩LOH。因此,我的基本理念是“避免分配超过80K的任何东西”。 - supercat

4
我很想了解在使用垃圾回收语言编写应用程序时的限制和劣势。假设GC实现是最优的,我只是想知道我们可能会受到GC的限制而无法做出一些优化决策。
我的观点是自动内存管理对效率施加了限制,但我没有证据来证明这一点。特别是,今天的GC算法只提供高吞吐量或低延迟,而不能同时拥有两者。生产系统如.NET和HotSpot JVM会在优化吞吐量时产生显著的暂停。专门的GC算法如Staccato可以提供更低的延迟,但代价是更低的最小mutator利用率,因此吞吐量也较低。

2
如果你对自己的内存管理技能有信心(很好),那么就没有优势。
这个概念是为了缩短开发时间而引入的,因为缺乏深入理解内存的编程专家。

13
“自信”和“优秀”是不同的。;) - Sean Edwards
3
即使你在内存管理方面非常熟练自信,使用垃圾回收仍然有很多优势。如果你使用紧凑型垃圾回收机制,这一点尤其明显。 - Reed Copsey
2
答案在我看来有点自以为是。这就像是说C语言是为那些不彻底理解汇编语言的人而发明的一样。垃圾回收大大简化了编程,从而允许更有效率的开发。在很少的情况下需要完全控制内存,理论上的性能提升也是如此。 - Janick Bernet
2
内存泄漏通常是由于遗漏的事情引起的,而不是缺乏专业知识。虽然我不会写出错误,但我听说其他人因为这个原因而犯了错误。 - jfsk3
1
@JonHarrop 我同意,经过这么多经验后,我才意识到将软件性能置于软件复杂性之上是幼稚的。我能说什么呢,我们只有随着年龄增长才变得聪明,尤其是像我这样不太聪明的人。 - Ramadheer Singh
显示剩余8条评论

1

在性能方面(尤其是实时系统),最大的问题是当GC启动时,您的程序可能会遇到一些意外的延迟。但是,现代GC试图避免这种情况,并且可以针对实时目的进行调整。

另一个明显的问题是,您不能自己管理内存(例如,在分布式共享内存上分配),当您实现低级软件时可能需要这样做。


通常情况下,如果您需要非常低级别(例如:NUMA本地内存)的要求,您可以使用传统的分配方式。 - Reed Copsey
与我之前的想法相反,.NET有一种针对嵌入式应用程序的版本,称为.NET Micro Framework。它具有GC。 - rpattabi
@Reed Copsey:但是这样你已经拥有了一些允许两者的混合系统。我认为大多数GC语言不允许这样的事情。 - Janick Bernet

1
在多CPU环境中,几乎不可能使非GC内存管理器工作而不需要在每次分配或释放内存时获取和释放锁。每个锁的获取或释放都需要一个CPU协调其行动与其他CPU,并且这种协调往往是相当昂贵的。基于垃圾收集的系统可以允许许多内存分配发生而不需要任何锁定或其他CPU间的协调。这是一个重要的优势。缺点是垃圾收集中的许多步骤需要CPU协调它们的操作,并且获得良好的性能通常需要将这些步骤相当程度地整合(如果CPU必须在每个垃圾收集步骤之前进行协调,则消除每个内存分配的CPU协调要求没有太多好处)。这种整合通常会导致系统中的所有任务在收集期间暂停不同长度的时间;一般来说,愿意接受的暂停时间越长,收集所需的总时间就越少。
如果处理器要回到基于描述符的句柄/指针系统(类似于80286所使用的,尽管现在不会再使用16位段),那么垃圾回收可以与其他操作同时进行(如果当GC想要移动一个句柄时该句柄正在使用,则使用该句柄的任务必须被冻结,同时将数据从旧地址复制到新地址,但这不应该花费太长时间)。不确定是否会发生这种情况(顺便说一下,如果我能选择,对象引用将是32位,指针将是对象引用加上32位偏移量;我认为在需要超过20亿个对象或任何超过4GB的对象之前,还需要一段时间。尽管摩尔定律成立,但如果一个应用程序有超过20亿个对象,通过使用更少、更大的对象来提高性能可能更好。如果应用程序需要超过4GB的对象,通过使用更多、较小的对象来提高性能可能更好。)

有趣的观点。顺便问一下,20亿个对象/每个对象4GB?到那时候,主流开发可能会有面向对象的替代方案,我猜 :-) - rpattabi
“……垃圾收集可以与其他操作同时进行。” 垃圾收集通常会与其他操作同时进行。 - J D
在典型的“并发”GC中,GC的大部分工作可以与其他操作同时进行,但有些工作则不能。此外,试图让GC与其他操作并发运行会增加GC开销,因为它必须跟踪自上次GC检查以来可能已被修改的项目。拥有硬件辅助描述符来帮助跟踪哪些对象已被修改可能会在两个方面都有所帮助。 - supercat
1
@supercat 典型的垃圾回收器只是“大多数并发”,因此确实会产生短暂的停顿,是的。然而,有些垃圾回收器是完全并发的,因此将其作为垃圾回收的缺点是无效的。特别是,您的断言“当垃圾回收发生时,直到完成所有其他操作都必须停止”并不适用于所有垃圾回收器。 - J D

0
通常,垃圾回收有一定的缺点:
垃圾回收消耗计算资源来决定哪些内存需要被释放,重构可能已知于程序员的事实。在源代码中不手动注释对象生命周期的方便性的代价是开销,通常会导致性能降低或不均匀。与内存层次结构的交互作用可能使这种开销在难以预测或在例行测试中难以检测的情况下变得无法忍受。
垃圾实际上被收集的时间点可能是不可预测的,从而导致会话中散布着停顿。在实时环境(如设备驱动程序、事务处理或交互式程序)中,不可预测的停顿可能是不可接受的。
即使存在垃圾收集器,内存也可能泄漏,如果对未使用的对象的引用本身没有手动处理,则会发生逻辑内存泄漏。例如,递归算法通常延迟释放堆栈对象,直到最后一个调用完成之后。缓存和记忆化是常见的优化技术,通常会导致这种逻辑泄漏。许多程序员认为垃圾收集可以消除所有泄漏,因此不会防范创建这种泄漏。
在现代台式计算机的虚拟内存环境中,垃圾收集器很难注意到何时需要进行收集,导致大量累积的垃圾、长时间的干扰性收集阶段和其他程序的数据被交换出去。
也许最重要的问题是,依赖垃圾收集器的程序通常表现出较差的局部性(与缓存和虚拟内存系统相互作用不良),占用比程序实际使用的地址空间更多的地址空间,并触及其他闲置页面。它们可能会结合成一种称为抖动的现象,在这种现象中,程序花费更多的时间在各种存储等级之间复制数据,而不是执行有用的工作。它们可能使程序员无法推断设计选择的性能影响,从而使性能调整变得困难。它们可能会导致垃圾收集程序干扰竞争资源的其他程序。

2
很惊奇我错过了在维基百科上查找我的问题。谢谢!http://en.wikipedia.org/wiki/Garbage_collection_(computer_science)#Disadvantages - rpattabi
垃圾回收消耗计算资源来决定哪些内存需要被释放。同时,它通过消除所有对 free 的调用来节省资源。 - J D
尽管存在垃圾回收器,内存仍可能泄漏。这并不是垃圾回收的缺点,就像下一个编程错误一样,它也无法为您节省。 - J D
他们可能会使程序员无法理解设计选择对性能的影响。这并不是真的。 - J D

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