2009-03-26 9 views
90

Приложение C# /. 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 помещает небольшой массив объектов там? Кроме того, знает ли кто-нибудь об альтернативном способе определения корней этих объектов?


Update 1

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

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


Update 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. Это становится все более очевидным в ЦКБ/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.

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

+2

Большой вопрос - я замечаю то же самое в моем приложении. Небольшие объекты, оставшиеся в LOH после того, как большие блоки очищены, и это вызывает проблемы фрагментации. –

+2

Согласен, отличный вопрос. Я буду следить за ответами. –

+2

Очень интересно. Похоже, проблема была отладка! –

ответ

39

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

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

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

+0

Проблемные объекты - это объекты [] s, содержащие ссылки на строки, которые, как мне известно, создаются кодом приложения.Это означает, что приложение создает объект [] s (я не вижу этого) или что часть CLR (например, сериализация) использует их для работы над объектами приложения. –

+1

Это может быть внутренняя структура, используемая для интернированных строк. Пожалуйста, проверьте мой ответ на этот вопрос для получения дополнительной информации: http://stackoverflow.com/questions/372547/where-do-java-and-net-string-literals-reside/372559#372559 –

+0

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

1

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

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

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


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

+0

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

2

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

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

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

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

Обновление: Выход !dumpheap -stat довольно сильно ударяет эту теорию из воды! У поколения 2 и LOH есть свои собственные области.

+0

Используйте! eeheap, чтобы показать сегменты, составляющие каждую кучу. Gen 0 и gen 1 живут в одном сегменте (один и тот же сегмент), gen 2 и LOH могут распределять несколько сегментов, но сегменты для каждой кучи остаются отдельными. –

+0

Да, см. Обновление, которое я добавил сразу после публикации! –

+0

Да, видел это, спасибо. Просто хотел упомянуть команду! Eeheaps, поскольку она показывает это поведение намного более четко. –

1

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

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

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

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

1

Вот несколько способов определить точное call-stack из LOH распределения.

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

+0

Не могу понять, почему здесь нужно помогать? Большие объекты BTW на LOH в любом случае не перемещаются GC. Однако его деталь реализации. – user492238

11

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

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

Смотреть подробнее в GCSettings.LargeObjectHeapCompactionMode

+0

Это здорово, спасибо, спасибо. –