Фрагментация кучи больших объектов - PullRequest
95 голосов
/ 26 марта 2009

Приложение 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 сообщит, что он в порядке: </p>

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


Обновление 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.

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

Ответы [ 7 ]

43 голосов
/ 26 марта 2009

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

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

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

12 голосов
/ 02 января 2015

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

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

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

2 голосов
/ 26 марта 2009

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

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

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

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

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

1 голос
/ 21 февраля 2017

Согласно http://webcache.googleusercontent.com/search?q=cache:l3Ppy_eFoAAJ:www.wintellect.com/devcenter/sloscialo/hey-who-stole-all-my-memory+&cd=5&hl=en&ct=clnk&gl=mx&client=firefox-b

GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect();
1 голос
/ 04 апреля 2010

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

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

1 голос
/ 15 января 2010

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

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

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

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

1 голос
/ 26 марта 2009

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

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

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


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

...