Додаток 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.
У випадку з нашим додатком у шляху коду десериалізації є загальний код, який інтернірує ідентифікатори сутності під час демаршалінгу: я зараз сильно підозрюю, що це винуватця. Однак наміри розробника, очевидно, були добрими, оскільки вони хотіли переконатись, що якщо одна й та сама сутність буде десеріалізована кілька разів, тоді в пам'яті буде збережено лише один екземпляр рядка ідентифікатора.