Большой необъяснимый объем памяти в дампе памяти.NET-процесса
Я не могу объяснить большую часть памяти, используемой процессом C#. Общий объем памяти составляет 10 ГБ, но общий объем доступных и недоступных объектов составляет всего 2,5 ГБ. Интересно, что эти 7,5 ГБ могут быть?
Я ищу наиболее вероятные объяснения или метод, чтобы узнать, что это за память.
Вот точная ситуация. Процесс.NET 4.5.1. Он загружает страницы из Интернета и обрабатывает их с помощью машинного обучения. Как показывает VMMap, память почти полностью находится в управляемой куче. Это, кажется, исключает неуправляемую утечку памяти.
Процесс длился несколько дней, и память медленно росла. В какой-то момент объем памяти составляет 11 ГБ. Я прекращаю все, что работает в процессе. Я запускаю сборки мусора, включая сжатие кучи больших объектов, несколько раз (с интервалом в одну минуту):
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect();
Память уходит до 10 ГБ. Затем я создаю дамп:
procdump -ma psid
Дамп составляет 10 ГБ, как и ожидалось.
Я открываю дамп с помощью профилировщика памяти.NET (версия 5.6). Дамп отображает 2,2 ГБ доступных объектов и 0,3 ГБ недоступных объектов. Чем можно объяснить оставшиеся 7,5 ГБ?
Возможные объяснения, о которых я думал:
- LOH действительно не полностью уплотняется
- некоторая память используется вне объектов, отображаемых профилировщиком
1 ответ
После исследования проблема заключается в фрагментации кучи из-за закрепленных буферов. Я объясню, как исследовать и какие буферы закреплены.
Все профилировщики, которых я использовал, согласились сказать, что большая часть кучи бесплатна. Теперь мне нужно было посмотреть на фрагментацию. Я могу сделать это с WinDbg, например:
!dumpheap -stat
Затем я посмотрел на раздел "Фрагментированные блоки больше...". WinDbg говорит, что объекты лежат между свободными блоками, что делает уплотнение невозможным. Затем я посмотрел, что удерживает эти объекты и, если они закреплены, вот, например, объект по адресу 0000000bfaf93b80:
!gcroot 0000000bfaf93b80
Он отображает контрольный график:
00000004082945e0 (async pinned handle)
-> 0000000535b3a3e0 System.Threading.OverlappedData
-> 00000006f5266d38 System.Threading.IOCompletionCallback
-> 0000000b35402220 System.Net.Sockets.SocketAsyncEventArgs
-> 0000000bf578c850 System.Net.Sockets.Socket
-> 0000000bf578c900 System.Net.SocketAddress
-> 0000000bfaf93b80 System.Byte[]
00000004082e2148 (pinned handle)
-> 0000000bfaf93b80 System.Byte[]
Последние две строки говорят вам, что объект закреплен.
Закрепленные объекты - это буферы, которые невозможно переместить, поскольку их адрес используется совместно с неуправляемым кодом. Здесь вы можете догадаться, что это системный уровень TCP. Когда управляемому коду необходимо отправить адрес буфера во внешний код, ему необходимо "закрепить" буфер, чтобы адрес оставался действительным: GC не может его переместить.
Эти буферы, будучи очень маленькой частью памяти, делают невозможным сжатие и, таким образом, вызывают большую "утечку" памяти, даже если это не совсем утечка, а скорее проблема фрагментации. Это может произойти на LOH или на кучах поколений точно так же. Теперь вопрос: что заставляет эти закрепленные объекты жить вечно: найдите основную причину утечки, которая вызывает фрагментацию.
Вы можете прочитать подобные вопросы здесь:
https://ayende.com/blog/181761-C/the-curse-of-memory-fragmentation
.NET удаляет закрепленный выделенный буфер (хорошее объяснение закрепленных объектов в ответе)
Примечание: основная причина была в сторонней библиотеке AerospikeClient, использующей.NET async Socket API, который известен тем, что закрепляет буферы, отправленные ему. Хотя AerospikeClient правильно использовал буферный пул, буферный пул был воссоздан при повторном создании их клиента. Поскольку мы воссоздали их клиента каждый час, а не создавали один навсегда, пул буферов был воссоздан, что привело к увеличению количества закрепленных буферов, что, в свою очередь, привело к неограниченной фрагментации. Что остается неясным, так это то, почему старые буферы никогда не открепляются, когда передача заканчивается или, по крайней мере, когда их клиент удаляется.