Фрагментація купи великих об’єктів


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

Елементами масиву об'єктів є всі рядки, і рядки розпізнаються з нашого коду програми.

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

Отже, я був би дуже вдячний, якби хтось проливав світло на те, чому ці малі (<85 тис.) Масивів об’єктів потрапляють на 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
Згоден, чудове запитання. Я буду стежити за відповідями.
Charlie Flowers

2
Дуже цікаво. Здається, це була проблема для налагодження!
Метт Джордан,

Відповіді:


46

CLR використовує LOH для попереднього розподілу кількох об'єктів (наприклад, масиву, що використовується для інтернованих рядків ). Деякі з них мають розмір менше 85000 байт, і, отже, зазвичай не розподіляються на LOH.

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

Також завдяки дещо езотеричній оптимізації, будь-який double[]з 1000 або більше елементів також виділяється на LOH.


Проблемними об’єктами є об’єкти [], що містять посилання на рядки, які, як я знаю, створюються кодом програми. Це означає, що програма створює об’єкт [] (я не бачу підтвердження цього) або що якась частина CLR (наприклад, серіалізація) використовує їх для роботи над об’єктами програми.
Пол Руан,

1
Це може бути внутрішня структура, що використовується для інтернованих рядків. Будь ласка, перевірте мою відповідь на це запитання для отримання додаткової інформації: stackoverflow.com/questions/372547/…
Брайан Расмуссен

Ах, це дуже цікава пропозиція, дякую. Зовсім забув про стажер. Я знаю, що один з наших розробників зацікавлений у роботі, тому це, безумовно, я буду досліджувати.
Пол Руан,

1
85000 байт або 84 * 1024 = 87040 байт?
Пітер Мортенсен

5
85000 байт. Ви можете перевірити це, створивши байтовий масив 85000-12 (розмір довжини, MT, блок синхронізації) і викликавши GC.GetGenerationекземпляр. Це поверне Gen2 - API не розрізняє Gen2 та LOH. Зробіть масив на один байт менше, і API поверне Gen0.
Брайан Расмуссен


2

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

Якщо це те, що насправді відбувається, то це пояснює, як дрібні предмети опиняються там же, де і LOH - якщо вони досить довго живуть, щоб опинитися в 2-му поколінні.

І тому ваша проблема, здається, є досить добрим спростуванням ідеї, яка мені спала на думку - це призведе до фрагментації LOH.

Короткий зміст: вашу проблему можна пояснити тим, що LOH та покоління 2 спільно використовують одну і ту ж область купи, хоча це аж ніяк не є доказом того, що це пояснення.

Оновлення: результат !dumpheap -statмайже викидає цю теорію з води! Покоління 2 та LOH мають свої регіони.


Використовуйте! Eeheap, щоб показати сегменти, що складають кожну купу. Gen 0 і gen 1 живуть в одному сегменті (один і той же сегмент), Gen 2 і LOH можуть обидва виділяти кілька сегментів, але ці сегменти для кожної купи залишаються окремими.
Пол Руан,

Так, бачив це, дякую. Просто хотів згадати команду! Eeheaps, оскільки вона демонструє цю поведінку набагато чіткіше.
Пол Руан,

Ефективність основного GC багато в чому випливає з того, що він може переміщати об'єкти, тому на головній купі буде лише невелика кількість вільних областей пам'яті. Якщо об'єкт у головному купі закріплений під час колекції, простір над і під закріпленим об'єктом, можливо, доведеться відстежувати окремо, але оскільки кількість закріплених об'єктів зазвичай дуже мала, то буде і кількість окремих областей, які повинен трек. Поєднання переміщуваних та не переміщуваних (великих) об’єктів в одній купі погіршить продуктивність.
supercat

