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

97

Приложение 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

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

Кроме того, я не могу найти их корни сборщика мусора, поскольку команда! 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.

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

Пол Руан
источник
2
Отличный вопрос - я заметил то же самое в своем приложении. Небольшие объекты, оставшиеся в LOH после очистки больших блоков, вызывают проблемы фрагментации.
Рид Копси,
2
Согласен, отличный вопрос. Я буду ждать ответов.
Чарли Флауэрс,
2
Очень интересно. Похоже, это была настоящая проблема для отладки!
Мэтт Джордан

Ответы:

47

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

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

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

Брайан Расмуссен
источник
Проблемными объектами являются object [], содержащие ссылки на строки, которые, как я знаю, создаются кодом приложения. Это означает, что приложение создает объект [] (я не вижу свидетельства этого) или что некоторая часть среды CLR (например, сериализация) использует их для работы с объектами приложения.
Пол Руан,
1
Это может быть внутренняя структура, используемая для интернированных строк. Пожалуйста, проверьте мой ответ на этот вопрос для получения более подробной информации: stackoverflow.com/questions/372547/…
Брайан Расмуссен
Ах, это очень интересный зацеп, спасибо. Совсем забыл про стажерский стол. Я знаю, что один из наших разработчиков - заядлый стажер, поэтому я обязательно займусь этим.
Paul Ruane
1
85000 байт или 84 * 1024 = 87040 байт?
Питер Мортенсен
5
85000 байт. Вы можете проверить это, создав массив байтов 85000-12 (размер длины, MT, блок синхронизации) и вызвав GC.GetGenerationэкземпляр. Это вернет Gen2 - API не различает Gen2 и LOH. Сделайте массив на один байт меньше, и API вернет Gen0.
Брайан Расмуссен
13

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

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

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

Андре Абрантес
источник
2

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

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

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

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

Обновление: вывод в !dumpheap -statзначительной степени выбивает эту теорию из воды! Поколение 2 и LOH имеют свои собственные регионы.

Дэниел Эрвикер
источник
Используйте! Eeheap, чтобы показать сегменты, составляющие каждую кучу. Gen 0 и Gen 1 живут в одном сегменте (одном и том же сегменте), Gen 2 и LOH могут выделять несколько сегментов, но эти сегменты для каждой кучи остаются отдельными.
Пол Руан,
Да, видел, спасибо. Я просто хотел упомянуть команду! Eeheaps, поскольку она гораздо яснее показывает это поведение.
Пол Руан,
Эффективность основного сборщика мусора во многом связана с тем, что он может перемещать объекты, поэтому в основной куче будет только небольшое количество свободных областей памяти. Если объект в основной куче закреплен во время сбора, пространство над и под закрепленным объектом, возможно, придется отслеживать отдельно, но поскольку количество закрепленных объектов обычно очень мало, то будет и количество отдельных областей, которые должен выполнять сборщик мусора. трек. Смешивание перемещаемых и неперемещаемых (больших) объектов в одной куче снизит производительность.
supercat
Более интересный вопрос: почему .NET помещает doubleмассивы размером более 1000 элементов в LOH, а не настраивает сборщик мусора, чтобы обеспечить их выравнивание по 8-байтовым границам. На самом деле, даже в 32-битной системе я ожидал, что из-за поведения кеша наложение 8-байтового выравнивания для всех объектов, чей выделенный размер кратен 8 байтам, вероятно, будет выигрышем в производительности. В противном случае, хотя производительность активно используемого устройства double[]с выравниванием по кешу будет лучше, чем у другого, я не знаю, почему размер будет коррелировать с использованием.
supercat
@supercat Также две кучи по-разному ведут себя при распределении. Основная куча (в настоящее время) в основном представляет собой стек в шаблонах распределения - она ​​всегда выделяется наверху, игнорируя любое свободное пространство - когда происходит сжатие, свободное пространство выжимается. Это делает выделение почти бесполезным и помогает локализовать данные. С другой стороны, распределение на LOH аналогично тому, как работает malloc - он находит первое свободное место, которое может содержать то, что вы выделяете, и размещает там. Поскольку это для больших объектов, локальность данных является заданной, и штраф за выделение не так уж и плох.
Luaan 05
1

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

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

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


РЕДАКТИРОВАТЬ: на данный момент игнорируйте область памяти и конкретный размер массива: просто выясните, что делается с этими строками, чтобы вызвать утечку. Попробуйте! GCRoot, если ваша программа создала эти строки или манипулировала ими всего один или два раза, когда нужно отследить меньше объектов.

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

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

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

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

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

Ян Рингроуз
источник
1

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

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

Naveen
источник
Я не понимаю, почему закрепление здесь должно помочь? Кстати, большие объекты на LOH в любом случае не перемещаются GC. Однако это деталь реализации.
user492238