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

Що таке ZGC

Ключове інженерне досягнення ZGC: тривалість пауз Stop-The-World (STW) становить менше 1 мілісекунди і практично не залежить від обсягу купи (Heap). Пауза на купі в 16 ГБ та на...


🟢 Junior Level

ZGC (Z Garbage Collector) — це ультрасучасний збирач сміття з наднизькими затримками (Sub-millisecond Latency), здатний керувати оперативною пам’яттю від десятків гігабайтів до 16 терабайтів.

Ключове інженерне досягнення ZGC: тривалість пауз Stop-The-World (STW) становить менше 1 мілісекунди і практично не залежить від обсягу купи (Heap). Пауза на купі в 16 ГБ та на купі в 16 ТБ буде однаково мікроскопічною (зазвичай від 0.1 до 0.5 мс).


Головний компроміс ZGC

ZGC спроєктований для систем із жорсткими вимогами до SLA відповіді: FinTech, High-Frequency Trading (HFT), Real-Time Bidding, телеком, ігрові сервери.

  • Ціна ультранизьких пауз: Пропускна здатність (Throughput) у ZGC на 5–15% нижча, ніж у G1 або Parallel GC, через постійні накладні витрати на бар’єри читання (Load Barriers) на процесорі.

Побутова аналогія

  • Звичайний збирач (G1 / Parallel): Ремонт центрального шосе. Рух повністю перекривають бетонними блоками на 30 хвилин, щоб дорожня техніка переклала асфальт (STW пауза). Усі машини стоять у заторі.
  • ZGC: Робітники вкладають новий асфальт прямо на сусідній смузі, доки автомобілі продовжують їхати зі швидкістю 80 км/год. Швидкість потоку злегка знизилася через обмеження, але шосе ні на секунду не зупиняється.

🟡 Middle Level

3 технологічні стовпи ZGC

Секрет відсутності пауз у ZGC базується на трьох фундаментальних технологіях:

1. Colored Pointers (Кольорові покажчики)

У 64-бітній архітектурі для адресації пам’яті достатньо 42–44 бітів (до 16 ТБ). ZGC використовує вільні старші біти 64-бітного посилання для прямого кодування метаданих збирача сміття:

┌────────────────────────────────────────────────────────┐
│                   64-BIT ZGC POINTER                   │
├─────────────┬───────────┬───────────────┬──────────────┤
│ 16 біт      │ 4 біти    │ 1 біт         │ 43 біти      │
│ Не вик. (0) │ Колір (GC)│ Зарезервовано │ Адреса в RAM │
└─────────────┴───────────┴───────────────┴──────────────┘
  • Кольорові біти: Marked0, Marked1, Remapped, Finalizable.
  • Перевага: Збирач сміття визначає статус об’єкта (чи він живий, чи перенесений на нову адресу), просто прочитавши сам покажчик, без звернення до заголовка об’єкта в оперативній пам’яті!
  • Наслідок: ZGC працює суворо на 64-бітних JVM і не підтримує стиснені покажчики (Compressed OOPs).

2. Load Barriers (Бар’єри читання) та механізм Self-Healing

На відміну від G1, що перехоплює запис посилань, ZGC вставляє перевірку під час кожного читання посилання з купи:

String name = user.getName(); // JIT вставляє Load Barrier перед розіменуванням
  1. Процесор перевіряє колір біта Remapped в адресі. Якщо біт коректний — читання виконується за 1 такт процесора.
  2. Якщо біт сигналізує, що об’єкт зараз евакуюється на нову адресу:
    • Спрацьовує обробник Load Barrier: він знаходить нову адресу об’єкта за таблицею переадресації.
    • Self-Healing (Самозцілення): Потік прямо на льоту перезаписує старий покажчик у пам’яті на нову адресу через атомарний CAS.
    • При повторному зверненні цей покажчик уже «зцілений» і читається миттєво.

3. Concurrent Evacuation (Конкурентна евакуація)

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


Порівняльна таблиця: G1 GC проти ZGC

Критерій G1 GC ZGC (Generational)
Тривалість STW пауз 20–200 мс (залежить від налаштувань) < 1 мс (гарантовано)
Залежність паузи від Heap Зростає зі збільшенням пам’яті Не залежить від розміру купи
Граничний розмір пам’яті Ефективний до 32–64 ГБ До 16 Терабайтів
Бар’єри пам’яті Write Barriers (при записі) Load Barriers (при читанні)
Пропускна здатність (Throughput) Висока Нижча на 5–15%
Compressed OOPs Підтримується (< 32 ГБ) Не підтримується (вимагає 64 біти)

🔴 Senior Level

Еволюція: Generational ZGC (Java 21+, JEP 439)

У Java 11–17 ZGC був однопоколінним (Single-Generational):

  • Під час кожного циклу сканувалася вся 16-терабайтна купа цілком.
  • Якщо застосунок генерував величезну кількість короткоживучих об’єктів за секунду (High Allocation Rate), збирач не встигав розмічати сміття. Виникав Allocation Stall (потоки застосунку примусово засинали в очікуванні звільнення сторінок пам’яті).

Починаючи з Java 21, представлено Generational ZGC:

  • Купа логічно розділена на Young та Old покоління.
  • Young Generation збирається незалежно, безперервно і в рази швидше, витрачаючи мінімум ресурсів CPU.
  • Результат: Навантаження на ядра CPU знижено в 2–4 рази, а ризик Allocation Stalls практично зведено до нуля.
  • Як увімкнути: Прапорці -XX:+UseZGC -XX:+ZGenerational.

Мультимапінг віртуальної пам’яті (Multi-Mapping)

