在 C# 中進行大量、快速和頻繁的記憶體分配期間避免 OutOfMemoryException
我們的應用程序不斷地為大量數據(比如幾十到幾百兆字節)分配數組,這些數據在被丟棄之前的生存時間很短。
天真地這樣做可能會導致大的對象堆碎片,最終導致應用程序崩潰並出現 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 位載入項中可用記憶體的限制。
所以如果我沒看錯的話,我們的問題似乎是我們的虛擬記憶體已經被託管堆段碎片化了。
我想我的問題是:
- 我的分析正確嗎?
- 我們使用分塊避免 LOH 碎片的方法是否合理?
- 有沒有一個好的策略來避免我們現在看到的記憶體碎片?
我能想到的最明顯的答案是池化和重用我們的記憶體塊。這可能是可行的,但我寧願避免這樣做,因為它涉及我們自己有效地管理那部分記憶。
對於那些感興趣的人,這裡是我發現的關於這個問題的更新:
似乎最好的解決方案是實現塊池化以減輕垃圾收集器的壓力,所以我這樣做了。
結果是載入項在其任務中稍微進一步,但不幸的是它仍然很快耗盡了記憶體。
再次查看 WinDbg,我能看到的唯一真正區別是我們合併的託管堆大小始終較小,約為 200MB,而池化前約為 250MB。
似乎 .NET 可用的記憶體量隨著時間的推移而減少,因此實現池化只是延遲了記憶體耗盡。
如果這是真的,那麼明顯的罪魁禍首就是我們用來將數據載入到記憶體中的 COM 組件。我們對 COM 對象進行了一些記憶體,以改善對數據的重複訪問時間。我刪除了所有記憶體,並確保在每次查詢數據後釋放所有內容。
現在就記憶體而言,一切看起來都很好,只是速度要慢得多(接下來我必須解決這個問題)。
我想事後看來,COM 組件應該是記憶體問題的第一個嫌疑人,但是,嘿,我學到了一些東西 :) 從好的方面來說,池化對於減少 GC 成本仍然有用,因此也值得這樣做。
謝謝大家的評論。