1. Знайомство зі збирачем сміття (GC)
Якщо ви коли-небудь писали на C або C++, то напевно стикалися з необхідністю вручну звільняти пам’ять за допомогою free() або delete. У Java усе набагато простіше: ви створюєте об’єкт через new, а видаляти його не потрібно — цим займається спеціальний «двірник» під назвою збирач сміття (Garbage Collector, GC).
GC — це частина JVM, яка автоматично звільняє пам’ять, зайняту об’єктами, на які більше немає посилань. Завдяки цьому Java‑розробники можуть не переживати, що забудуть очистити пам’ять (і отримають витік пам’яті), або, навпаки, випадково видалять об’єкт, який усе ще потрібен (і станеться аварійне завершення).
Але, як і будь‑який двірник, GC не ідеальний: часом він може втрутитися в найнесприятливіший момент, влаштувавши «генеральне прибирання» (Stop-the-World), або працювати не так швидко, як хотілося б. Тому в JVM існує кілька різних реалізацій збирача сміття — і вибір правильної реалізації може помітно вплинути на продуктивність застосунку.
Основні типи збирачів сміття
Serial GC
- Serial GC — найпростіший і найстаріший збирач сміття.
- Працює в одному потоці.
- Зупиняє всі інші потоки на час прибирання (Stop-the-World).
- Підходить для невеликих застосунків без активної багатопоточності.
- Вмикається прапорцем: -XX:+UseSerialGC
Parallel GC
- Parallel GC (також «Throughput Collector»).
- Використовує кілька потоків для прибирання сміття.
- Орієнтований на максимальну пропускну здатність.
- Як і раніше, виконує прибирання з паузами Stop-the-World, але швидше, ніж Serial.
- Підходить для серверних застосунків, де невеликі паузи некритичні.
- Вмикається прапорцем: -XX:+UseParallelGC
CMS (Concurrent Mark Sweep)
- CMS — застарілий, але довго популярний GC, що мінімізує паузи.
- Працює частково паралельно із застосунком, зменшуючи час зупинки.
- Складніший у налаштуванні, має накладні витрати.
- Починаючи з Java 9 позначений як застарілий (deprecated).
- Вмикається прапорцем: -XX:+UseConcMarkSweepGC
G1 (Garbage First)
- G1 GC — сучасний збирач за замовчуванням (починаючи з Java 9).
- Баланс між мінімальними паузами та продуктивністю.
- Ділить купу на безліч невеликих регіонів (регіональна модель).
- Може збирати сміття вибірково за регіонами, не торкаючись усієї купи.
- Дозволяє задати цільову паузу, наприклад -XX:MaxGCPauseMillis=200.
- Прапорець увімкнення: -XX:+UseG1GC (зазвичай не потрібен, оскільки G1 використовується за замовчуванням).
ZGC і Shenandoah
- ZGC і Shenandoah — сучасні збирачі сміття з низькою затримкою (low‑latency).
- Мета — мінімальні паузи (мілісекунди), навіть на величезних купах (до терабайтів).
- Працюють практично повністю паралельно із застосунком.
- Потребують Java 11+ (ZGC) або Java 12+ (Shenandoah).
- Підходять для систем, чутливих до затримок (latency‑critical: біржі, фінтех, real‑time‑аналітика).
- Прапорці увімкнення: -XX:+UseZGC або -XX:+UseShenandoahGC
3. Принципи роботи сучасних GC
Молоде і старе покоління (Young/Old Generation)
JVM ділить купу на дві великі частини:
Молоде покоління (Young Generation): сюди потрапляють усі нові об’єкти. Тут прибирання відбувається часто і швидко (Minor GC).
Старе покоління (Old Generation, Tenured): сюди переїжджають об’єкти, які «вижили» кілька прибирань у молодому поколінні. Тут прибирання відбувається рідше, але довше (Major/Full GC).
Чому так? Більшість об’єктів у Java живе дуже недовго (наприклад, тимчасові рядки, колекції всередині методу). Тому прибирати молоде покоління можна швидко і часто, не чіпаючи старе.
Minor GC
- Очищує лише молоде покоління.
- Швидко, з короткою паузою.
- Не зачіпає старі об’єкти.
Major (Full) GC
- Очищує всю купу (і молоде, і старе покоління).
- Може займати багато часу (секунди й більше на великих купах).
- Зазвичай супроводжується довгою паузою застосунку.
Як GC визначає, які об’єкти видалити?
GC шукає «живі» об’єкти, починаючи з кореневих посилань (root set): локальні змінні у стеках потоків, статичні поля, параметри методів тощо. Усе, до чого можна «дістатися», вважається живим. Решта — сміття.
4. Порівняння сучасних збирачів: G1, ZGC, Shenandoah
Розберімося, чим відрізняються найсучасніші та найпопулярніші GC. Для цього наочна таблиця:
| Збирач | Основна мета | Модель пам’яті | Мінімальні паузи | Масштабованість | Підтримка | Коли використовувати |
|---|---|---|---|---|---|---|
| G1 | Баланс пауз/швидкості | Регіони | ~10–200 мс | До сотень ГБ | Java 9+ (за замовчуванням) | Більшість серверних застосунків |
| ZGC | Мінімальна пауза | Регіони, «кольорові мітки» | <10 мс | До терабайтів | Java 11+ | Реальний час, latency‑critical |
| Shenandoah | Мінімальна пауза | Регіони, «кольорові мітки» | <10 мс | До терабайтів | Java 12+ (Red Hat) | Реальний час, latency‑critical |
G1 GC: Garbage First
- Ділить купу на безліч регіонів (зазвичай 1–32 МБ кожен).
- Під час прибирання обирає регіони, де найбільше сміття («garbage first»).
- Може збирати лише частину купи, а не всю одразу.
- Дозволяє задати цільову паузу: -XX:MaxGCPauseMillis=200.
- Підходить для балансу між швидкістю та паузами; використовується за замовчуванням із Java 9.
Приклад увімкнення (якщо раптом вимкнено):
java -XX:+UseG1GC -jar myapp.jar
ZGC: Z Garbage Collector
- Експериментальний у Java 11, стабільний із Java 15.
- Майже не зупиняє застосунок: паузи зазвичай <10 мс, навіть за 1–2 ТБ купи.
- Використовує «кольорові мітки» (coloring) та особливі вказівники.
- Потребує 64‑бітної JVM; не працює на 32‑бітних системах.
- Підтримується на Linux, macOS, Windows.
Приклад увімкнення:
java -XX:+UseZGC -jar myapp.jar
Shenandoah
- Розроблений Red Hat; цілі подібні до ZGC.
- Мінімальні паузи, активна паралельна робота із застосунком.
- Підтримує Linux і Windows; є частиною збірок OpenJDK.
- Використовує схожі техніки, але інші внутрішні алгоритми.
Приклад увімкнення:
java -XX:+UseShenandoahGC -jar myapp.jar
Візуальне порівняння
5. Практика: як дізнатися та змінити GC
Як дізнатися, який GC використовується?
- Логи JVM: Запустіть застосунок із параметрами -Xlog:gc* (Java 9+) або -verbose:gc (до Java 8). У логах буде видно, який GC використовується і як часто відбуваються паузи.
- jcmd: Виконайте:
де <pid> — ідентифікатор процесу Java.jcmd <pid> VM.flags - jvisualvm: У розділі «Моніторинг» можна переглянути тип GC.
Як змінити GC для свого застосунку?
Додайте потрібний прапорець під час запуску Java‑програми:
G1 GC (за замовчуванням, можна вказати явно):
java -XX:+UseG1GC -jar myapp.jar
ZGC:
java -XX:+UseZGC -jar myapp.jar
Shenandoah:
java -XX:+UseShenandoahGC -jar myapp.jar
Як задати розмір купи та паузи?
- Максимальний розмір купи: -Xmx2G
- Мінімальний розмір купи: -Xms512M
- Для G1: бажана пауза — -XX:MaxGCPauseMillis=200
Приклад повного запуску:
java -Xms512M -Xmx2G -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -jar myapp.jar
6. Особливості вибору GC для різних завдань
Коли обирати G1
- У більшості серверних і настільних застосунків — чудовий вибір за замовчуванням.
- Добре працює на купах від сотень мегабайт до сотень гігабайт.
- Баланс між швидкістю та паузами.
Коли обирати ZGC або Shenandoah
- Якщо застосунок чутливий до затримок (latency‑critical: біржі, онлайн‑ігри, real‑time‑аналітика).
- Якщо купа величезна (сотні гігабайт і більше).
- Якщо допустимі лише мінімальні паузи (мілісекунди).
- Потрібні Java 11+ (ZGC) або Java 12+ (Shenandoah).
Коли достатньо Parallel GC
- Для невеликих застосунків, де важлива максимальна пропускна здатність, а паузи некритичні.
- Для пакетної обробки (batch), де можна «пережити» зупинку на Full GC.
7. Приклад: порівняння поведінки GC на простому застосунку
Невеликий застосунок, який генерує багато тимчасових об’єктів (імітація обробки замовлень):
public class GCSimulator {
public static void main(String[] args) {
while (true) {
// Створюємо 100 000 об’єктів у кожному циклі
for (int i = 0; i < 100_000; i++) {
String s = new String("Order-" + i);
}
// Трохи відпочиваємо
try { Thread.sleep(100); } catch (InterruptedException e) {}
}
}
}
Запустіть його з різними GC і подивіться на логи:
java -Xmx256M -XX:+UseG1GC -Xlog:gc* GCSimulator
java -Xmx256M -XX:+UseZGC -Xlog:gc* GCSimulator
Що ви побачите?
G1 робитиме часті, але короткі паузи. ZGC/Shenandoah — паузи ще коротші, але можуть траплятися частіше. Parallel GC — паузи довші, але рідше.
8. Типові помилки й нюанси під час роботи з GC
Помилка № 1: Очікування, що GC вирішить усі проблеми з пам’яттю. GC — це не чарівна паличка. Якщо ви тримаєте посилання на непотрібні об’єкти, жоден GC не допоможе — буде витік пам’яті.
Помилка № 2: Примусовий виклик System.gc(). JVM сама краще знає, коли прибирати сміття. Примусовий GC може спричинити довгу паузу й знизити продуктивність.
Помилка № 3: Ігнорування логів GC. Якщо не дивитися логи GC, можна не помітити, що ваш застосунок регулярно «підвисає» на Full GC.
Помилка № 4: Використання застарілих GC. Наприклад, CMS більше не розвивається. Краще переходити на G1 або сучасні low‑latency збирачі.
Помилка № 5: Неправильний вибір GC для завдання. Якщо у вас застосунок, чутливий до затримок (latency‑critical), а ви використовуєте Parallel GC — очікуйте довгих пауз. Якщо у вас пакетна обробка, а ви увімкнули ZGC — отримаєте зайві накладні витрати.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