我们的应用程序不断为大量数据(比如几十到几百兆字节)分配数组,这些数据在被丢弃之前会活很短的时间。
天真地这可能会导致大对象堆碎片,最终导致应用程序崩溃并出现OutOfMemoryException,尽管当前活动对象的大小并不过多。
我们过去成功管理过这种方法的一种方法是对数组进行分块,以确保它们不会最终出现在LOH上,这样做的目的是通过允许内存被垃圾收集器压缩来避免碎片。
我们的最新应用程序处理的数据比以前多,并且在单独的AppDomain或单独进程中托管的加载项之间非常频繁地传递此序列化数据。我们采用了与以前相同的方法,确保我们的内存总是被分块并且非常小心地避免大对象堆分配。
但是我们有一个必须在外部32位进程中托管的加载项(因为我们的主应用程序是64位,加载项必须使用32位库)。在特别繁重的负载下,当很多SOH内存块快速分配并在之后不久被丢弃时,即使我们的分块方法还不足以保存我们的32位加载项,并且它会因OutOfMemoryException而崩溃。
在发生OutOfMemoryException时使用WinDbg,!heapstat -inclUnrooted
显示:
Heap Gen0 Gen1 Gen2 LOH
Heap0 24612 4166452 228499692 9757136
Free space: Percentage
Heap0 12 12 4636044 12848SOH: 1% LOH: 0%
Unrooted objects: Percentage
Heap0 72 0 5488 0SOH: 0% LOH: 0%
!dumpheap -stat
显示:
-- SNIP --
79b56c28 3085 435356 System.Object[]
79b8ebd4 1 1048592 System.UInt16[]
79b9f9ac 26880 1301812 System.String
002f7a60 34 4648916 Free
79ba4944 6128 87366192 System.Byte[]
79b8ef28 17195 145981324 System.Double[]
Total 97166 objects
Fragmented blocks larger than 0.5 MB:
Addr Size Followed by
18c91000 3.7MB 19042c7c System.Threading.OverlappedData
这些告诉我,我们的内存使用量并不过多,而且我们的大对象堆非常小(因此我们绝对不会在这里处理大对象堆碎片)。
但是,!eeheap -gc
显示了这一点:
Number of GC Heaps: 1
generation 0 starts at 0x7452b504
generation 1 starts at 0x741321d0
generation 2 starts at 0x01f91000
ephemeral segment allocation context: none
segment begin allocated size
01f90000 01f91000 02c578d0 0xcc68d0(13396176)
3cb10000 3cb11000 3d5228b0 0xa118b0(10557616)
3ece0000 3ece1000 3fc2ef48 0xf4df48(16047944)
3db10000 3db11000 3e8fc8f8 0xdeb8f8(14596344)
42e20000 42e21000 4393e1f8 0xb1d1f8(11653624)
18c90000 18c91000 19c53210 0xfc2210(16523792)
14c90000 14c91000 15c85c78 0xff4c78(16731256)
15c90000 15c91000 168b2870 0xc21870(12720240)
16c90000 16c91000 17690744 0x9ff744(10483524)
5c0c0000 5c0c1000 5d05381c 0xf9281c(16328732)
69c80000 69c81000 6a88bc88 0xc0ac88(12627080)
6b2d0000 6b2d1000 6b83e8a0 0x56d8a0(5691552)
6c2d0000 6c2d1000 6d0f2608 0xe21608(14816776)
6d2d0000 6d2d1000 6defc67c 0xc2b67c(12760700)
6e2d0000 6e2d1000 6ee7f304 0xbae304(12247812)
70000000 70001000 70bfb41c 0xbfa41c(12559388)
71ca0000 71ca1000 72893440 0xbf2440(12526656)
73b40000 73b41000 74531528 0x9f0528(10421544)
Large object heap starts at 0x02f91000
segment begin allocated size
02f90000 02f91000 038df1d0 0x94e1d0(9757136)
Total Size: Size: 0xe737614 (242447892) bytes.
------------------------------
GC Heap Size: Size: 0xe737614 (242447892) bytes.
令我印象深刻的是,我们的最终SOH堆段从0x73b41000开始,这正好在我们的32位加载项中可用内存的极限。
因此,如果我正确地阅读,我们的问题似乎是我们的虚拟内存已经被托管堆段碎片化。
我想我的问题是:
我能想到的最明显的答案是汇集和重用我们的内存块。这可能是可行的,但我宁愿避免,因为它涉及我们自己有效地管理我们记忆的那部分。
答案 0 :(得分:8)
对于那些感兴趣的人,这里是我发现的关于这个问题的更新:
似乎最好的解决方案是实现我们的块的汇集以减轻对垃圾收集器的压力,所以我这样做了。
结果是加载项在其任务中稍微进一步,但不幸的是它仍然很快耗尽内存。
再次查看WinDbg,我唯一看到的真正区别是我们的组合托管堆大小一直较小,大约为200MB,相比之前大约为250MB。
几乎就好像.NET可用的内存量随着时间的推移而减少,因此实现池化只会延迟内存不足。
如果这是真的,那么显而易见的罪魁祸首就是我们用来将数据加载到内存中的COM组件。我们对COM对象进行一些缓存,以改善对数据的重复访问时间。我删除了所有缓存并确保在每次查询数据后都释放了所有内容。
现在关于内存的一切看起来都很好,它只是慢得多(我接下来要解决)。
我想事后看来,COM组件应该是内存问题的第一个嫌疑人,但是嘿,我学到了一些东西:)从好的方面来说,池化仍然有助于降低GC开销,所以这值得做同样。
感谢大家的评论。