🧠 Розділ 3 · Питання #17

Які GC мінімізують stop-the-world паузи

Збирачі сміття з ультранизькими затримками (Low-Latency Garbage Collectors) — це спеціалізовані алгоритми JVM, спроєктовані для мінімізації або практичного усунення пауз Stop-Th...


🟢 Junior Level

Збирачі сміття з ультранизькими затримками (Low-Latency Garbage Collectors) — це спеціалізовані алгоритми JVM, спроєктовані для мінімізації або практичного усунення пауз Stop-The-World (STW).

У сучасних мікросервісах та вебзастосунках час відгуку оцінюється за жорсткими угодами про рівень сервісу (SLA) та процентилями затримки (p99 / p99.9). Якщо звичайний GC ставить застосунок на паузу на 500 мс, запити клієнтів стають у чергу, таймаути обриваються, а балансувальники починають відключати вузли кластера.


Топ-3 збирачі сміття для мінімізації пауз:

  1. ZGC (Z Garbage Collector):
    • Паузи: Гарантовано $< 1$ мс (у сучасних версіях зазвичай 0.1–0.4 мс).
    • Розмір купи: Від 16 МБ до 16 Терабайтів. Тривалість пауз не залежить від розміру купи!
  2. Shenandoah GC:
    • Паузи: Конкурентний збирач від Red Hat із паузами 1–10 мс (в останніх версіях — до часток мілісекунди).
    • Підтримує стиснуті покажчики (Compressed OOPs) на купах до 32 ГБ.
  3. G1 GC (Garbage-First):
    • Паузи: Налаштовувані м’які паузи в діапазоні 20–200 мс (-XX:MaxGCPauseMillis=200).
    • Компромісний вибір за замовчуванням у Java 9+, що поєднує високу пропускну здатність і контрольовані паузи.

У чому секрет ультранизьких пауз?

Традиційні збирачі сміття зупиняють застосунок на найдовшу фазу — евакуацію (переміщення) живих об’єктів на нове місце в пам’яті.

Збирачі ZGC та Shenandoah переміщують об’єкти конкурентно (Concurrent Evacuation) — у фонових потоках, доки прикладні потоки продовжують виконувати бізнес-код. Потоки застосунку зупиняються лише на частки мілісекунди для швидкого сканування коренів GC Roots.


🟡 Middle Level

Порівняльний аналіз стратегій мінімізації пауз

┌────────────────────────────────────────────────────────────────────────┐
│             ПОРІВНЯННЯ LOW-LATENCY ЗБИРАЧІВ СМІТТЯ                     │
├───────────────────┬──────────────┬──────────────┬──────────────────────┤
│ Параметр          │ G1 GC        │ Shenandoah   │ ZGC (Generational)   │
├───────────────────┼──────────────┼──────────────┼──────────────────────┤
│ Цільова пауза     │ 20–200 мс    │ 1–10 мс      │ < 1 мс               │
│ Евакуація         │ Stop-The-World│ Concurrent   │ Concurrent           │
│ Тип бар'єрів      │ Write Barrier│ Load-Ref     │ Load Barrier         │
│ Compressed OOPs   │ Є (< 32 ГБ)  │ Є (< 32 ГБ)  │ Немає (Colored Ptrs) │
│ Втрати Throughput │ Мінімальні   │ 5–15% CPU    │ 5–15% CPU            │
│ Підтримка версій  │ Java 9+      │ Java 12+     │ Java 15+ (Gen у 21+) │
└───────────────────┴──────────────┴──────────────┴──────────────────────┘

Небезпека: Збій алокації (Allocation Stall)

У збирачах із конкурентним ущільненням (ZGC, Shenandoah) криється небезпечна пастка:

  • Застосунок генерує об’єкти з величезною швидкістю (High Allocation Rate).
  • Фоновий збирач сміття не встигає завершити розмітку й евакуацію до того, як вільна пам’ять вичерпається.
  • Allocation Stall: Потік застосунку, який викликав new Object(), не може отримати пам’ять. JVM примусово присипляє потік або змушує його самостійно брати участь в евакуації.
  • Результат: Затримка відповіді конкретного потоку злітає на сотні мілісекунд, нівелюючи весь сенс використання low-latency GC!

