垃圾回收需要多少额外的内存?

14

我曾听说,为了正确实现和运行垃圾回收机制,平均需要多出3倍的内存。我不确定这是针对小型应用、大型应用还是通用应用。

因此,我想知道是否有关于垃圾回收开销的研究或实际数据。另外,我想说垃圾回收是一个非常好的功能。


那种“平均3倍内存”的笼统说法可以安全地忽略,它变化太多了,无法做出这样的笼统陈述。垃圾回收可能是一个表面上看起来简单的主题(标记-清除的基本原理相对简单,你会听到的前几个变化也是如此),但是当你深入挖掘时,它变得越来越深入。因此,从内存管理子专家中听到很多不正确的笼统说法。 - TaylanKammer
4个回答

8
需要的内存空间取决于你程序中的分配速率。如果你的分配速率很高,GC在工作时需要更多的空间来实现增长。
另一个因素是对象的生命周期。如果你的对象通常存在非常短的生命周期,则可以通过使用分代收集器来管理略少一些的空间。
有很多研究论文可能会引起您的兴趣。稍后我会编辑一下以添加参考文献。
编辑(2011年1月):
我想到了一篇特定的论文,但现在似乎找不到了。以下这些论文很有意思,并包含一些相关的性能数据。作为经验法则,通常情况下,可用内存应该是程序驻留内存的两倍左右。有些程序需要更多,但其他程序即使在受限的环境中也可以表现得非常好。有很多变量会影响这一点,但分配速率是最重要的。
  1. Immix: a mark-region garbage collector with space efficiency, fast collection, and mutator performance

  2. Myths and realities: the performance impact of garbage collection

    编辑(2013年2月):这个编辑在一篇引用文章的平衡观点和Tim Cooper提出的异议上进行了回应。

  3. Quantifying the Performance of Garbage Collection vs. Explicit Memory Management,正如Natan Yellin所指出的,实际上是我最初在2011年1月时试图想起的参考文献。然而,我认为Natan提供的解释是不正确的。该研究并没有将GC与传统手动内存管理进行比较。相反,它将GC与一个可以执行完美显式释放的oracle进行比较。换句话说,它让我们无法知道传统手动内存管理与神奇的oracle相比表现如何。这也很难弄清楚,因为源程序要么是针对GC编写的,要么是针对手动内存管理编写的。因此,任何基准测试都具有固有的偏见。

在Tim Cooper的异议之后,我想澄清一下我对内存空间问题的立场。我主要是为了记录,因为我认为Stack Overflow的回答应该成为许多人长期使用的资源。
在典型的GC系统中有许多内存区域,但有三种抽象的内存类型:
  • 已分配空间(包含活动、死亡和未跟踪对象)
  • 保留空间(从中分配新对象)
  • 工作区域(长期和短期GC数据结构)
什么是“headroom”?Headroom是维持期望性能所需的最小保留空间量。我认为这就是OP所问的。您还可以将headroom视为附加到实际程序驻留内存(最大活动内存)所需的内存,以实现良好的性能。
是的 - 增加headroom可以延迟垃圾收集并增加吞吐量。这对于离线非关键操作很重要。
实际上,大多数问题领域需要实时解决方案。有两种不同的实时性:
硬实时关注最坏情况下的延迟(对于关键任务系统) - 分配器的响应延迟是一种错误。
软实时关注平均或中位延迟 - 分配器的响应延迟是可以接受的,但不应经常发生。
大多数最先进的垃圾收集器针对软实时,这对于桌面应用程序以及按需提供服务的服务器都很好。如果将实时性作为要求取消,则可以使用停止-世界垃圾收集器,其中headroom开始失去意义。(注意:具有主要短期对象和高分配率的应用程序可能是一个例外,因为生存率较低。)
现在假设我们正在编写具有软实时要求的应用程序。为了简单起见,让我们假设GC在专用处理器上并发运行。假设程序具有以下人工属性:
平均驻留时间:1000 KB
保留headroom:100 KB
GC周期持续时间:1000 ms
和:
分配速率A:100 KB / s
分配速率B:200 KB / s
现在,我们可能会看到使用分配速率A的以下事件时间轴:
T + 0000 ms:GC周期开始,可用于分配的100 KB,已分配1000 KB
T + 1000 ms:
保留空间中没有免费空间,已分配1100 KB
GC周期结束,释放100 KB
预留空间中有100 KB空闲,已分配1000 KB
T + 2000 ms:与上述相同
使用分配速率B的事件时间轴不同。
  • T+0000毫秒:GC循环开始,可用于分配的100KB,已经分配了1000KB
  • T+0500毫秒:
    • 保留空间中没有0KB可用,已分配1100KB
    • 要么
      • 延迟到GC循环结束(不好,但有时是必须的),或者
      • 将保留大小增加到200KB,并保留100KB的空闲空间(在此假设)
  • T+1000毫秒:
    • 保留空间中没有0KB可用,已分配1200KB
    • GC循环结束,释放200KB
    • 保留空间中有200KB空闲,已分配1000KB
  • T+2000毫秒:
    • 保留空间中没有0KB可用,已分配1200KB
    • GC循环结束,释放200KB
    • 保留空间中有200KB空闲,已分配1000KB

