我应该在使用大对象堆后立即调用GC.Collect以防止碎片

时间:2009-12-18 21:18:20

标签: c# memory-management out-of-memory fragmentation large-object-heap

我的应用程序对大对象进行了大量的二进制序列化和压缩。未压缩的序列化数据集大约为14 MB。压缩它是1.5 MB左右。我发现每当我在我的数据集上调用serialize方法时,我的大对象堆性能计数器就会从1 MB以下跳到大约90 MB。我也知道在一个相对繁重的系统下,通常在运行(天)一段时间后,这个序列化过程发生了一段时间,已知应用程序在调用此序列化方法时会丢失内存消除,即使在那里似乎是充足的记忆。我猜测碎片是个问题(虽然我不能说我百分百肯定,但我很接近)

最简单的短期修复(我想我正在寻找一个短期和长期的答案)我能想到的是在完成序列化过程后立即调用GC.Collect。在我看来,这将垃圾收集来自LOH的对象,并且可能在其他对象被添加到它之前这样做。这将允许其他对象紧密地紧贴堆中的其余对象,而不会造成太多碎片。

除了这个荒谬的90MB分配之外,我认为我没有其他任何使用丢失的LOH。这种90 MB的分配也相对较少(每4小时一次)。我们当然仍然会有1.5 MB的阵列,也许还有其他一些较小的序列化对象。

有什么想法吗?

因良好回复而更新

这是我的代码,它完成了这项工作。我实际上已经尝试将其更改为压缩WHILE序列化,以便序列化同时序列化为流并且我没有得到更好的结果。我还尝试将内存流预先分配到100 MB并尝试连续两次使用相同的流,LOH最高可达180 MB。我正在使用Process Explorer来监控它。这太疯狂了。我想我接下来会尝试UnmanagedMemoryStream的想法。

如果你不习惯,我会鼓励你们尝试一下。它不一定是这个确切的代码。只需序列化一个大型数据集,你就会得到令人惊讶的结果(我有很多表,15周围有大量的字符串和列)

        byte[] bytes;
        System.Runtime.Serialization.Formatters.Binary.BinaryFormatter serializer =
        new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();            
        System.IO.MemoryStream memStream = new System.IO.MemoryStream();
        serializer.Serialize(memStream, obj);
        bytes = CompressionHelper.CompressBytes(memStream.ToArray());
        memStream.Dispose();
        return bytes;

使用UnmanagedMemoryStream尝试二进制序列化后更新

即使我序列化为UnmanagedMemoryStream,LOH也会跳到相同的大小。似乎无论我做什么,调用BinaryFormatter来序列化这个大对象都会使用LOH。至于预分配,它似乎没什么帮助。假设我预分配说我预分配100MB,然后我序列化,它将使用170 MB。这是代码。甚至比上面的代码更简单

BinaryFormatter serializer  = new BinaryFormatter();
MemoryStream memoryStream = new MemoryStream(1024*1024*100);
GC.Collect();
serializer.Serialize(memoryStream, assetDS);

中间的GC.Collect()只是为了更新LOH性能计数器。您将看到它将分配正确的100 MB。但是当你调用序列化时,你会注意到它似乎在你已经分配的100之上添加了它。

6 个答案:

答案 0 :(得分:4)

注意像MemoryStream这样的集合类和流在.NET中的工作方式。它们有一个底层缓冲区,一个简单的数组。每当集合或流缓冲区增长超出分配的数组大小时,数组就会重新分配,现在是之前大小的两倍。

这会在LOH中导致阵列的许多副本。您的14MB数据集将开始使用128KB的LOH,然后再使用256KB,然后再使用512KB等等。最后一个,实际使用的,将是大约16MB。 LOH包含这些的总和,大约30MB,其中只有一个在实际使用中。

这样做三次没有gen2集合,你的LOH已经增长到90MB。

通过将缓冲区预分配到预期大小来避免这种情况。 MemoryStream有一个构造函数,它具有初始容量。所有集合类都是如此。在废除所有引用之后调用GC.Collect()可以帮助疏通LOH并清除那些中间缓冲区,代价是过早堵塞gen1和gen2堆。

答案 1 :(得分:3)

不幸的是,我能解决这个问题的唯一方法就是以块的形式分解数据,以免在LOH上分配大块。这里提出的所有答案都很好,预计会有效,但事实并非如此。似乎.NET中的二进制序列化(使用.NET 2.0 SP2)在引擎盖下发挥了自己的神奇作用,阻止用户控制内存分配。

然后回答问题是“这不太可行”。在使用.NET序列化时,最好的办法是将较大的对象序列化为较小的块。对于所有其他场景,上面提到的答案都很棒。

答案 2 :(得分:2)

90MB的RAM并不多。

除非遇到问题,否则请避免调用GC.Collect。如果您遇到问题,并且没有更好的解决方法,请尝试调用GC.Collect并查看问题是否已解决。

答案 3 :(得分:0)

如果您确实需要将LOH用于服务或需要长时间运行的东西,则需要使用从未解除分配的缓冲池,并且理想情况下可以在启动时分配。这意味着你必须自己做这个“记忆管理”。

根据您对此内存的操作,您可能还必须p / Invoke到所选部分的本机代码,以避免必须调用某些.NET API,强制您将数据放在新分配的空间中LOH。

这是关于问题的一篇很好的起点文章:http://blogs.msdn.com/maoni/archive/2004/12/19/327149.aspx

如果你的GC技巧能够奏效,我会认为你很幸运,只有在系统中同时没有多少事情发生时它才真正有用。如果你有并行工作,这只会稍微延迟不可避免的事情。

另请阅读有关GC.Collect.IIRC的文档,GC.Collect(n)仅表示它收集的数量不超过第n代 - 而不是实际上GETS生成n。

答案 4 :(得分:0)

不要担心LOH尺寸会上升。担心分配/解除分配LOH。 .Net对LOH非常愚蠢 - 而不是将LOH对象远离常规堆分配,而是在下一个可用的VM页面分配。我有一个3D应用程序,可以分配/释放LOH和常规对象 - 结果(如DebugDiag转储报告中所示)是小堆和大堆的页面在整个RAM中交替,直到没有大块应用程序2 GB VM空间。在可能的情况下,解决方案是分配一次您需要的,然后不释放它 - 下次重新使用它。

使用DebugDiag分析您的流程。了解VM如何逐渐向2 GB地址标记爬行。然后做出改变以防止这种情况发生。

答案 5 :(得分:0)

我同意其他一些海报,您可能希望尝试使用技巧来使用.NET Framework,而不是试图通过GC.Collect强制它与您合作。

您可能会发现这个Channel 9 video有用,它讨论了减轻垃圾收集器压力的方法。