Порятунок продуктивності: Поколіннєві ZGC та Shenandoah у Java 21+

До Java 21 і ZGC, і Shenandoah були однопоколіннєвими (Single-Generational), скануючи всю купу цілком під час кожного циклу.

У Java 21+ обидва збирачі отримали поколіннєву архітектуру:

  • Generational ZGC (-XX:+ZGenerational): Молоде покоління збирається в десятки разів частіше та швидше. Навантаження на ядра CPU знижено на 50%, а ризик Allocation Stall практично зведено нанівець.
  • Generational Shenandoah: Аналогічно фокусує процесорний час на короткоживучих об’єктах, що швидко відмирають.

🔴 Senior Level

Фізика накладних витрат: Бар’єри запису проти Бар’єрів читання

Причина, чому ZGC та Shenandoah поступаються G1 за чистою пропускною здатністю (Throughput) на 5–15%, полягає в типі використовуваних бар’єрів:

┌────────────────────────────────────────────────────────┐
│ 1. Write Barrier (G1 GC):                              │
│    Спрацьовує ТІЛЬКИ при ЗАПИСІ: a.field = b;          │
│    Операцій запису в коді лише ~10% від усіх звернень   │
│    Оверхед: ~1-2 такти процесора                       │
├────────────────────────────────────────────────────────┤
│ 2. Load Barrier (ZGC / Shenandoah):                    │
│    Спрацьовує при КОЖНОМУ ЧИТАННІ посилання:           │
│    b = a.field;                                        │
│    Операцій читання в коді понад ~90%!                 │
│    Оверхед: перевірка бітів кольору покажчика          │
│    (Colored) та запуск Self-Healing за необхідності    │
└────────────────────────────────────────────────────────┘

Оскільки операції читання посилань у реальному бізнес-коді відбуваються в рази частіше за операції запису, Load Barriers безперервно утилізують 5–15% потужностей CPU.


Архітектурна матриця вибору: Коли НЕ МОЖНА використовувати Low-Latency GC

Вибір ZGC або Shenandoah не повинен бути «модним рішенням за замовчуванням». Існують сценарії, де вони суворо протипоказані:

  1. CPU-bound навантаження: Якщо процесор сервера вже утилізований на 80–90% (криптографія, стиснення, важка математика), додаткові 10–15% оверхеду на бар’єри читання призведуть до відчутної деградації продуктивності всього сервісу.
  2. Пакетна обробка даних (Batch / ETL / Big Data): Якщо сервіс виконує нічний розрахунок зарплат або будує аналітичний звіт у Hadoop, час пауз нікого не хвилює. Головна метрика — час завершення завдання. Parallel GC виконає розрахунок на 15–20% швидше, ніж ZGC.
  3. Мікрокупи (< 4 ГБ RAM): На невеликих обсягах пам’яті відсутність стиснутих покажчиків у ZGC роздуває посилання з 4 до 8 байтів, з’їдаючи до 30% корисного простору. Для невеликих сервісів G1 є набагато ефективнішим.

Рецепти тюнінгу для ультранизьких затримок

# 1. ZGC для жорсткого SLA < 1 мс (Java 21+)
-XX:+UseZGC
-XX:+ZGenerational
-XX:ConcGCThreads=4               # Потоки для фонової евакуації (cores / 4)
-XX:SoftMaxHeapSize=12g           # Еластичне керування пам'яттю у хмарі
-Xmx16g -Xms16g

# 2. G1 GC для помірного SLA 100 мс та високого throughput
-XX:+UseG1GC
-XX:MaxGCPauseMillis=100          # Зниження дефолтної цільової паузи з 200 до 100 мс
-XX:+G1UseAdaptiveIHOP            # Адаптивний старт розмітки
-XX:InitiatingHeapOccupancyPercent=45

🎯 Шпаргалка для інтерв’ю

