Подтвердить что ты не робот

Избегание исключения OutOfMemoryException при больших, быстрых и частотных выделениях памяти в С#

Наше приложение постоянно выделяет массивы для больших объемов данных (скажем, от десятков до сотен мегабайт), которые живут в течение короткого промежутка времени, прежде чем их отбрасывать.

Выполнено наивно, это может вызвать фрагментацию кучи большого объекта, в результате чего приложение вылетает с OutOfMemoryException, несмотря на то, что размер живых живых объектов не является чрезмерным.

Один из способов, которым мы успешно справлялись с этим в прошлом, состоит в том, чтобы объединить массивы, чтобы гарантировать, что они не попадут в LOH, идея состоит в том, чтобы избежать фрагментации, позволяя объединять память сборщиком мусора.

Наше последнее приложение обрабатывает больше данных, чем раньше, и очень часто передает эти сериализованные данные между надстройками, размещенными в отдельных приложениях AppDomains или отдельных процессах. Мы использовали тот же подход, что и раньше, гарантируя, что наша память всегда была чересчур и была очень осторожной, чтобы избежать выделения больших кусков объектов.

Однако у нас есть одна надстройка, которая должна быть размещена во внешнем 32-битном процессе (потому что наше основное приложение - 64 бит, а надстройка должна использовать 32-битную библиотеку). При особенно большой нагрузке, когда много блоков памяти SOH быстро выделяются и отбрасываются вскоре, даже нашего подхода к chunking недостаточно, чтобы сохранить нашу 32-битную надстройку, и она выходит из строя с OutOfMemoryException.

Используя WinDbg в момент возникновения исключения OutOfMemoryException, !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 с разумным использованием chunking?
  • Есть ли хорошая стратегия, чтобы избежать фрагментации памяти, которую мы теперь видим?

Самый очевидный ответ, который я могу придумать, - это объединить и повторно использовать наши куски памяти. Это потенциально возможно, но это то, чего я бы предпочел избежать, поскольку он вовлекает нас в эффективное управление этой частью нашей памяти.

4b9b3361

Ответ 1

Для тех, кого это интересует, вот обновленная информация о том, что я узнал по этой проблеме:

Похоже, что лучшим решением было реализовать объединение наших кусков, чтобы уменьшить давление на сборщик мусора, поэтому я сделал это.

В результате получилось, что надстройка немного улучшилась в своей задаче, но, к сожалению, она по-прежнему быстро исчерпала память.

Глядя в WinDbg снова, единственное реальное различие, которое я мог видеть, заключалось в том, что наш объединенный размер управляемой кучи был последовательно меньше, примерно на 200 Мбайт по сравнению с примерно 250 МБ перед пулом.

Это было почти так, как если бы объем памяти, доступный для .NET, со временем уменьшался, и поэтому реализация пула просто задерживала нехватку памяти.

Если это правда, очевидным виновником был COM-компонент, который мы используем для загрузки данных в память. Мы выполняем кэширование COM-объектов, чтобы улучшить время повторного доступа к данным. Я удалил все кэширование и обеспечил, чтобы все было выпущено после каждого запроса данных.

Теперь все выглядит отлично в отношении памяти, оно намного медленнее (что мне придется решать дальше).

Я предполагаю, что задним числом COM-компонент должен был быть первым подозреваемым в проблемах с памятью, но, эй, я чему-то научился:) И с положительной стороны, объединение будет по-прежнему полезно для уменьшения накладных расходов GC, так что стоило делать также.

Спасибо за ваши комментарии.