请注意,分配速率直接影响所需的头部空间大小。使用分配速率B,我们需要两倍的头部空间来防止暂停并保持相同的性能水平。

这只是一个非常简化的例子,旨在说明一个想法。还有很多其他因素,但它确实展示了所期望的内容。请记住我提到的另一个主要因素:平均对象寿命。短寿命导致低存活率,与分配速率一起影响维持给定性能水平所需的内存量。

简而言之,在不知道和理解应用程序特性的情况下,不能对所需的头部空间做出一般性的声明。


我不明白为什么分配率会影响所需的内存余量。这会影响调用垃圾回收的频率和运行时间,但不会影响内存需求。 - Tim Cooper
@TimCooper分配速率会影响到实时要求下的最小内存余量。为了让分配器在规定时间内响应,它必须有一部分备用内存。更高的分配速率(每秒字节数)导致更多的内存在活动GC周期完成之前被占用,因此对内存的需求更大。 - Kevin A. Naudé
根据这个逻辑,你的服务器拥有的RAM越多,你就会遇到更多的问题。我不认为这个问题与实时情况有关,但即使在实时情况下,你所关心的是最坏情况而不是平均情况,你似乎主张在任务固有要求之上留有更少的余地,而不是更多的余地..? - Tim Cooper
2
@TimCooper 我不确定为什么我们沟通出现了误解,但是您对我的措辞和我原本意图传达的信息的解读并不符合。我已经扩展了我的回答以进行澄清。最好的问候,Kevin。 - Kevin A. Naudé
根据对象的总数和其复杂性,即使是“停止世界”收集器也可以逐步地完成其工作(即使每个“停止世界”事件涉及访问每个非叶子对象,这在嵌入式系统中可能不会花费太长时间,其中堆总共只包含几千个对象)。例如,可以将内存划分为区域,并且每个“停止世界”事件都识别出具有最少有用内容的区域,并将该内容复制到空区域中(从而使旧区域为空)。 - supercat

4
根据2005年的研究《量化垃圾收集与显式内存管理的性能》(PDF),分代垃圾回收器需要5倍的内存才能达到相同的性能。以下是重点:
我们比较了显式内存管理和复制和非复制垃圾收集器在一系列基准测试中的表现,并包括验证我们结果的真实(非模拟)运行。这些结果量化了垃圾收集的时间空间权衡:使用五倍的内存,一个Appel风格的分代垃圾收集器与非复制成熟空间的显式内存管理相匹配的性能。只需三倍的内存,它的平均速度比显式内存管理慢17%。然而,只需两倍的内存,垃圾收集会使性能下降近70%。当物理内存稀缺时,分页会导致垃圾收集比显式内存管理慢一个数量级。

TL;DR: 如果你将麦卡锡 (RIP) 1960年的参考实现与今天行业级别的malloc实现进行基准测试,那么它确实需要5倍的内存。 (这句话以讽刺的方式书写。:-) ) http://taylanub.github.io/webapps-js-gc/ - TaylanKammer

3
我希望原作者清楚地标记出他们认为的垃圾收集(garbage collection)使用的正确方法以及其主张的背景。
当然,开销肯定取决于许多因素; 例如,如果您较少运行垃圾回收器,开销就会更大;复制垃圾回收器的开销比标记和扫描回收器的开销要高;而在单线程应用程序中编写开销较低的垃圾回收器要比在多线程世界中容易得多,特别是对于任何移动对象的操作(复制和/或紧凑gc)。

没有提到什么是“正确”的,但我认为他的意思是实际上找到所有垃圾(如果a指向b,b指向a,但没有任何东西指向它们两个,则正确的意思是将它们都识别为垃圾),而不是错误地收集正在使用的对象。此外,这应该可以在多线程中工作。无论如何,我只是假设这就是他的意思,因为任何不足之处都可能会导致崩溃或无法使用。 - user34537
如果你只是单线程的话,你不需要MT解决方案。标记和清除可以找到循环引用,并且永远不会错误地收集任何东西,它的开销只是在每个分配块的头部有一个单独保留位,这比三倍还要少得多。(虽然在多线程场景下,它可能不是最时间有效的算法。)结合引用计数,如果m&s阶段被足够频繁地调用,内存开销将保持较低水平。 - Christopher Creutzig

3
所以我想知道是否有任何关于垃圾收集开销的研究或实际数字。大约十年前,我研究了两个等价的程序,一个是使用C++和STL(在Linux上使用GCC),另一个是使用OCaml和其垃圾收集器。我发现C++平均使用了两倍的内存。我尝试通过编写自定义的STL分配器来改善它,但从未能够匹配OCaml的内存占用。

此外,GC通常会进行大量压缩,进一步减少内存占用。因此,我要对比典型的非托管代码(例如使用标准库集合的C ++)是否存在内存开销的假设提出挑战。


你能解释一下为什么C++有这么多开销吗?我无法想象为什么C++会使用更多的开销,因为它非常接近底层。 - user34537
2
主要原因是C++析构函数将收集工作推迟到作用域结束之后,这通常比子图不可访问时间更长,导致大量所谓的“浮动”垃圾。这一问题加剧了子图本身就包含整个集合而非仅有偶尔出现的数据条目的事实。相比之下,OCaml采用了极其积极的垃圾回收机制。 - J D

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