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

9
我的应用程序需要大量二进制序列化和压缩大对象。序列化数据集未经压缩时约为14 MB,压缩后约为1.5 MB。我发现每当我对数据集调用serialize方法时,我的大对象堆性能计数器会从不到1 MB跳到大约90 MB。我还知道,在相对繁忙的系统下,在运行一段时间(几天)且进行了几次序列化处理后,尽管内存似乎很充足,但该应用程序有可能在调用此序列化方法时抛出内存不足异常。我猜测问题是碎片化(虽然我不能说我100%确定,但我非常接近)。
我能想到的最简单的短期解决方案(我猜我正在寻找一个短期和长期的答案)是在完成序列化处理后立即调用GC.Collect。在我看来,这将从LOH垃圾收集对象,并且在其他对象被添加到它之前可能会这样做。这将使其他对象紧密地贴合堆中剩余的对象,而不会引起太多的碎片化。
除了这个荒谬的90MB分配外,我认为我没有任何其他使用LOH的东西了。这个90 MB分配也相对罕见(大约每4个小时)。当然,我们仍将在其中保留1.5 MB的数组和可能还有一些较小的序列化对象。
有任何想法吗?
更新:由于好的反馈而更新
这里是我的代码,它执行了该工作。实际上,我尝试更改此代码以便在序列化时进行压缩,以便序列化同时将序列化到流中,但结果并不理想。我还尝试将内存流预分配到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,然后我进行序列化,它将使用170MB。以下是代码,比上面的代码更简单。

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

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


4
有没有办法直接将数据序列化到“Stream”中,而不使用大对象堆(LOH)作为缓冲区? - Marc Gravell
6个回答

4
.NET中的集合类和流(如MemoryStream)的工作方式需要注意。它们都有一个基础缓冲区,即一个简单的数组。每当集合或流缓冲区增长超过数组的分配大小时,该数组就会被重新分配,此时大小为先前大小的两倍。
这可能会导致LOH中出现许多数组副本。您的14MB数据集将从128KB开始使用LOH,然后再占用256KB,然后再占用512KB等等。最后一个实际使用的大小将约为16MB。 LOH包含这些大小的总和,约30MB,其中只有一个正在使用。
如果不进行gen2收集,连续三次这样操作后,LOH将增长到90MB。
为避免这种情况,应预先将缓冲区分配到预期大小。MemoryStream有一个带有初始容量的构造函数,所有集合类都有。在您将所有引用设置为null后调用GC.Collect()可以帮助清除LOH并清除那些中间缓冲区,但代价是过早地阻塞了gen1和gen2堆栈。

3
很不幸,我只能通过将数据分块来修复此问题,以避免在大型对象堆上分配大块内存。所有提出的答案都很好,并且预计可以工作,但实际并未如此。似乎 .NET 中的二进制序列化(使用 .NET 2.0 SP2)在幕后进行了一些小魔术,阻止用户对内存分配进行控制。
回答这个问题的方法是“这不太可能会起作用”。当涉及到使用 .NET 序列化时,最好的办法是将大型对象序列化为较小的块。对于所有其他情况,上述答案都非常好。

2

90MB的RAM并不算多。

除非你遇到了问题,否则请避免调用GC.Collect。如果你确实遇到了问题,并且没有更好的解决方法,请尝试调用GC.Collect,看看能否解决你的问题。


0

如果您确实需要像服务或需要长时间运行的东西一样使用LOH,则需要使用永远不会被释放并且可以在启动时理想地分配的缓冲池。当然,这意味着您必须自己进行“内存管理”。

根据您对此内存的使用方式,您可能还需要针对某些部分调用本机代码以避免调用一些.NET API,该API会强制将数据放在LOH中新分配的空间上。

这是一个关于这些问题的很好的起点文章:https://devblogs.microsoft.com/dotnet/using-gc-efficiently-part-3/

如果系统中同时有很多事情要处理,那么如果您的GC技巧能够奏效,那么我认为您非常幸运。如果您有并行工作正在进行,那么这只会稍微延迟不可避免的结果。

还要阅读有关GC.Collect的文档。我记得,GC.Collect(n)仅表示它收集不超过第n代 - 而不是它实际上曾经到达第n代。


0
不要担心LOH大小的变化。要担心的是分配/释放LOH。.Net对LOH非常愚蠢——它不会将LOH对象分配到远离常规堆的位置,而是在下一个可用的VM页面上进行分配。我有一个3D应用程序,需要大量分配/释放LOH和普通对象,结果(在DebugDiag转储报告中看到)是小堆和大堆的页面交替出现在RAM中,直到应用程序的2 GB VM空间没有大块剩余为止。如果可能,解决方案是一次性分配所需的内容,然后不要释放它——下次重复使用。
使用DebugDiag分析您的进程。看看VM地址逐渐向2 GB地址标记移动的情况。然后进行更改,防止这种情况发生。

0

我同意这里其他发帖人的看法,你可能想尝试使用技巧来处理.NET Framework,而不是试图通过GC.Collect强制它与你一起工作。

您可能会发现这个Channel 9视频有所帮助,它讨论了缓解垃圾回收器压力的方法。


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