为什么使用大对象堆,为什么我们要关心?


105

我已经阅读了有关“世代”和“大对象堆”的信息。但是我仍然不明白拥有大对象堆的意义(或好处)是什么?

如果CLR仅依赖第2代(考虑到Gen0和Gen1的阈值很小,无法处理大对象)来存储大对象,那可能会出错(在性能或内存方面)?


6
这给.NET设计人员两个问题:1.为什么在引发OutOfMemoryException之前不调用LOH碎片整理?2.为什么没有LOH对象具有保持在一起的亲和力(大喜欢堆的末端,小喜欢开始的地方)
Jacob Brewer 2014年

Answers:


195

垃圾回收不仅摆脱了未引用的对象,而且还压缩了堆。这是非常重要的优化。它不仅使内存使用效率更高(没有未使用的漏洞),而且使CPU缓存效率更高。高速缓存在现代处理器上确实非常重要,它们比内存总线快一个数量级。

压缩仅通过复制字节即可完成。但是,这需要时间。对象越大,复制它的成本越有可能超过可能的CPU缓存使用率改进。

因此,他们运行了一系列基准来确定收支平衡点。达到85,000字节作为截止点,复制不再能改善性能。除了double数组的特殊例外,当数组具有1000个以上的元素时,它们被视为“大”数组。这是对32位代码的另一种优化,大对象堆分配器具有特殊的属性,即它在对齐8的地址上分配内存,这与常规世代分配器只分配对齐4的地址不同。 ,读取或写入未对齐的double十分昂贵。奇怪的是,稀疏的Microsoft信息从未提及过很长的数组,不确定这是怎么回事。

Fwiw,关于大型对象堆没有压缩的很多程序员的担忧。当他们编写的程序占用了整个可用地址空间的一半以上时,就会始终触发此操作。然后使用诸如内存探查器之类的工具来找出为什么程序轰炸的原因,即使仍有大量未使用的虚拟内存可用。这样的工具可以显示LOH中的漏洞,即以前未使用的大块内存在其中的大型对象居住但被收集到的垃圾。这就是LOH的必然价格,只能通过分配大小相等或较小的对象来重复使用该孔。真正的问题是假设应允许程序在任何时间消耗所有虚拟内存。

通过仅在64位操作系统上运行代码,该问题可以完全消失。64位进程具有8 TB的虚拟内存地址空间可用,比32位进程大3个数量级。您就是不会洞洞。

长话短说,LOH使代码运行更有效率。以使用可用虚拟内存地址空间的效率较低为代价。


UPDATE.NET 4.5.1现在支持压缩LOH GCSettings.LargeObjectHeapCompactionMode属性。请当心后果。


3
@Hans Passant,能否请您说明一下x64系统,您是说这个问题完全消失了?
Johnny_D 2012年

LOH的一些实现细节很有意义,但有些使我困惑。例如,我可以理解,如果创建和被遗弃的许多大型对象,一般可以希望删除它们集体在第二代收集比GEN0收藏零碎的,但如果一个创建并放弃例如22000个字符串数组到没有外部引用存在,使Gen0和Gen1集合将所有22,000个字符串标记为“活动”有什么优势,而不必考虑数组是否存在任何引用?
2012年

6
当然,碎片问题在x64上是一样的。开始运行服务器进程只需要几天的时间
。– Lothar

1
嗯,不,永远不要低估3个数量级。垃圾回收需要多长时间才能发现4 TB的堆。
汉斯·帕桑

2
@HansPassant请您详细说明一下:“垃圾收集4 TB的内存需要花费多长时间,您很快就会发现它,直到接近它为止。”
相对

9

如果对象的大小大于某个固定值(.NET 1中为85000字节),则CLR会将其放入大对象堆中。优化:

  1. 对象分配(小对象不与大对象混合)
  2. 垃圾收集(仅在完整GC上收集LOH)
  3. 内存碎片整理(永远不会很少压缩LOH )

9

小对象堆(SOH)和大对象堆(LOH)的本质区别在于,如本文所述,SOH中的内存在收集时会压缩,而LOH不会。压缩大型对象的成本很高。与本文中的示例类似,假设在内存中移动一个字节需要2个周期,然后在2GHz的计算机中压缩8MB的对象需要8ms,这是很大的成本。考虑到大对象(在大多数情况下为数组)在实践中很常见,我想这就是Microsoft将大对象固定在内存中并提出LOH的原因。

顺便说一句,根据这篇文章,LOH通常不会产生内存碎片问题。


1
将大量数据加载到托管对象中通常会使压缩LOH的8毫秒成本相形见war。实际上,在大多数大数据应用程序中,LOH成本仅次于其余应用程序性能。
Shiv 2016年

3

原理是,进程不太可能(并且很可能是错误的设计)会创建大量短寿命的大对象,因此CLR将大对象分配给单独的堆,并在该堆上按与常规堆不同的时间表运行GC。http://msdn.microsoft.com/zh-CN/magazine/cc534993.aspx


同样,将大型对象放在第二代上可能会损害性能,因为压缩内存需要很长时间,特别是如果释放了少量对象并且必须将巨大的对象复制到新位置时。由于性能原因,当前的LOH未压缩。
Christopher Currens 2012年

我认为这只是一个糟糕的设计,因为GC无法很好地处理它。
CodesInChaos

@CodeInChaos显然,.NET 4.5
Christian.K

1
@CodeInChaos:虽然系统可能要等到gen2集合才尝试从短命的LOH对象中回收内存,但我看不出声明LOH对象(及其所持有的任何对象)在性能上的优势引用)在gen0和gen1收集期间无条件地存在。通过这种假设是否可以进行一些优化?
supercat '02

@supercat我看了Myles McDonnell提到的链接。我的理解是:1. LOH收集发生在第2代GC中。2. LOH集合不包括压缩(在撰写本文时)。取而代之的是,它将死对象标记为可重用,并且如果足够大,这些漏洞将为将来的LOH分配服务。从第1点开始,考虑到如果第2代中有许多对象,第2代GC会比较慢,我认为在这种情况下最好避免使用LOH更好。
罗比·范

0

我不是CLR方面的专家,但我可以想象拥有一个专用于大型对象的堆可以防止对现有世代堆进行不必要的GC扫描。分配大对象需要大量连续的空闲内存。为了提供从世代堆中分散的“漏洞”中获得的信息,您需要频繁压缩(仅通过GC循环完成)。

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.