大型数组和LOH碎片化。什么是公认的惯例?

16

我有一个活跃的问题是关于一些无望的内存问题,可能涉及LOH碎片以及其他未知因素。请点击此处查看。

我的问题是,什么是被接受的做法呢? 如果我的应用程序需要使用Visual C#完成,并且需要处理int [4000000]这样规模的大数组,如何避免垃圾回收器拒绝处理LOH的情况发生?

似乎我被迫将任何大数组设置为全局变量,并且从来不在它们周围使用“new”这个词。所以,我只能留下笨重的全局数组,带有“maxindex”变量,而不是通过函数传递的整洁大小的数组。

我一直被告知这是不好的做法。有什么替代方法吗?

是否有类似于System.GC.CollectLOH("Seriously")的功能呢? 是否有可能将垃圾收集外包给System.GC之外的其他东西?

总之,处理大于85Kb的变量通常采用什么样的方法呢?

6个回答

33

首先,垃圾收集器确实会收集大对象堆(LOH),因此不要因其存在而立即感到恐慌。当第二代被收集时,LOH也会被收集。

区别在于,LOH不会被压缩,这意味着如果您有一个具有长生命周期的对象,则将有效地将LOH分成两个部分--该对象之前和之后的区域。如果这种行为持续发生,则可能出现这样一种情况:长寿命对象之间的空间不足以进行后续分配,.NET需要分配更多内存以放置您的大对象,即LOH变得分散。

现在,话虽如此,如果在其末尾的区域完全没有活动对象,则LOH的大小可以缩小,因此唯一的问题是如果您在其中留下对象很长时间(例如应用程序的持续时间)。

.NET 4.5.1开始,可以对LOH进行压缩,请参见GCSettings.LargeObjectHeapCompactionMode属性。

