Розгляньмо питання про те, як збирач сміття визначає момент, коли об'єкт більше не потрібен. Щоб розібратися в деталях, що стоять за цим, необхідно знати, що таке кореневі елементи додатка (application roots). Простіше кажучи, кореневим елементом (root) називається комірка в пам'яті, в якій міститься посилання на об'єкт, розміщений у купі. Суворо кажучи, кореневими можуть називатися елементи будь-якої з наведених нижче категорій:
- Посилання на глобальні об'єкти (хоча в C# вони не дозволені, CIL-код дозволяє розміщувати глобальні об'єкти).
- Посилання на будь-які статичні об'єкти або статичні поля.
- Посилання на локальні об'єкти в межах кодової бази додатка.
- Посилання на об'єкти, що передаються як параметри методу.
- Посилання на об'єкти, що очікують фіналізації.
- Будь-які регістри центрального процесора, які посилаються на об'єкт.
Під час процесу збору сміття середовище виконання досліджуватиме об'єкти в керованої купі, щоб визначити, чи залишаються вони досяжними (тобто кореневими) для додатка. Для цього середовище CLR створюватиме графи об'єктів, що представляють усі досяжні для додатка об'єкти в купі.
Головне — засвоїти, що графи застосовуються для документування всіх досяжних об'єктів. Крім того, слід мати на увазі, що збирач сміття ніколи не створюватиме граф для одного й того ж об'єкта двічі, уникаючи необхідності виконання підрахунку циклічних посилань.
Після побудови графа всі недосяжні об'єкти позначаються як сміття. Після того, як об'єкт позначено для знищення, його буде видалено з пам'яті. Залишковий простір у купі після цього стискатиметься до компактного стану, що, своєю чергою, змусить CLR змінити набір активних кореневих елементів додатка (та базових на них покажчиків) так, щоб вони посилалися на правильне місце в пам'яті (це робиться автоматично та прозоро). І, нарешті, покажчик на наступний об'єкт також буде налаштовуватися так, щоб вказувати на наступний доступний ділянку пам'яті.
Загалом, збирач сміття використовує дві окремі купи, одна з яких призначена спеціально для зберігання дуже великих об'єктів (більше 85 000 байт). Доступ до цієї купи під час збору сміття здійснюється рідше через можливі наслідки з точки зору продуктивності, які може спричинити зміна місця розміщення великих об'єктів. Незважаючи на цей факт, керована купа все одно може спокійно вважатися єдиною областю пам'яті.
Під час спроби виявити недосяжні об'єкти середовище CLR не перевіряє буквально кожен об'єкт, що знаходиться в купі. Очевидно, що на це витрачалося б маса часу, особливо в більших (реальних) додатках. Для оптимізації процесу кожен об'єкт у купі належить до певного "покоління". Сенс застосування поколінь виглядає досить просто: що довше об'єкт перебуває в купі, то вища ймовірність того, що він там і залишиться. Наприклад, клас, визначений у головному вікні настільного додатка, залишатиметься в пам'яті аж до завершення виконання програми. З іншого боку, об'єкти, які були розміщені в купі лише недавно (наприклад, ті, що знаходяться в межах області видимості методу), найімовірніше, стануть недосяжними досить швидко. Виходячи з цих припущень, кожен об'єкт у купі належить до одного з наведених нижче поколінь:
- Покоління 0: Ідентифікує новий щойно розміщений об'єкт, який ще ніколи не позначався як такий, що підлягає видаленню, під час збору сміття.
- Покоління 1: Ідентифікує об'єкт, який уже "вижив" один процес збору сміття (був позначений як такий, що підлягає видаленню, під час збору сміття, але не був видалений через наявність достатнього місця в купі).
- Покоління 2: Ідентифікує об'єкт, якому вдалося пережити більше одного проходу збирача сміття.
Збирач сміття спочатку аналізує всі об'єкти, які належать до покоління 0. Якщо після їх видалення залишається достатня кількість пам'яті, статус усіх інших (залишених) об'єктів підвищується до покоління 1.
Якщо всі об'єкти покоління 0 уже були перевірені, але все одно потрібен додатковий простір, на досяжність перевіряються та піддаються процесу збору сміття об'єкти покоління 1. Об'єктам покоління 1, які вдалося вижити після цього процесу, потім призначається статус об'єктів покоління 2. Якщо ж збирачу сміття все одно потрібна додаткова пам'ять, тоді на досяжність починають перевірятися й об'єкти покоління 2. Об'єктам, які вдається пережити збір сміття на цьому етапі, залишається статус об'єктів покоління 2, оскільки вищі покоління просто не підтримуються.
З усього вищесказаного важливо зробити такий висновок: через віднесення об'єктів у купі до певного покоління, новіші об'єкти (наприклад, локальні змінні) видалятимуться швидше, а старіші (такі як об'єкти додатків) — рідше.
Процес збору сміття запускається, коли:
- Заповнено покоління 0;
- Виклик з коду
GC.Collect; - Операційна система повідомляє, що пам'яті недостатньо.
У .NET 9 модель поколінь (0, 1, 2) та кореневі елементи залишаються незмінними, як і поріг для великих об'єктів (85 КБ для LOH — Large Object Heap). Однак введено кілька ключових удосконалень для підвищення продуктивності та зменшення пауз:
-
Динамічна адаптація до розміру додатка (DATAS): За замовчуванням увімкнена для серверного GC. Функція динамічно регулює обсяг пам'яті, зменшуючи її споживання під час низького навантаження, що особливо корисно в контейнеризованих середовищах. Це наближає продуктивність серверного GC до робочого, зберігаючи високу пропускну здатність.
-
Паралельне сортування під час стиснення купи: Використовується алгоритм
vxsortдля паралельного сортування об'єктів під час компакції. Раніше обмежений Windows/x64, тепер доступний на Linux, що зменшує час пауз GC. -
Оптимізації write-бар'єрів: Кілька покращень, включаючи дешевші unchecked бар'єри для статичних полів, усунення бар'єрів для
ref struct(вони не на GC-купі) та нові bulk-бар'єри для ефективних оновлень. Це знижує накладні витрати на запис у generational GC.
Ці зміни роблять GC у .NET 9 швидшим і ефективнішим, особливо для великих і динамічних навантажень, без фундаментальних змін у базовій логіці, описаній вище.