9

我的应用程序对大对象进行了大量的二进制序列化和压缩。未压缩的序列化数据集约为 14 MB。压缩后约为 1.5 MB。我发现每当我在我的数据集上调用 serialize 方法时,我的大型对象堆性能计数器就会从 1 MB 以下跃升至大约 90 MB。我也知道,在一个负载相对较重的系统下,通常在运行一段时间(几天)后,这个序列化过程发生了几次,当调用这个序列化方法时,应用程序会抛出内存异常,即使有似乎是足够的内存。我猜碎片化是问题所在(虽然我不能说我 100% 确定,但我已经很接近了)

我能想到的最简单的短期修复(我想我正在寻找短期和长期答案)是在我完成序列化过程后立即调用 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,然后我序列化,它将使用 170MB。这是代码。比上面的代码还要简单

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

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

4

6 回答 6

4

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

这可能会导致 LOH 中出现多个数组副本。您的 14MB 数据集将开始使用 128KB 的 LOH,然后再使用 256KB,然后再使用 512KB,依此类推。最后一个,即实际使用的那个,大约为 16MB。LOH 包含这些的总和,大约 30MB,其中只有一个在实际使用中。

在没有 gen2 集合的情况下执行此操作 3 次,您的 LOH 已增长到 90MB。

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

于 2009-12-18T21:51:11.957 回答
3

不幸的是,我可以解决此问题的唯一方法是将数据分成块,以免在 LOH 上分配大块。这里提出的所有答案都很好,预计会奏效,但没有奏效。似乎 .NET 中的二进制序列化(使用 .NET 2.0 SP2)在幕后发挥了自己的小魔力,阻止了用户控制内存分配。

Answer then to the question would be "this is not likely to work". When it comes to using .NET serialization, your best bet is to serialize the large objects in smaller chunks. For all other scenarios, the answers mentioned above are great.

于 2010-03-05T16:01:30.880 回答
2

90MB 的 RAM 并不多。

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

于 2009-12-18T21:48:35.480 回答
0

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

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

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

如果您的 GC 技巧有效,我会认为您非常幸运,并且只有在系统中同时发生的事情不多时它才会真正有效。如果你有工作并行进行,这只会稍微延迟不可避免的事情。

还阅读了有关 GC.Collect.IIRC 的文档,GC.Collect(n) 只说它收集的内容不超过第 n 代——并不是说它实际上曾经到达第 n 代。

于 2009-12-18T22:05:12.387 回答
0

不用担心 LOH 大小会上升。担心分配/取消分配 LOH。.Net 对 LOH 非常愚蠢——它不是在远离常规堆的地方分配 LOH 对象,而是在下一个可用的 VM 页面上分配。我有一个 3D 应用程序,它对 LOH 和常规对象进行大量分配/解除分配——结果(如 DebugDiag 转储报告中所见)是小堆和大堆的页面最终在整个 RAM 中交替,直到没有大块剩余 2 GB VM 空间。可能的解决方案是分配一次你需要的东西,然后不要释放它——下次再使用它。

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

于 2010-01-14T20:46:35.497 回答
0

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

您可能会发现此第 9 频道视频很有帮助,其中讨论了减轻垃圾收集器压力的方法。

于 2010-01-14T20:56:03.793 回答