避免LOH分散的策略包括:

  • 避免创建长期存在的大对象,基本上只需大数组或包装大数组的对象(例如MemoryStream,它包装了一个字节数组),因为其他东西都不那么大(复杂对象的组件单独存储在堆上,因此很少是非常大的)。还要注意大型词典和列表,因为它们在内部使用数组。
  • 注意双重数组--这些进入LOH的阈值要小得多 - 我记不清确切的数字,但只有几千个。
  • 如果需要MemoryStream,请考虑制作一个基于多个较小数组的分块版本,而不是一个巨大的数组。还可以制作使用分块来避免首先将内容放入LOH的IList和IDictionary的自定义版本。
  • 避免非常长的远程调用,因为Remoting会在调用期间大量使用MemoryStreams,这可能导致LOH在调用期间发生碎片。
  • 注意字符串内联(string interning) - 出于某种原因,它们被存储为LOH上的页,如果应用程序继续遇到需要内联的新字符串,则可能会导致严重的碎片。因此,除非字符串集是已知有限的并且完整集在应用程序的早期阶段就能够遇到,否则请避免使用string.Intern。(参见我的早期问题。)
  • 使用Son of Strike来查看确切使用LOH内存的内容。有关如何执行此操作的详细信息,请参见这个问题
  • 考虑池化大数组
  • 编辑:双倍数组的LOH阈值似乎为8k。


    这可能是一个不太好的问题...但我怎样才能获得CDB和SOS?谷歌似乎并不知道。我跟随的所有链接都指向一些MSDN搜索结果页面,其中列出了“DDK3”等听起来不像CDB的其他内容。 - Gorchestopher H
    1
    下载“Windows调试工具”。CDB是控制台调试器,WinDbg是其“图形”等效物(仍然是文本模式,但呈现在MDI窗口中)。SOS随.NET一起提供:“%systmeroot%\microsoft.net\framework\v2.0.50727\sos.dll”(.NET 3使用.NET 2 SOS.dll,.NET 4带有自己的版本)。 - Paul Ruane
    2
    此外,显然您可以从Visual Studio的即时窗口加载SOS(避免使用CDB),但我从未尝试过。 - Paul Ruane
    嗯...也许LOH碎片化并不是我的问题。我没有看到任何东西旁边有“free”这个词... - Gorchestopher H

    8

    这是一个老问题,但我认为更新答案并介绍.NET引入的变化不会有害。现在可以对大对象堆进行碎片整理。显然,首选应该是确保做出了最佳设计选择,但现在有这个选项也很好。

    https://msdn.microsoft.com/en-us/library/xe0c2357(v=vs.110).aspx

    "从.NET Framework 4.5.1开始,您可以通过将GCSettings.LargeObjectHeapCompactionMode属性设置为GCLargeObjectHeapCompactionMode.CompactOnce来压缩大对象堆(LOH),然后再调用Collect方法,如下面的示例所示。"
    "GCSettings可在System.Runtime命名空间中找到。"
    GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
    GC.Collect(); 
    

    这非常有用 - 我们的应用程序在它们执行大量“缓存重新加载”后调用它(以及诱导GC),这使得.NET GC可以回收所有旧的长寿命缓存对象并将所有新缓存压缩到LOH中。大多数缓存都是大型的,并且会提前/不经常创建。但是,可能会溢出到LOH的动态缓存效果不太好,因为很难知道请求.NET在何时压缩LOH是一个“好时机”。 - user2864740

    7
    首先要考虑的是将数组拆分成较小的数组,这样它们就不会占用GC需要放置在LOH中的内存。您可以将数组分割成一些小的数组,例如每个数组包含10,000个元素,并构建一个对象,该对象将根据传递的索引器知道要查找哪个数组。
    现在我还没有看到代码,但我也会质疑为什么您需要一个那么大的数组。我可能会考虑重构代码,以便不需要一次性将所有信息存储在内存中。

    我的应用程序接收潜在尺寸为1024x720的图像,根据像素强度将其转换为“高度”矩阵。然后,它将该矩阵渲染成OpenGL中的表面地图。因此,我确实需要同时获取所有这些数据。 - Gorchestopher H
    1
    顺便说一句:10k元素是一个“好的阈值”,因为10k * 8字节(64位对象引用)< 85k LOH阈值。使用单独的计算将双精度数组移动到LOH。 - user2864740

    5
    你理解错了。你不需要一个大小为4000000的数组,也绝对不需要调用垃圾回收器。
    请编写自己的IList实现,例如“PagedList”。
    将项目存储在65536个元素的数组中。
    创建一个数组来保存页面。
    这样可以通过一次重定向访问所有元素。由于单个数组较小,因此碎片化不是问题...
    如果是...那么请重复使用页面。不要在dispose时将它们丢弃,而是将它们放在静态的“PageList”上,并首先从那里获取它们。所有这些都可以在您的类中透明地完成。
    真正好的事情是,这个列表在内存使用方面非常动态。您可能希望调整持有者数组(重定向器)。即使没有,每页仅约为512kb数据。
    第二级数组基本上每字节64k - 这是一个类的8个字节(32位系统上的512kb,256kb),或一个结构体字节的64kb。
    技术上:
    将 int[] 转换为 int[][]
    决定哪个更好,32位或64位;)两者都有优点和缺点。
    在任何语言中,处理像那样的一个大数组都很难控制 - 如果必须这样做,那么......基本上......在程序启动时分配并永远不重新创建。唯一的解决方案。

    那么,一个交错数组int[][]并不需要使用连续的内存吗? 像int[,]这样的数组是否行为相同? - Gorchestopher H
    我相信新的int[,]会分配一个连续的块。而交错数组则是分别分配的。 - Paul Williams
    请注意:LOH选择在内存大小时启动,而不是元素计数。 LOH阈值为85k 字节,因此在64位系统上大约为10k(x8字节对象引用)个元素。即使使用int [],也仅有约20k的元素。两个阈值都小于建议的65k元素,这些元素仍将最终位于LOH中。 - user2864740

    1
    这是一个老问题,但是在.NET Standard 1.1(.NET Core, .NET Framework 4.5.1+)中,还有另一种可能的解决方案:使用System.Buffers包中的ArrayPool,我们可以池化数组以避免这个问题。

    0
    我在上面的答案中添加了一些阐述,关于问题如何出现。LOH的碎片化不仅取决于对象的寿命长短,而且如果你有多个线程,每个线程都创建大型列表进入LOH,那么第一个线程需要增加其列表大小,但下一个连续的内存位已经被第二个线程的列表占用,因此运行时将为第一个线程的列表分配新的内存 - 留下一个相当大的空洞。这是我继承的一个项目目前正在发生的情况,即使LOH大约为4.5 MB,运行时也有总共117MB的可用内存,但最大的可用内存段是28MB。
    另一种没有多个线程的情况是,如果你有多个列表在某种循环中被添加,并且每个列表都超出了最初分配给它的内存,那么它们会互相跃迁,因为它们超出了它们分配的空间。
    一个有用的链接是:https://www.simple-talk.com/dotnet/.net-framework/the-dangers-of-the-large-object-heap/ 仍然在寻找解决方案,其中一种选择可能是使用某种池对象并从池中请求进行工作。如果你处理大型数组,那么另一个选项是开发自定义集合,例如集合的集合,这样你就不会只有一个巨大的列表,而是将其分成较小的列表,每个列表都避免LOH。

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