为什么要关注大对象堆(Large Object Heap,LOH)?

125

我已经了解了代和大对象堆,但我仍然无法理解拥有大对象堆的意义(或益处)是什么?

如果CLR只依靠第二代来存储大型对象(考虑到Gen0和Gen1的阈值过小),可能会出现什么问题(在性能或内存方面)?


9
这让我有两个问题要问.NET设计师:1. 为什么不在抛出OutOfMemoryException之前调用LOH碎片整理呢?2. 为什么不让LOH对象有一个聚集在一起的倾向(大对象偏向堆的末尾,小对象偏向堆的开头)? - Jacob Brewer
5个回答

224
一次垃圾回收不仅仅是清除没有被引用的对象,它还会压缩堆。这是非常重要的优化。它不仅使内存使用更加高效(没有未使用的空洞),而且使CPU缓存更加高效。在现代处理器上,缓存是一个非常重要的问题,它们比内存总线快一个数量级。
通过复制字节来完成压缩。然而,这需要时间。对象越大,复制它的成本就越高,超过了可能的CPU缓存使用改进。
因此,他们进行了一堆基准测试来确定平衡点。并将85,000个字节作为截断点,其中复制不再改善性能。对于双精度数组有一个特殊例外,当数组有超过1000个元素时,它们被认为是“大型”的。这是32位代码的另一个优化,大型对象堆分配器具有特殊属性,即它分配对齐于8的地址的内存,而常规的生成分配器只分配对齐于4的地址。对于双倍精度,这种对齐是一个大问题,读取或写入不对齐的双倍精度会非常昂贵。奇怪的是,微软稀疏的信息从未提到长数组,不确定出了什么问题。
值得一提的是,有很多程序员对大型对象堆不被压缩感到烦恼。这总是在他们编写消耗半个可用地址空间以上的程序时触发。随后使用诸如内存分析器之类的工具查找程序为什么失败,即使还有很多未使用的虚拟内存可用。这样的工具显示LOH中的空洞,以前大对象存在但已垃圾回收的未使用内存块。这就是LOH所付出的必然代价,空洞只能被等于或小于该大小的对象分配使用。真正的问题是假设程序随时都应该允许使用所有虚拟内存。

在64位操作系统上运行代码,可以完全消除一个问题。64位进程有8TB的虚拟内存地址空间,比32位进程多了3个数量级。你绝对不会用尽内存。

简而言之,LOH使代码运行更有效率。但代价是使用可用的虚拟内存地址空间不够高效。


更新,.NET 4.5.1现在支持压缩LOH,GCSettings.LargeObjectHeapCompactionMode属性。请注意后果。


4
@Hans Passant,您能否解释一下x64系统的情况,您的意思是说这个问题完全消失了吗?(注:已翻译为中文) - Johnny_D
6
当然,在x64上也存在碎片化问题。在运行服务器进程几天之后,它就会出现。 - Lothar
1
嗯,不要低估三个数量级。当你需要进行4TB堆的垃圾回收时,你会发现这需要多长时间,而且在接近这个阈值之前你无法避免这种情况。 - Hans Passant
2
@HansPassant,您能否详细说明一下这个语句的含义:“在接近4TB堆大小之前,你无法避免发现垃圾回收需要多长时间。” - relatively_random
1
@HansPassant 当SOH中的对象变得大于85kB时,它会移动到LOH还是仍然留在SOH? - XTL
显示剩余3条评论

14
小对象堆(SOH)和大对象堆(LOH)的本质区别在于,当收集时SOH内存会被压缩,而LOH不会,如这篇文章所示。 压缩大对象成本高。 如文章中的示例,假设在内存中移动一个字节需要2个周期,则在2GHz计算机上压缩8MB对象需要8毫秒,这是一笔巨大的开销。 考虑到大对象(大多数情况下是数组)在实践中非常常见,我想这就是为什么微软将大对象固定在内存中并提出LOH的原因。

另外,根据这篇文章,LOH通常不会产生内存碎片问题。


1
将大量数据加载到托管对象中通常会使 LOH 压缩的 8 毫秒成本相形见绌。在实践中,在大多数大数据应用程序中,LOH 成本与应用程序性能的其余部分相比微不足道。 - Shiv

11
如果对象的大小大于某个固定值(在.NET 1中为85000字节),则CLR将其放入大对象堆中。这优化了以下内容:
  1. 对象分配(小对象不会与大对象混合)
  2. 垃圾回收(仅在完全GC时才收集LOH)
  3. 内存碎片整理(LOH很少被压缩)

5
原则是,一个进程不太可能(并且很可能是设计不良)创建大量短生命周期的大型对象,因此CLR将大型对象分配给单独的堆,对其进行垃圾回收,并在不同的时间表上运行。 有关详细信息,请参见http://msdn.microsoft.com/en-us/magazine/cc534993.aspx

同时,将大型对象放置在第二代上可能会损害性能,因为压缩内存需要很长时间,特别是如果释放了少量内存并且需要复制巨大的对象到新位置。当前的 LOH 由于性能原因而不进行压缩。 - Christopher Currens
我认为这只是糟糕的设计,因为垃圾回收器无法很好地处理它。 - CodesInChaos
@CodeInChaos 看来,在 .NET 4.5 中会有一些改进。 (http://blogs.msdn.com/b/dotnet/archive/2011/10/03/large-object-heap-improvements-in-net-4-5.aspx) - Christian.K
1
@CodeInChaos:虽然在尝试从短期LOH对象中回收内存之前,系统等待进行gen2收集可能是有意义的,但我无法看到在gen0和gen1收集期间无条件地声明LOH对象(以及它们持有引用的任何对象)为活动状态的任何性能优势。这种假设是否可以实现一些优化? - supercat
@supercat 我看了Myles McDonnell提到的链接。我的理解是:
  1. LOH集合发生在第2代GC中。
  2. LOH集合不包括压缩(在文章撰写时)。相反,它将死对象标记为可重用,并且如果足够大,则这些空洞将用于未来的LOH分配。
由于第1点,考虑到如果第2代中有许多对象,则第2代GC会很慢,因此我认为在这种情况下尽可能避免使用LOH会更好。
- robbie fan
@robbiefan:如果在进行G0垃圾回收时,仅存在一个全新的LOH对象持有对L0或L1对象的引用,我认为最好尝试确定是否存在对LOH对象的任何引用,只有在存在引用时才将L0或L1对象提升,而不是无条件地保留LOH对象中保存的所有引用,而不考虑是否存在对它的引用。 - supercat

0

我不是CLR专家,但我认为为大对象设置专用堆可以防止对现有分代堆的不必要GC扫描。分配大对象需要大量连续的自由内存。为了从分散的分代堆中提供这些内存,“洞”需要频繁压缩(只能在GC周期中完成)。


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