Dot-Net

在 C# 中進行大量、快速和頻繁的記憶體分配期間避免 OutOfMemoryException

  • August 20, 2012

我們的應用程序不斷地為大量數據(比如幾十到幾百兆字節)分配數組,這些數據在被丟棄之前的生存時間很短。

天真地這樣做可能會導致大的對象堆碎片,最終導致應用程序崩潰並出現 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 成本仍然有用,因此也值得這樣做。

謝謝大家的評論。

引用自:https://stackoverflow.com/questions/11974694