Що таке Garbage Collection
У Java використовується алгоритм Tracing (Трасування досяжності) від коренів (GC Roots), а не підрахунок посилань (Reference Counting):
🟢 Junior Level
Garbage Collection (GC, Збирання сміття) — це автоматична підсистема керування оперативною пам’яттю у віртуальній машині Java (JVM). Головне завдання GC — безперервно відстежувати динамічно виділену пам’ять у купі (Heap), знаходити об’єкти, які більше не використовуються застосунком, і безпечно звільняти займану ними пам’ять без участі розробника.
Навіщо потрібен GC?
- У мовах з ручним керуванням (C / C++): Програміст зобов’язаний вручну звільняти пам’ять (
free()/delete). Людський фактор неминуче призводить до двох фатальних проблем:- Витоки пам’яті (Memory Leaks): забули викликати
free(), процес з’їдає всю пам’ять і аварійно завершується. - Висячі вказівники (Dangling Pointers): пам’ять звільнили, але продовжили звертатися за старою адресою (пошкодження даних, вразливості безпеки).
- Витоки пам’яті (Memory Leaks): забули викликати
- У Java: JVM бере керування пам’яттю на себе, гарантуючи типобезпечність та відсутність висячих посилань.
Як GC визначає, що об’єкт — «сміття»?
У Java використовується алгоритм Tracing (Трасування досяжності) від коренів (GC Roots), а не підрахунок посилань (Reference Counting):
- GC Roots — це базові гарантовано живі посилання (локальні змінні у Stack фреймах усіх потоків, статичні поля завантажених класів, JNI-посилання).
- Якщо від GC Roots ланцюжком посилань до об’єкта неможливо дістатися, об’єкт вважається недосяжним (Unreachable) і підлягає знищенню збирачем сміття.
public class GcEligibilityDemo {
public static void main(String[] args) {
Order order1 = new Order("A100"); // order1 досяжний зі Stack main
Order order2 = new Order("B200"); // order2 досяжний зі Stack main
order1 = order2; // Об'єкт "A100" втратив єдине посилання!
// Тепер об'єкт "A100" став недосяжним і буде зібраний GC під час наступного збирання.
}
}
🟡 Middle Level
Слабка гіпотеза про покоління (Weak Generational Hypothesis)
Практично всі збирачі сміття в HotSpot спираються на емпіричний закон:
- Переважна більшість створених об’єктів помирають дуже швидко (локальні DTO, ітератори, конкатенації рядків живуть мілісекунди).
- Об’єкти, що пережили кілька збирань сміття, зазвичай живуть дуже довго (пули з’єднань, конфігурації, синглтони Spring).
Виходячи з цього, купа Heap поділена на два ключові покоління:
- Young Generation (Молоде покоління): поділяється на Eden (простір для новостворених об’єктів) та два напівпростори вижилих: Survivor 0 (S0 / From) та Survivor 1 (S1 / To). Тут збирання сміття (Minor GC) відбувається часто і триває лічені мілісекунди.
- Old Generation (Старше покоління / Tenured): зберігає довгоживучі об’єкти. Збирання тут (Major GC / Full GC) відбувається рідше, але вимагає більше часу.
Базові алгоритми збирання сміття
| Алгоритм | Принцип роботи | Плюси | Мінуси | Де застосовується |
|---|---|---|---|---|
| Copying (Копіювання) | Живі об’єкти копіюються з активної області в порожню сусідню область. Початкова область очищається одним скиданням покажчика. | Екстремально швидкий, нульова фрагментація | Вимагає подвійного запасу пам’яті | У Young Gen (Eden $\rightarrow$ Survivor) |
| Mark-Sweep (Розмітка та очищення) | 1. Розмічаються всі живі об’єкти від GC Roots. 2. Непозначені об’єкти видаляються, їхні адреси додаються у Free-List. |
Пам’ять не копіюється, немає оверхеду на дублювання | Сильна фрагментація пам’яті (прогалини між об’єктами) | Старі алгоритми Old Gen (CMS) |
| Mark-Compact (Розмітка та ущільнення) | 1. Розмітка живих об’єктів. 2. Зсув (ущільнення) усіх вижилих об’єктів до початку регіону пам’яті. |
Усуває фрагментацію, відновлює bump-the-pointer | Висока тривалість пауз STW (багатопрохідний алгоритм) | Full GC у G1, Parallel Old |
Card Table та Write Barriers: оптимізація Minor GC
Щоб зібрати сміття в Young Generation, GC не повинен сканувати гігабайти об’єктів в Old Generation у пошуках посилань на молоді об’єкти:
- Пам’ять Old Gen логічно нарізається на «картки» розміром 512 байтів.
- Щоразу, коли в коді відбувається присвоєння посилання
oldObject.field = youngObject;, JIT-компілятор вставляє інструкцію Write Barrier. - Write Barrier позначає відповідну картку в спеціальній бітовій карті Card Table як «брудну» (Dirty).
- Під час Minor GC скануються лише «брудні» картки Old Gen, що заощаджує до 99% часу збирання.
🔴 Senior Level
Трикутник компромісів GC (Trade-off Triangle)
Не існує універсально «найкращого» збирача сміття. Архітектура GC завжди балансує між трьома взаємовиключними характеристиками:
Latency (Мінімальні паузи)
▲
/ \
/ \
/ \
Throughput ◄───────┴───────► Footprint
(Пропускна здатність) (Оверхед по пам'яті/CPU)
- Throughput (Пропускна здатність): відсоток часу CPU, що витрачається на корисну бізнес-логіку застосунку, а не на GC.
- Latency (Затримка / Паузи): тривалість пауз Stop-The-World (STW), що безпосередньо впливає на SLA відповіді API.
- Footprint (Розмір накладних витрат): додаткова оперативна пам’ять, необхідна для службових структур GC (RSet, Card Table, метадані покажчиків).
Механіка Stop-The-World (STW) та Safepoints
Під час певних фаз збирання сміття потоки застосунку (Mutators) зобов’язані призупинити свою роботу, щоб граф об’єктів не змінювався в процесі перерахунку посилань:
- Потоки не можуть зупинитися миттєво в будь-якій точці байткоду. Вони зобов’язані дійти до так званої Safepoint (Безпечної точки).
- JIT вставляє перевірки Safepoint Poll на межах методів і в кінцях ітерацій циклів.
⚠️ Критичний дефект: Time-To-Safepoint (TTSP): Якщо один потік крутиться у важкому оптимізованому циклі з лічильником (
Counted Loopбез safepoint poll), він може діставатися безпечної точки кілька секунд. У цей час усі інші потоки застосунку вже заморожені й очікують на нього. У логах це виглядає як секундна пауза GC, хоча саме збирання зайняло лише 10 мс. Виправлення: прапорець-XX:+UseCountedLoopSafepoints.
Палітра збирачів сміття в HotSpot (Java 17–21+)
1. Parallel GC (-XX:+UseParallelGC)
- Фокус: Максимальний Throughput.
- Збирання сміття в Young та Old Gen виконується паралельно всіма ядрами CPU, але з повним блокуванням застосунку (STW). Ідеальний для пакетної обробки даних (Batch, Big Data, Hadoop).
2. G1 GC (-XX:+UseG1GC) — стандарт за замовчуванням з Java 9
- Фокус: Баланс між Throughput та Latency.
- Купа розбита на 2048 динамічних регіонів (від 1 до 32 МБ). Збирач інкрементально очищає регіони з найбільшою кількістю сміття («Garbage-First»).
- Дозволяє задати цільовий орієнтир максимальної паузи:
-XX:MaxGCPauseMillis=200.
3. ZGC (-XX:+UseZGC) / Generational ZGC (Java 21+)
- Фокус: Ультранизькі затримки (Sub-millisecond Latency, паузи $< 1$ мс).
- Здатний керувати терабайтами пам’яті (до 16 ТБ). Усі важкі фази (розмітка, переміщення об’єктів) виконуються паралельно з роботою застосунку.
- Базується на Colored Pointers (метадані зберігаються у старших бітах 64-бітного покажчика) та Load Barriers (перехоплювач під час кожного читання посилання з пам’яті).
- Ціна: Поступається G1 на 5–15% за пропускною здатністю через постійну роботу Load Barriers на CPU.
4. Epsilon GC (-XX:+UseEpsilonGC)
- No-Op збирач сміття: вміє лише виділяти пам’ять, але ніколи її не очищає. У разі вичерпання купи процес падає з
OutOfMemoryError. Застосовується для ультрачистих бенчмарків продуктивності та serverless-функцій (AWS Lambda), що живуть лічені секунди.
Небезпека: Allocation Stall
Якщо швидкість генерації сміття застосунком (Allocation Rate) перевищує швидкість фонової роботи паралельного збирача (ZGC або G1 Concurrent Mark), купа переповнюється до завершення циклу очищення.
- Потоки застосунку стикаються з Allocation Stall: потік під час спроби створити
new Object()примусово засинає, очікуючи, поки збирач звільнить бодай якийсь обсяг пам’яті. Це призводить до непередбачуваних просідань latency на сотні мілісекунд.
🎯 Шпаргалка для інтерв’ю
30-секундна відповідь (Elevator Pitch)
«Garbage Collection у Java — це автоматична система очищення недосяжних об’єктів у купі за допомогою алгоритму трасування від коренів GC Roots. Вона базується на слабкій гіпотезі про покоління: розділенні Heap на Young Gen (швидкі збирання Minor GC методом Copying) та Old Gen (рідкісні збирання Mark-Compact). Усі збирачі сміття балансують у трикутнику компромісів Latency ↔ Throughput ↔ Footprint. У Java 9+ стандартом є G1 GC, що забезпечує налаштовувані паузи, а для високонавантажених систем із жорстким SLA (< 1 мс) у Java 21+ застосовується Generational ZGC, що виконує переміщення об’єктів конкурентно через Load Barriers та Colored Pointers».
3–4 каверзних запитання з відповідями
- Чому в Java використовується Tracing (GC Roots), а не Reference Counting (підрахунок посилань)?
- Відповідь: Reference Counting (що використовується, наприклад, у Python чи COM) має фатальний недолік: він не здатний виявити та видалити циклічні посилання (коли об’єкт А посилається на Б, а Б — на А, але зовнішніх посилань на них більше немає). У Java алгоритм Reachability Analysis стартує суворо від зовнішніх коренів GC Roots, тому ізольовані циклічні острови об’єктів гарантовано ідентифікуються як сміття і видаляються.
- Що таке Safepoint і чому Time-To-Safepoint (TTSP) може бути небезпечнішим за саме збирання сміття?
- Відповідь: Safepoint — це точка в машинному коді, де потік зобов’язаний безпечно призупинити виконання для проходження фаз STW. Для старту GC необхідно, щоб усі потоки застосунку прийшли на Safepoint. Якщо один потік виконує тривалий цикл без перевірок safepoint poll (наприклад, довгий counted loop), він може затримувати початок GC на секунди. Усі інші потоки в цей час уже зупинені. Затримка TTSP проявляється в логах як незрозуміле зависання застосунку.
- За рахунок чого ZGC досягає пауз менше 1 мілісекунди навіть на купах у терабайти?
- Відповідь: ZGC переніс найдовшу фазу — переміщення (евакуацію) вижилих об’єктів та оновлення посилань — з фази Stop-The-World у паралельний режим роботи (Concurrent). Для цього використовуються Colored Pointers (біти статусу прямо всередині адреси покажчика) та Load Barriers (JIT-інструкції під час кожного читання посилання): якщо потік звертається до об’єкта, який ще не встиг переміститися, Load Barrier сам на льоту оновлює посилання та повертає актуальну адресу без зупинки всієї JVM.
- Що таке Premature Promotion (передчасне просування) і до чого воно призводить?
- Відповідь: Це ситуація, коли короткоживучі об’єкти не встигають померти в областях Survivor через їхній недостатній розмір або занадто низький поріг
MaxTenuringThresholdі виштовхуються в Old Generation. Це призводить до швидкого засмічення старшого покоління, частих викликів високовартісних Mixed/Full GC і різкого падіння пропускної здатності системи.
- Відповідь: Це ситуація, коли короткоживучі об’єкти не встигають померти в областях Survivor через їхній недостатній розмір або занадто низький поріг
Типові помилки та Red Flags
- 🚩 Виклик
System.gc()у production-коді (грубий антипатерн: провокує примусовий синхронний Full GC із зупинкою всіх потоків). - 🚩 Твердження: «Java використовує підрахунок посилань для GC» (фундаментальне нерозуміння JVM).
- 🚩 Сліпа впевненість, що «ZGC завжди кращий за G1» (ZGC жертвує 5–15% CPU Throughput і вимагає більше пам’яті на службові бар’єри; для Batch processing Parallel або G1 ефективніші).
- 🚩 Нерозуміння концепції Safepoint та причин виникнення пауз TTSP.
Пов’язані теми
- Коли об’єкт стає кандидатом на видалення GC — критерії втрати досяжності
- Що таке покоління в GC — Eden, Survivor, Old Gen
- Які алгоритми GC існують — класифікація алгоритмів збирання
- Що таке G1 GC — регіональний збирач сміття за замовчуванням
- Що таке stop-the-world — фізика пауз та safepoints