Оскільки ZGC змінює біти кольору в покажчику, процесор без спеціальної підготовки сприйняв би посилання з бітами Marked0 та Remapped як абсолютно різні адреси оперативної пам’яті.

Для обходу цього обмеження ZGC на рівні ядра ОС (Linux virtual memory manager через mmap) відображає один і той самий фізичний фрейм оперативної пам’яті на 3 різні віртуальні діапазони адрес:

Віртуальна адреса (Marked0)  ────┐
Віртуальна адреса (Marked1)  ────┼───► Єдина фізична сторінка RAM
Віртуальна адреса (Remapped) ────┘

Завдяки цьому читання за будь-якою з «кольорових» адрес завжди апаратно потрапляє в одні й ті самі реальні дані у фізичній RAM.


Виробничий тюнінг та моніторинг ZGC

# 1. Увімкнення Generational ZGC (Java 21+)
-XX:+UseZGC -XX:+ZGenerational

# 2. Виділення потоків збирача (за замовчуванням = кількість ядер / 4)
-XX:ConcGCThreads=4

# 3. SoftMaxHeapSize: м'який ліміт купи для динамічного стиснення в контейнерах
-XX:SoftMaxHeapSize=12g -Xmx16g

# 4. Логування пауз та фаз ZGC
-Xlog:gc*,gc+phases=debug:file=/var/log/zgc.log:time,uptime,pid:filecount=5,filesize=100M

⚠️ Коли НЕ варто використовувати ZGC:

  1. Якщо навантаження є CPU-bound (процесор утилізований на 85–95%): бар’єри читання Load Barriers створять додаткове навантаження, що просадить загальну продуктивність.
  2. Якщо купа менше 4 ГБ: G1 ефективніше використовує пам’ять малого обсягу завдяки Compressed OOPs.
  3. Для пакетних Big Data розрахунків (Batch): Parallel GC виконає розрахунки швидше на 15–20%.

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

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

«ZGC — масштабований збирач сміття з наднизькими затримками (паузи < 1 мс), що не залежать від розміру пам’яті (аж до 16 ТБ). Працює на основі Colored Pointers (біти статусу прямо в 64-бітному покажчику) та Load Barriers (перехоплення читання посилань із механізмом Self-Healing). Уся важка фаза переміщення об’єктів відбувається конкурентно, без зупинки застосунку. У Java 21+ став генераційним (Generational ZGC), що усунуло проблему Allocation Stalls і знизило споживання CPU. Платою за мілісекундні паузи є зниження пропускної здатності (Throughput) на 5–15%».


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

  1. Яким чином ZGC утримує паузи Stop-The-World менше 1 мс незалежно від розміру купи?
    • Відповідь: У традиційних збирачах тривалість паузи залежить від обсягу живих об’єктів, які необхідно скопіювати на нове місце (евакуація). ZGC виніс фазу евакуації та оновлення посилань із паузи STW у повністю паралельний режим (Concurrent). Під час ультракоротких пауз (< 1 мс) ZGC виконує лише зупинку потоків на Safepoints для фіксації коренів GC Roots, розмір яких залежить лише від кількості потоків, а не від гігабайтів купи.
  2. Що таке Colored Pointers (кольорові покажчики) і чому ZGC не підтримує стиснені посилання (Compressed OOPs)?
    • Відповідь: Colored Pointers — це технологія використання вільних верхніх 4 бітів 64-бітної адреси покажчика (біти 42–45) для зберігання метаданих стану збирача сміття (Marked0, Marked1, Remapped). Це дозволяє миттєво дізнаватися статус об’єкта без звернення до його заголовка в RAM. Через необхідність маніпулювати бітами всередині 64-бітного покажчика ZGC не може використовувати стиснені 32-бітні покажчики (Compressed OOPs), тому на купах менше 32 ГБ посилання в ZGC займають по 8 байтів.
  3. Що таке механізм Self-Healing (самозцілення) у бар’єрах читання ZGC?
    • Відповідь: Коли потік застосунку читає поле-посилання (Object obj = ref.field;), бар’єр читання Load Barrier перевіряє колір покажчика. Якщо об’єкт був переміщений збирачем, але посилання в пам’яті ще зберігає стару адресу, потік звертається до таблиці переадресації, знаходить нову адресу, атомарно перезаписує старе посилання в купі на нову адресу і повертає її викликаючому коду. Наступне читання цього поля пройде без затримки.
  4. Чому в Java 21 знадобилося зробити ZGC генераційним (Generational ZGC)?
    • Відповідь: В однопоколінному ZGC збирач був змушений під час кожного циклу сканувати всю купу цілком, включаючи мільйони довгоживучих об’єктів. При високому темпі створення короткоживучих об’єктів збирач не встигав звільняти сторінки пам’яті, що призводило до Allocation Stalls (потоки застосунку засинали в очікуванні вільної пам’яті). Generational ZGC розділив купу на Young та Old: молоде покоління збирається в рази частіше й дешевше, що знизило навантаження на CPU на 50% і вирішило проблему затримок алокації.

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

  • 🚩 Твердження: «ZGC використовує Write Barriers, як і G1» (ZGC використовує Load Barriers на читання посилань).
  • 🚩 Омана: «ZGC підтримує Compressed OOPs» (ZGC вимагає повні 64-бітні покажчики для Colored Pointers).
  • 🚩 Сліпий перехід на ZGC при CPU-bound задачах (Load Barriers забирають до 15% процесорного часу, посилюючи дефіцит CPU).
  • 🚩 Використання однопоколінного ZGC на Java 11/17 при екстремальному темпі алокацій без урахування ризику Allocation Stall (потрібно переходити на Java 21+ Generational ZGC).

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