Що таке Garbage Collection?
GC базується на двох спостереженнях:
🟢 Junior Level
Garbage Collection (GC) — автоматичне «прибирання сміття» в пам’яті Java.
«Сміття» — об’єкти, які більше не використовуються програмою (недосяжні з GC Roots). GC знаходить їх і звільняє пам’ять. Без GC розробнику довелося б вручну звільняти пам’ять (як у C/C++), що загрожує витоками та dangling pointer.
Проста аналогія: Уявіть, що ви працюєте за столом. З часом стіл захаращується папірцями. GC — це прибиральник, який приходить і викидає непотрібні папірці, звільняючи місце.
Навіщо потрібен:
- У C/C++ програміст сам видаляє пам’ять → часто забуває → витоки
- У Java GC сам знаходить і видаляє непотрібні об’єкти → немає витоків (майже)
Як GC розуміє, що об’єкт непотрібний:
- Якщо на об’єкт немає посилань → він сміття
- Якщо до об’єкта не можна дістатися з коду → він сміття
Приклад:
public void example() {
String s1 = "hello"; // Об'єкт "hello" використовується
s1 = "world"; // "hello" більше не потрібен → GC видалить!
}
Основні GC в Java:
- G1 GC — за замовчуванням (Java 9+)
- ZGC — мінімальні паузи (< 1 мс), але throughput на 5-10% нижчий, ніж у G1.
- Parallel GC — для пакетної обробки
🟡 Middle Level
Weak Generational Hypothesis
Терміни:
- STW (Stop-The-World) — повна зупинка всіх потоків додатку на час збору сміття.
- SATB (Snapshot-At-The-Beginning) — алгоритм конкурентного маркування: запам’ятовує об’єкти, які були досяжні на момент початку GC, і відстежує тільки їх видалення з графу досяжності.
- Card Table — бітова карта, де кожен біт показує, чи є посилання з Old Gen в Young Gen. Дозволяє GC не сканувати весь Old Gen.
GC базується на двох спостереженнях:
- Для більшості додатків вірно: більшість об’єктів умирають молодими — тимчасові змінні, буфери. Але для workload-ів з великими довгоживучими кешами це спостереження не працює.
- Об’єкти, що пережили кілька зборок, живуть довго — синглтони, кеші
Наслідок: Розділити Heap на покоління:
- Young Generation — часті, швидкі збірки
- Old Generation — рідкі, повільні збірки
Основні алгоритми
| Алгоритм | Як працює | Де застосовується |
|---|---|---|
| Copying | Копіює живі в нову область, стару очищує | Young Gen |
| Mark-Sweep | Позначає живих, видаляє мертвих | Old Gen |
| Mark-Compact | Позначає + зміщує живих (прибирає фрагментацію) | Full GC |
Фази GC
1. Mark (Розмітка)
→ Обхід графу об'єктів від GC Roots
→ Позначаються всі досяжні об'єкти
2. Sweep (Очищення)
→ Видалення непозначених об'єктів
3. Compact (Ущільнення) — не завжди
→ Зміщення живих об'єктів для усунення фрагментації
Stop-The-World (STW)
Під час GC додаток зупиняється!
→ Усі потоки чекають
→ Додаток "завмирає"
→ Тривалість: від 1 мс (ZGC) до кількох секунд (Full GC)
Типи GC
G1 GC (за замовчуванням):
- Ділить Heap на регіони
- Цільова пауза:
-XX:MaxGCPauseMillis=200 - Хороший баланс latency/throughput
ZGC (Java 21+):
- Паузи < 1 мс (не залежать від розміру Heap)
- До 16 ТБ пам’яті
- -5-10% throughput vs G1
Parallel GC:
- Максимальний throughput
- Довгі паузи (500 мс+)
- Для Big Data, batch processing
Типові помилки
- System.gc() в коді
// ❌ Виклик GC вручну — антипатерн System.gc(); // Зупиняє весь додаток! - Занадто маленький Heap
-Xmx256m для Spring Boot → постійний GC → Додаток гальмує → Рішення: -Xmx1g мінімум - Витоки пам’яті
// ❌ Статична колекція росте нескінченно static List<Data> cache = new ArrayList<>(); cache.add(data); // GC не може видалити!
Коли НЕ використовувати кожен GC
- Serial GC — НЕ для серверів (один потік, довгі паузи)
- Parallel GC — НЕ коли важливі паузи (SLA < 100 мс)
- G1 GC — НЕ для SLA < 10 мс (беріть ZGC), НЕ для batch без latency-вимог (беріть Parallel)
- ZGC — НЕ на Java < 15 (немає стабільної версії), НЕ коли важливий максимальний throughput
🔴 Senior Level
GC Trade-offs (Трикутник GC)
Latency
/ \
/ \
Throughput ── Footprint
Не можна оптимізувати всі три одночасно!
G1: баланс latency + throughput
ZGC: мінімальна latency (ціною throughput: ZGC жертвує 5-15% throughput заради пауз < 1 мс)
Parallel: максимальний throughput (ціною latency)
Safepoints та STW механіка
Потоки не можуть зупинитися миттєво!
→ Повинні дійти до Safepoint (точка зупинки)
Де розставлені Safepoints:
- Перед виходом з методу
- В кінцях циклів
- Перед викликами методів
Механізм: Safepoint Poll
→ JVM позначає сторінку як недоступну
→ Потік читає → SIGSEGV → переходить в очікування
TTSP (Time To Safepoint):
→ Час від команди "стоп" до останнього потоку
→ Counted Loops можуть затримати на секунди!
→ -XX:+UseCountedLoopSafepoints
Write Barriers та Card Tables
Проблема: Minor GC не хоче сканувати весь Old Gen
Рішення: Card Table
→ Old Gen розбитий на картки по 512 байт
→ При запису oldObj.field = youngObj
→ Write Barrier позначає картку як Dirty
→ Minor GC сканує тільки Dirty картки
Write Barrier (вставляється JIT):
card_table[address >> 9] = DIRTY
→ ~1-2 ns overhead на кожен запис посилання
Сучасні GC (Java 17-21+)
G1 GC:
- Регіони 1-32 МБ (динамічні)
- Remembered Sets (RSet) для міжрегіональних посилань
- SATB (Snapshot-At-The-Beginning) для конкурентного маркування
- Mixed GC: Young + найбільш “брудні” регіони Old
ZGC:
- Colored Pointers (метадані у вказівниках)
- Load Barriers (перевірка при читанні)
- Multi-mapping віртуальної пам’яті
- Generational ZGC (Java 21+) — поділ на покоління
Shenandoah:
- Brooks Pointers → Load Reference Barriers
- Конкурентне ущільнення (Evacuation)
- Альтернатива ZGC для OpenJDK
Epsilon GC:
- “No-op GC” — тільки алокація
- Для тестів продуктивності
- Для мікросервісів з коротким життєвим циклом
Allocation Stall
Найнебезпечніший сценарій для конкурентних GC:
Додаток створює сміття швидше, ніж GC збирає
→ Пам'ять закінчується
→ Потік додатку ЗАМИРАЄ і чекає GC
→ Пауза: сотні мілісекунд або секунди
Рішення:
→ Збільшити Heap
→ Збільшити ConcGCThreads
→ Зменшити Allocation Rate
Production Experience
Реальний сценарій #1: Full GC кожну хвилину
- Spring Boot,
-Xmx512m→ занадто мало - Full GC кожні 60 секунд, пауза 2 секунди
- SLA порушено
- Рішення:
-Xmx2g→ Minor GC кожні 5 хвилин, пауза 50 мс
Реальний сценарій #2: ZGC врятував HFT систему
- High-frequency trading: SLA < 10 мс
- G1 GC: паузи 50-200 мс → пропуски угод
- ZGC: паузи < 1 мс → SLA виконано
- Ціна: -8% throughput (прийнято)
Best Practices
- Не викликайте System.gc() вручну
- G1 GC — хороший вибір за замовчуванням
- ZGC (Java 21+) — якщо SLA < 10 мс
- Parallel GC — для batch processing
- Моніторте GC логи через
-Xlog:gc* - Safepoint логи для діагностики “зависань”
- Уникайте Allocation Stall → достатній Heap
- Найкраще сміття — те, що не створене → оптимізуйте алокації
Резюме для Senior
- GC = система управління ресурсами, не просто “очищення”
- Трикутник: Latency ↔ Throughput ↔ Footprint
- Safepoints = механізм STW, TTSP може бути проблемою
- Write Barriers = Card Tables для оптимізації Minor GC
- ZGC = Colored Pointers + Load Barriers → < 1 мс паузи
- Allocation Stall = катастрофа для latency
- Вибір GC = архітектурне рішення на основі SLA
- Найкраще сміття = не створене → оптимізуйте код до налаштування GC
🎯 Шпаргалка для інтерв’ю
Обов’язково знати:
- GC автоматично видаляє недосяжні з GC Roots об’єкти — розробнику не потрібно управляти пам’яттю вручну
- Weak Generational Hypothesis: більшість об’єктів умирають молодими → Heap ділиться на Young/Old Generation
- Основні алгоритми: Copying (Young Gen), Mark-Sweep/Mark-Compact (Old Gen)
- G1 GC — за замовчуванням з Java 9, баланс latency/throughput; ZGC — паузи < 1 мс, але throughput на 5-15% нижчий
- Stop-The-World: усі потоки зупиняються на час GC; ZGC < 1 мс, G1 20-200 мс, Full GC — секунди
- Safepoints — точки в коді, де потоки можуть зупинитися; TTSP може бути довшим за сам GC
- Card Table + Write Barriers оптимізують Minor GC, не скануючи весь Old Gen
- Найкраще сміття — те, що не створене; оптимізуйте алокації до налаштування GC
Часті уточнюючі запитання:
- Чому не можна оптимізувати latency, throughput і footprint одночасно? — Трикутник GC: покращення одного погіршує інше (наприклад, ZGC жертвує throughput заради пауз < 1 мс)
- Що таке Allocation Stall? — Додаток створює сміття швидше, ніж concurrent GC встигає → потік завмирає і чекає
- Чому ZGC повільніший за G1 по throughput? — Load Barriers при кожному читанні посилання додають 5-15% CPU overhead
- Коли викликати System.gc()? — Ніколи у production. Тільки в тестах/бенчмарках.
Червоні прапорці (НЕ говорити):
- «Я викликаю System.gc() після великих операцій для оптимізації» — це антипатерн, GC розумніший
- «GC використовує Reference Counting» — Java використовує Reachability Analysis, а не підрахунок посилань
- «ZGC завжди краще G1» — ZGC жертвує throughput; для batch processing G1/Parallel краще
- «GC можна повністю вимкнути» — Epsilon GC не збирає сміття взагалі, але це не production-рішення для довготривалих додатків
Пов’язані теми:
- [[5. Коли об’єкт стає кандидатом на видалення GC]]
- [[8. Що таке покоління в GC (young, old, metaspace)]]
- [[12. Які алгоритми GC існують]]
- [[13. Що таке G1 GC]]
- [[16. Що таке stop-the-world]]