Фрагментация больших объектов

Приложение С#/. NET, над которым я работаю, страдает медленной утечкой памяти. Я использовал CDB с SOS, чтобы попытаться определить, что происходит, но данные, похоже, не имеют никакого смысла, поэтому я надеялся, что один из вас, возможно, испытал это раньше.

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

0:000> !DumpHeap 000000005b5b1000  000000006351da10
         Address               MT     Size
...
000000005d4f92e0 0000064280c7c970 16147872
000000005e45f880 00000000001661d0  1901752 Free
000000005e62fd38 00000642788d8ba8     1056       <--
000000005e630158 00000000001661d0  5988848 Free
000000005ebe6348 00000642788d8ba8     1056
000000005ebe6768 00000000001661d0  6481336 Free
000000005f214d20 00000642788d8ba8     1056
000000005f215140 00000000001661d0  7346016 Free
000000005f9168a0 00000642788d8ba8     1056
000000005f916cc0 00000000001661d0  7611648 Free
00000000600591c0 00000642788d8ba8     1056
00000000600595e0 00000000001661d0   264808 Free
...

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

Также обратите внимание на то, что CDB не сообщает тип, когда кусок сегмента сбрасывается: я не уверен, связано это или нет. Если я выгружаю отмеченный (< -) объект, CDB/SOS сообщает об этом:

0:015> !DumpObj 000000005e62fd38
Name: System.Object[]
MethodTable: 00000642788d8ba8
EEClass: 00000642789d7660
Size: 1056(0x420) bytes
Array: Rank 1, Number of elements 128, Type CLASS
Element Type: System.Object
Fields:
None

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

Кроме того, я не могу найти их корни GC, поскольку команда GCRoot зависает и никогда не возвращается (я даже пытался оставить ее на ночь).

Итак, я был бы очень признателен, если бы кто-нибудь мог пролить свет на то, почему эти маленькие (< 85k) массивы объектов заканчиваются на LOH: в каких ситуациях .NET помещает в него небольшой массив объектов? Кроме того, может ли кто-нибудь узнать об альтернативном способе определения корней этих объектов?


Обновление 1

Еще одна теория, с которой я пришел вчера, заключается в том, что эти массивы объектов начались большими, но были сокращены, оставив блоки свободной памяти, которые очевидны в дампах памяти. Что вызывает у меня подозрение, так это то, что массивы объектов всегда кажутся 1056 байтами (128 элементов), 128 * 8 для ссылок и 32 байтами служебных данных.

Идея состоит в том, что, возможно, какой-то небезопасный код в библиотеке или в CLR искажает количество полей элементов в заголовке массива. Я знаю длинный выстрел...


Обновление 2

Благодаря Брайану Расмуссену (см. принятый ответ) проблема была идентифицирована как фрагментация LOH, вызванная табличной статической таблицей! Я написал приложение для быстрого тестирования, чтобы подтвердить это:

static void Main()
{
    const int ITERATIONS = 100000;

    for (int index = 0; index < ITERATIONS; ++index)
    {
        string str = "NonInterned" + index;
        Console.Out.WriteLine(str);
    }

    Console.Out.WriteLine("Continue.");
    Console.In.ReadLine();

    for (int index = 0; index < ITERATIONS; ++index)
    {
        string str = string.Intern("Interned" + index);
        Console.Out.WriteLine(str);
    }

    Console.Out.WriteLine("Continue?");
    Console.In.ReadLine();
}

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

Во втором цикле создаются и интернируются уникальные строки. Это действие заставляет их встречаться в стажерской таблице. То, что я не понимал, - это то, как представлен стажер-стол. По-видимому, он состоит из набора страниц - массивов объектов из 128 строковых элементов, которые создаются в LOH. Это более очевидно в CDB/SOS:

0:000> .loadby sos mscorwks
0:000> !EEHeap -gc
Number of GC Heaps: 1
generation 0 starts at 0x00f7a9b0
generation 1 starts at 0x00e79c3c
generation 2 starts at 0x00b21000
ephemeral segment allocation context: none
 segment    begin allocated     size
00b20000 00b21000  010029bc 0x004e19bc(5118396)
Large object heap starts at 0x01b21000
 segment    begin allocated     size
01b20000 01b21000  01b8ade0 0x00069de0(433632)
Total Size  0x54b79c(5552028)
------------------------------
GC Heap Size  0x54b79c(5552028)

Взятие дампа сегмента LOH показывает образец, который я видел в протекающем приложении:

0:000> !DumpHeap 01b21000 01b8ade0
...
01b8a120 793040bc      528
01b8a330 00175e88       16 Free
01b8a340 793040bc      528
01b8a550 00175e88       16 Free
01b8a560 793040bc      528
01b8a770 00175e88       16 Free
01b8a780 793040bc      528
01b8a990 00175e88       16 Free
01b8a9a0 793040bc      528
01b8abb0 00175e88       16 Free
01b8abc0 793040bc      528
01b8add0 00175e88       16 Free    total 1568 objects
Statistics:
      MT    Count    TotalSize Class Name
00175e88      784        12544      Free
793040bc      784       421088 System.Object[]
Total 1568 objects

Обратите внимание, что размер массива объекта составляет 528 (а не 1056), потому что моя рабочая станция 32 бит, а сервер приложений - 64 бит. Массивы объектов по-прежнему составляют 128 элементов.

