13

我有一个关于一些无望的内存问题的其他活跃问题这些问题可能涉及 LOH 碎片以及可能的其他未知数。

我现在的问题是,公认的做事方式是什么?如果我的应用程序需要在 Visual C# 中完成,并且需要处理 int[4000000] 的大数组,我怎么能被垃圾收集器拒绝处理 LOH 所注定呢?

似乎我被迫将任何大型数组设为全局,并且永远不要在其中任何一个周围使用“新”这个词。所以,我留下了带有“maxindex”变量的不优雅的全局数组,而不是由函数传递的大小整齐的数组。

我一直被告知这是不好的做法。有什么选择?

是否有某种功能System.GC.CollectLOH("Seriously")?有没有办法将垃圾收集外包给 System.GC 以外的东西?

无论如何,处理大型(> 85Kb)变量的普遍接受的规则是什么?

4

6 回答 6

31

首先,垃圾收集器确实会收集 LOH,所以不要立即被它的出现吓到。收集第 2 代时收集 LOH。

不同之处在于 LOH 不会被压缩,这意味着如果你有一个生命周期很长的对象,那么你将有效地将 LOH 分成两部分——对象之前的区域和对象之后的区域。如果这种行为继续发生,那么您最终可能会遇到这样的情况:长寿命对象之间的空间对于后续分配来说不够大,并且 .NET 必须分配越来越多的内存才能放置大对象,即 LOH变得支离破碎。

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

从 .NET 4.5.1 开始,可以压缩 LOH,请参阅GCSettings.LargeObjectHeapCompactionMode属性。

避免 LOH 碎片化的策略是:

  • 避免创建悬挂的大物体。基本上这只是意味着大型数组,或包装大型数组的对象(例如包装字节数组的 MemoryStream),因为没有其他东西那么大(复杂对象的组件单独存储在堆上,因此很少很大)。还要注意大型字典和列表,因为它们在内部使用数组。
  • 注意双数组——这些进入 LOH 的阈值要小得多——我不记得确切的数字,但它只有几千个。
  • 如果您需要 MemoryStream,请考虑制作一个支持多个较小数组而不是一个巨大数组的分块版本。您还可以制作 IList 和 IDictionary 的自定义版本,它们使用分块来避免内容首先出现在 LOH 中。
  • 避免很长的远程调用,因为远程处理大量使用 MemoryStreams,这可能会在调用期间分割 LOH。
  • 注意字符串实习——出于某种原因,这些作为页面存储在 LOH 上,如果您的应用程序继续遇到要实习的新字符串,可能会导致严重的碎片,即避免使用 string.Intern,除非已知字符串集是有限的并且在应用程序生命的早期就遇到了全套。(请参阅我之前的问题。)
  • 使用“罢工之子”查看究竟是什么在使用 LOH 内存。有关如何执行此操作的详细信息,请再次查看此问题。
  • 考虑汇集大型数组

编辑:双数组的 LOH 阈值似乎是 8k。

于 2010-03-08T15:59:47.167 回答
8

这是一个老问题,但我认为使用 .NET 中引入的更改来更新答案并没有什么坏处。现在可以对大对象堆进行碎片整理。显然,第一选择应该是确保做出最佳的设计选择,但现在有了这个选择真是太好了。

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

“从 .NET Framework 4.5.1 开始,您可以通过在调用 Collect 方法之前将 GCSettings.LargeObjectHeapCompactionMode 属性设置为 GCLargeObjectHeapCompactionMode.CompactOnce 来压缩大对象堆 (LOH),如下例所示。”

GCSettings 可以在 System.Runtime 命名空间中找到

GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect(); 
于 2015-04-08T16:20:29.357 回答
7

首先想到的是将数组拆分成更小的数组,这样它们就达不到 GC 将 LOH 放入其中所需的内存。您可以将数组拆分为较小的数组,例如 10,000 个,并构建一个对象,该对象会根据您传递的索引器知道要查看哪个数组。

现在我还没有看到代码,但我也会质疑为什么你需要一个那么大的数组。我可能会考虑重构代码,这样所有信息就不需要一次存储在内存中。

于 2010-03-08T15:18:56.913 回答
5

你弄错了。您不需要数组大小为 4000000,而且您绝对不需要调用垃圾收集器。

  • 编写您自己的 IList 实现。像“分页列表”
  • 将项目存储在 65536 个元素的数组中。
  • 创建一个数组数组来保存页面。

这允许您基本上只使用一个重定向来访问所有元素。而且,由于单个数组更小,碎片不是问题......

...如果是...然后重用页面。不要把它们扔掉,把它们放在一个静态的“PageList”上,然后先从那里拉出来。所有这些都可以在您的班级中透明地完成。

真正的好处是这个 List 在内存使用方面是非常动态的。您可能想要调整持有者数组(重定向器)的大小。即使没有,每页也只有大约 512kb 数据。

二级数组基本上每个字节有 64k - 一个类是 8 个字节(每页 512kb,32 位 256kb),或每个结构字节 64kb。

技术上:

把 int[] 变成 int[][]

根据需要确定 32 位还是 64 位更好;)都有优点和缺点。

处理这样的一个大型数组在任何语言中都是笨拙的 - 如果你愿意,那么......基本上......在程序开始时分配并且永远不会重新创建。唯一的解决方案。

于 2010-03-08T15:29:05.697 回答
1

这是一个老问题,但对于 .NET Standard 1.1(.NET Core、.NET Framework 4.5.1+)还有另一种可能的解决方案:

ArrayPool<T>在包中使用System.Buffers,我们可以池化数组来避免这个问题。

于 2019-12-22T12:15:34.330 回答
0

就问题如何出现而言,我正在对上述答案进行详细说明。LOH 的碎片不仅取决于对象的寿命长,而且如果您遇到有多个线程并且每个线程都在创建进入 LOH 的大列表的情况,那么您可能会遇到第一个线程的情况需要增加它的 List 但下一个连续的内存位已经被第二个线程的 List 占用,因此运行时将为第一个线程 List 分配新内存 - 留下一个相当大的洞。这是我继承的一个项目目前正在发生的事情,因此即使 LOH 大约为 4.5 MB,运行时总共有 117MB 可用内存,但最大的可用内存段为 28MB。

在没有多个线程的情况下可能发生这种情况的另一种方式是,如果您在某种循环中添加了多个列表,并且随着每个列表的扩展超出最初分配给它的内存,那么当它们超出分配的空间时,每个列表都会越过另一个.

一个有用的链接是:https ://www.simple-talk.com/dotnet/.net-framework/the-dangers-of-the-large-object-heap/

仍在为此寻找解决方案,一种选择可能是在工作时使用某种池对象并从池中请求。如果您正在处理大型数组,那么另一种选择是开发一个自定义集合,例如一个集合集合,这样您就不会只有一个巨大的列表,而是将它分解成更小的列表,每个列表都避免了 LOH。

于 2013-05-16T10:26:14.360 回答