Більш цікавим питанням є те, чому .NET розміщує doubleмасиви більше 1000 елементів на LOH, а не налаштовує GC, щоб забезпечити їх вирівнювання на 8-байтових межах. Насправді, навіть на 32-бітовій системі я би очікував, що через поведінку кешу нав'язування 8-байтового вирівнювання для всіх об'єктів, розмір яких кратний 8 байтам, мабуть, є виграшем у продуктивності. В іншому випадку, хоча продуктивність інтенсивно використовуваного double[], вирівняного кешу, була б кращою, ніж продуктивність, яка не є такою, я не знаю, чому розмір корелював би із використанням.
supercat

@supercat Крім того, дві купи також поводяться дуже по-різному при розподілі. Основною купою є (на даний момент) в основному стос у шаблонах розподілу - він завжди розподіляється вгорі, ігноруючи будь-який вільний простір - коли відбувається ущільнення, вільні простори видавлюються. Це робить розподіл майже забороною та допомагає локалізації даних. З іншого боку, розподіл на LOH подібно до того, як працює malloc - він знайде перше вільне місце, яке може вмістити те, що ви виділяєте, і розподілить там. Оскільки це стосується великих об’єктів, локалізація даних є заданою, і покарання за розподіл не надто погане.
Луан

1

Якщо формат розпізнається як ваша програма, чому ви не визначили код, який генерує цей формат рядка? Якщо можливостей декілька, спробуйте додати унікальні дані, щоб з’ясувати, який шлях коду є винуватцем.

Той факт, що масиви чергуються з великими звільненими елементами, змушує мене здогадуватися, що вони спочатку були парними або принаймні пов'язаними. Спробуйте ідентифікувати звільнені об'єкти, щоб з'ясувати, що їх генерувало, та пов'язані з ними рядки.

Визначивши, що породжує ці рядки, спробуйте з’ясувати, що заважало б їм бути GCed. Можливо, їх забивають у забутий або невикористаний список для ведення журналу чи чогось подібного.


EDIT: Ігноруйте область пам’яті та конкретний розмір масиву на даний момент: просто з’ясуйте, що робиться з цими рядками, щоб викликати витік. Спробуйте! GCRoot, коли ваша програма створила або маніпулювала цими рядками лише один-два рази, коли відстежується менше об’єктів.


Рядки - це комбінація напрямних (які ми використовуємо) та рядкових ключів, які легко ідентифікувати. Я бачу, де вони генеруються, але вони ніколи (безпосередньо) не додаються до масивів об'єктів, і ми явно не створюємо 128 масивів елементів. Однак для початку ці невеликі масиви не повинні бути в LOH.
Пол Руан,

1

Чудове питання, я дізнався, прочитавши питання.

Я думаю, що інший біт шляху десеріалізації коду також використовує велику купу об'єктів, отже, і фрагментація. Якби всі рядки були інтерновані в ТЕ Ж ТИЙ самий час, я думаю, ви б у порядку.

З огляду на те, наскільки хорошим є збирач сміття .net, просто дозволити шляху коду десериалізації створити нормальний об’єкт рядка, швидше за все, буде досить хорошим. Не робіть нічого складнішого, поки не буде доведено необхідність.

Я хотів би поглянути на ведення хеш-таблиці останніх декількох рядків, які ви бачили, та повторне їх використання. Обмежуючи розмір хеш-таблиці та передаючи розмір при створенні таблиці, ви можете зупинити більшу частину фрагментації. Потім вам потрібен спосіб видалити з хеш-таблиці рядки, яких ви недавно не бачили, щоб обмежити їх розмір. Але якщо рядки, що створюються шляхом десеріалізації коду, у будь-якому випадку недовго, ви нічого не отримаєте.


1

Ось кілька способів , щоб визначити точний виклик стькі з LOH розподілу.

А щоб уникнути фрагментації LOH Попередньо виділіть великий масив об’єктів і закріпіть їх. За потреби використовуйте ці об’єкти повторно. Ось допис про фрагментацію LOH. Щось подібне може допомогти уникнути фрагментації LOH.


Я не розумію, чому закріплення тут має допомогти? BTW великі об'єкти на LOH і так не переміщуються GC. Це деталь реалізації, хоча.
користувач492238

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.