Итак, мораль этой истории должна быть очень осторожным интернированием. Если строка, в которой вы выполняете интернирование, как известно, не является членом конечного набора, ваше приложение будет течь из-за фрагментации LOH, по крайней мере, в версии 2 CLR.

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

Ответ 1

CLR использует LOH для предварительного выделения нескольких объектов (таких как массив, используемый для интернированных строк). Некоторые из них составляют менее 85000 байт и поэтому обычно не выделяются на LOH.

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

Также из-за некоторой эзотерической оптимизации любой double[] из 1000 или более элементов также выделяется на LOH.

Ответ 2

Почему Microsoft не предоставляет нам простой API в .NET:

GC.CompactLargeObjectHeap()

даже если этот метод заставляет приложение зависать в течение 10 секунд или более - он удаляет червь из-за сбоя и требует перезапуска приложения для очистки памяти!

Существует огромное количество приложений, которые используют библиотеки 3rdparty, которые выделяют LOH. Например, даже Microsoft, принадлежащая XNA-фрагментам, выделяется из LOH каждый раз, когда вы делаете "новый Texture2D()" или "Texture.FromFile()" без каких-либо средств!

Итак, если вы пытаетесь создать "Игровой движок" в .NET - это настоящая боль в шее, чтобы наш LOH просачивал память без каких-либо ограничений. Все, что мы можем сделать, это "замедлить это", если мы не решим "сделать нашу собственную библиотеку" и забыть о XNA. Конечно, это не то, что хочет Microsoft!

Многие месяцы головной боли будут решены для нас (и других!), если MS просто предоставит нам этот метод; Я еще раз скажу:

GC.CompactLargeObjectHeap()

И пока я делаю это предложение, я предлагаю MS сделать это еще на один шаг и разрешить эту команду:

GC.DoLargeObjectHeapCompactionWhenOutOfMemory = true;   // would be false by default

Итак, мы просто установили бы это значение true для "Visual3D Game Engine" (наш продукт), а затем автоматически, когда фрагментация приведет к первому исключению OOM,.NET автоматически скомпонует LOH, а затем повторит выделение!

Ничего себе, теперь это будет соответствовать всем трюкам .NET о том, что он "просто работает" и "прост в использовании"... и приведет только к этому "результату" для экстремальных случаев, когда альтернативой является сбой OOM, заставляя перезапустить приложение!

Пожалуйста, Microsoft - сделайте это для нас!


Пожалуйста, не объявляйте свое невежество, говоря банальный наивный совет "просто используйте лучшее управление памятью" - yeesh! Это очень неулокальный совет! Вы должны также сказать "напишите ВСЕ ваши собственные библиотеки 3rdparty" (например, XNA)!!!!

Почему Microsoft не предоставила такое поведение в .NET???? Что было бы больно? Это избавит нас от этих очень неприятных и трудоемких решений для групповой поддержки, которые в лучшем случае могут "замедлить утечку памяти", но не полностью их исправить.

Решение, которое мы запрашиваем, будет безвредным для тех, кто не хочет его использовать, и обеспечивает освежающее спасение тем, кто действительно хочет его использовать. Пожалуйста, предоставьте это в .NET 4.0 +.

Ответ 3

.NET Framework 4.5.1 имеет возможность явно уплотнять кучу больших объектов (LOH) во время сбора мусора.

GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect();

См. дополнительную информацию в GCSettings.LargeObjectHeapCompactionMode

Ответ 4

При чтении описаний того, как работает GC, и о том, как долгоживущие объекты заканчиваются в поколении 2, а коллекция объектов LOH происходит только в полной коллекции - как и сборник поколения 2, идея, которая возникает ум... почему бы не просто сохранить поколение 2 и большие объекты в одной куче, так как они собираются собрать вместе?

Если это то, что на самом деле происходит, то это объясняет, как маленькие объекты оказываются в том же месте, что и LOH, - если они достаточно долго живут, чтобы закончить в поколении 2.

И поэтому ваша проблема окажется довольно хорошим опровержением идеи, которая возникает для меня - это приведет к фрагментации LOH.

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

Обновление: вывод !dumpheap -stat довольно сильно ударяет эту теорию из воды! Генерация 2 и LOH имеют свои собственные области.

Ответ 5

Если формат распознается как ваше приложение, почему вы не определили код, который генерирует этот строковый формат? Если есть несколько возможностей, попробуйте добавить уникальные данные, чтобы выяснить, какой путь к коду является виновником.

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

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


EDIT: Игнорируйте область памяти и конкретный размер массива на данный момент: просто выясните, что делается с этими строками, чтобы вызвать утечку. Попробуйте GCRoot, когда ваша программа создала или обработала эти строки всего один или два раза, когда меньше объектов трассируется.

Ответ 6

Отличный вопрос, я узнал, прочитав вопросы.

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

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

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

Ответ 7

Вот несколько способов определить точный стек вызовов LOH.

А чтобы избежать фрагментации LOH Предварительно выделите большой массив объектов и скопируйте их. Повторно используйте эти объекты, когда это необходимо. Вот post в LOH Fragmentation. Что-то вроде этого может помочь избежать фрагментации LOH.