30-секундна відповідь (Elevator Pitch)

«Головні збирачі сміття для мінімізації пауз STW — це ZGC (паузи < 1 мс незалежно від розміру купи) та Shenandoah (паузи 1–10 мс), а також G1 з налаштовуваним таргетом (зазвичай 50–200 мс). Наднизькі паузи в ZGC та Shenandoah досягаються завдяки конкурентній евакуації (Concurrent Evacuation): доки застосунок працює, об’єкти переміщуються у фоні завдяки бар’єрам читання (Load Barriers) та кольоровим покажчикам (Colored Pointers). У Java 21+ перехід ZGC на покоління (Generational ZGC) усунув проблему збоїв алокації (Allocation Stall) і знизив споживання CPU. Платою за відсутність пауз є зниження загальної пропускної здатності (Throughput) на 5–15%».


3–4 каверзних питання з відповідями

  1. Завдяки чому ZGC та Shenandoah утримують паузи менше 1 мс, тоді як у G1 вони становлять 50–200 мс?
    • Відповідь: У збирачі G1 найдовша операція — евакуація (фізичне переміщення живих об’єктів та оновлення покажчиків) — виконується під час паузи Stop-The-World. У ZGC та Shenandoah фаза евакуації винесена в повністю паралельний режим (Concurrent) і виконується фоновими потоками без зупинки застосунку. STW використовується лише для надкороткої фіксації коренів GC Roots, що займає частки мілісекунди.
  2. Чому бар’єри читання (Load Barriers) створюють більше навантаження на CPU, ніж бар’єри запису (Write Barriers)?
    • Відповідь: У типовому програмному коді операції читання полів об’єктів відбуваються в 5–10 разів частіше, ніж операції модифікації посилань. Write Barrier у G1 спрацьовує лише під час запису (obj.field = value). Load Barrier у ZGC зобов’язаний спрацьовувати при кожному розіменуванні посилання (value = obj.field), перевіряючи колір покажчика. Через колосальну кількість читань постійне виконання машинних інструкцій бар’єра забирає від 5% до 15% тактів CPU.
  3. Що таке Allocation Stall у low-latency збирачах і як з ним боротися?
    • Відповідь: Allocation Stall — це стан, коли швидкість генерації нових об’єктів застосунком перевищує швидкість фонової розмітки та евакуації збирача сміття. Вільні сторінки пам’яті закінчуються до завершення циклу GC, внаслідок чого потоки, що намагаються виділити пам’ять, примусово засинають в очікуванні звільнення пам’яті. Рішення: перехід на Generational ZGC у Java 21+, збільшення розміру купи Heap, виділення більшої кількості потоків збирача (-XX:ConcGCThreads), або оптимізація темпу алокацій у коді.
  4. Чому для пакетної обробки даних (Batch Processing) вигідніше використовувати Parallel GC, а не ZGC?
    • Відповідь: У пакетних розрахунках затримки окремих операцій (Latency) не мають значення, а критичною є загальна швидкість завершення завдання (Throughput). ZGC витрачає до 15% процесорного часу на бар’єри читання та фонові потоки евакуації. Parallel GC взагалі не використовує бар’єрів і віддає 100% потужності всіх ядер процесора чистим обчисленням застосунку, завершуючи пакетний розрахунок швидше.

Типові помилки та Red Flags

  • 🚩 Твердження: «ZGC та Shenandoah повністю усувають будь-які паузи STW» (STW паузи присутні, але вони мікроскопічні: < 1 мс для сканування GC Roots).
  • 🚩 Сліпа впевненість: «ZGC безкоштовний і не має недоліків» (плата за паузи < 1 мс — втрата 5–15% Throughput і неможливість стиснення покажчиків).
  • 🚩 Використання ZGC на серверах із граничним завантаженням CPU (> 85%) (бар’єри поглиблять дефіцит процесорного часу).
  • 🚩 Встановлення -XX:MaxGCPauseMillis=1ms для G1 GC (G1 не здатен витримати таку паузу і скотиться в серію довгих Full GC).

Пов’язані теми