Огляд Garbage Collectors в Java

💡 Усі статті, обговорення, новини про Java — в одному місці. Приєднуйтесь до Java спільноти!

Раніше я вже писав про модель памʼяті в Java, і стаття виявилась доволі популярною. Там я згадав про основні збирачі сміття коротко, але цього явно недостатньо, аби зрозуміти, як все працює, і які варіанти. Тому я вирішив написати другу частину, де сфокусуюсь саме на збирачах сміття.

Сучасні версії Java (JDK 21/23 і вище) пропонують декілька збирачів сміття, кожен з яких орієнтований на певні завдання. У цьому аналізі порівнюються використання пам’яті, час пауз та пропускна здатність для таких збирачів:

  • Serial GC (-XX:+UseSerialGC) — однопотоковий збирач, що працює у режимі stop-the-world.
  • Parallel GC (-XX:+UseParallelGC) — багатопотоковий збирач, орієнтований на максимальну пропускну здатність.
  • G1 GC (-XX:+UseG1GC) — збирач, який намагається збалансувати час пауз і пропускну здатність.
  • Shenandoah GC (-XX:+UseShenandoahGC) — конкурентний збирач з дуже низькими паузами.
  • ZGC (-XX:+UseZGC) — збирач з наднизькою паузою, що майже повністю працює конкурентно.
  • Generational ZGC (-XX:+UseGenerationalZGC або з параметром -XX:+ZGenerational у JDK 21) — новинка, що додає генераційну модель до ZGC.

Варто зауважити, що виклик System.gc() або finalize() лише пінгане збирач сміття, а той вже на свій розсуд вирішить, чи запускати збірку сміття, чи поки ні.

Давайте розглянемо кожен збирач сміття окремо.

Serial GC

Serial GC — це перший збирач сміття, впроваджений в Java. Весь офіс зупиняється, коли цей прибиральник приступає до роботи, і поки він не закінчить, ніхто не сміє ворухнутися.

Його робота доволі проста: зупинити застосунок, позначити всі живі обʼєкти, видалити зайві, зробити ущільнення (дефрагментацію). По факту його переваги закінчуються там, де починаються багатопоточні програми, бо він зупинить все, що можливо, коли прийметься до роботи.

Для використання додайте до параметрів запуску -XX:+UseSerialGC.

Особливості

  • Однопоточний підхід.
  • Підходить для невеликих heap.
  • Паузи на збір сміття.
  • Відсутність фрагментації (виконує compacting).

Робочі фази Serial GC:

  • Mark — зупинка всіх потоків, визначення «живих» об’єктів.
  • Sweep — видалення непотрібних об’єктів.
  • Compact — ущільнення пам’яті для уникнення фрагментації.

Parallel GC

Цей хлопчина вже орієнтований на багатопоточні програми. Він зупиняє виконання програми тільки на етапі дефрагментації, а маркування проводить в паралельному потоці. Цей збирач сміття є генеративним, тобто ділить купу на генерації: молоде, старе, постійне (Permanent) покоління. Молоде покоління ділиться на три зони: Eden (місце, куди потрапляють новостворені обʼєкти), і дві зони виживших. S0 i S1 — одна з них завжди пуста. При дефрагментації всі виживші обʼєкти з Eden та заповненої зони вирушають у пусту S-зону. У випадку, якщо обʼєкт пережив достатню кількість дефрагментацій, збирач сміття може його перемістити в старе покоління.

Цей збирач сміття робить нахил в сторону пропускної здатності. Йому властиві два різні типи збірок сміття: Minor GC та Full GC. Minor GC запускається у випадку, якщо памʼять виділена на молоде покоління заповнюється. Full GC — у випадку, якщо памʼять старого покоління заповнена.

Був збирачем сміття за замовчуванням до Java 9.

Для використання додайте до параметрів запуску -XX:+UseParallelGC.

Особливості

  • Багатопотоковий збірник для кращої продуктивності.
  • Зосереджений на високій пропускній здатності (throughput).
  • Паузи на збір сміття.
  • Відсутність фрагментації (виконує compacting).

Робочі фази Parallel GC:

  • Mark — виявлення активних об’єктів у young generation.
  • Copy — переміщення активних об’єктів у survivor space.
  • Mark — зупинка потоків, визначення «живих» об’єктів у old generation.
  • Sweep — видалення мертвих об’єктів.
  • Compact — ущільнення пам’яті.

PermGen -> Metaspace

Що зберігалося в PermGen?

Метадані класів (Class Metadata)

  • Опис класів, полів, методів.
  • Інформація про спадкування та інтерфейси.

Пул констант (Runtime Constant Pool)

  • Числа, рядки, посилання на методи та інші константи.
  • Це частина Class File Constant Pool, яка переноситься в пам’ять під час завантаження класу.

Інтерновані строки (String Interning)

  • String.intern() зберігав строки в PermGen, щоб уникнути дублікатів.

Дані про анотації

  • Метадані анотацій, особливо якщо вони мають RetentionPolicy.RUNTIME.

Динамічно згенеровані класи (наприклад, Proxy-класи)

  • Використовуються у reflection, JVM-байткод генераторах (CGLIB, Javassist).

Чому PermGen видалили в Java 8?

  1. Обмежений розмір — встановлювався при старті JVM (-XX:MaxPermSize), і якщо місця не вистачало, виникала OutOfMemoryError: PermGen space.
  2. Фрагментація пам’яті — об’єкти класів мали різні розміри, що ускладнювало управління пам’яттю.
  3. Не гнучке управління — JVM не могла легко змінювати розмір PermGen під час роботи.

Що прийшло на заміну PermGen?

З Java 8 всі ці дані перенесли в Metaspace, який тепер:
✔ Динамічно змінює розмір (не потребує -XX:MaxPermSize).
✔ Використовує пам’ять операційної системи, а не heap.
✔ Виключає String pool з Metaspace, зберігаючи його в heap (Eden/Old Gen).

Тому Java 8 і вище стали більш стабільними в роботі з великою кількістю класів (наприклад, у великих сервісах або при активному використанні Reflection/Proxy).

Concurrent Mark-Sweep (CMS) GC

Цей парубок моторний відмовляється виконувати зайву роботу, тому ніякої дефрагментації він не робить. Через це паузи менші, але він страждає від фрагментації і частіше вимушений запускати Full GC.

Для використання додайте до параметрів запуску -XX:+UseConcMarkSweepGC.

Особливості:

  • Мінімізує паузи за рахунок одночасної роботи з потоками додатку.
  • Підходить для low-latency систем.
  • Страждає від фрагментації (не виконує compacting).

Робочі фази CMS:

  • Initial Mark — швидке визначення кореневих об’єктів (stop-the-world).
  • Concurrent Mark — виявлення живих об’єктів (без зупинки потоків).
  • Remark — перевірка змін, зроблених під час Concurrent Mark (stop-the-world).
  • Sweep — видалення мертвих об’єктів.
  • Reset — підготовка до нового циклу.

G1 GC

G1 (Garbage First) — це збирач сміття, який використовується за замовчуванням у версіях Java 9 — 23. Він також вважається генеративним, тому що працює з поколіннями, але має багато особливостей, яких немає в Parallel GC. G1 ділить купу на регіони. Він уже не призначає якесь місце в памʼяті під ту чи іншу зону, а працює з цим більш гнучко, дозволяючи собі при потребі будь-який регіон зробити частиною молодого чи старого покоління, таким чином уникаючи багатьох проблем з нестачею памʼяті. Бувають масивні обʼєкти, які можуть не вміститися в одному регіоні, тоді такі обʼєкти займають декілька сусідніх регіонів і звуться Humongous.

Цей збирач сміття старається виконувати міксовані збірки, щоб не запускати повні.

G1 має ще одну цікаву здібність, він використовує RememberSet. Remembered Sets — це структури даних, які використовує G1 Garbage Collector для відстеження посилань на об’єкти, розташовані в інших регіонах. Кожен регіон має свій власний Remembered Set, який зберігає інформацію про всі посилання з об’єктів з інших регіонів на об’єкти в цьому регіоні. Приклад: якщо об’єкт у регіоні A має посилання на об’єкт у регіоні B, це посилання буде збережено в Remembered Set регіону B. Це дозволяє GC збирати регіон B без необхідності сканувати всю купу.

Write Barriers — це механізми, що затримують запис даних. Вони використовуються для оновлення Remembered Sets під час зміни посилань на об’єкти. Коли програма змінює посилання на об’єкт, write barrier виконується, щоб відповідний RSet був оновлений. Приклад: якщо об’єкт у регіоні A змінює своє посилання з об’єкта у регіоні B на об’єкт у регіоні C, Write Barrier забезпечить, що RSet для регіону B видалить це посилання, а RSet для регіону C додасть нове посилання.

Ці механізми допомагають збирачу сміття ефективно відстежувати та управляти збором сміття, мінімізуючи необхідність великих зупинок програми для аналізу всієї купи. Внутрішні об’єкти або поля класу в Java зберігаються у регіонах купи залежно від місця розташування об’єкта-контейнера (тобто об’єкта, який містить ці поля). Якщо поле класу є посиланням на інший об’єкт, який розташований в іншому регіоні, це посилання зазначатиметься в Remembered Set регіону, де знаходиться посиланий об’єкт. Це важливо для збирачів сміття, таких як G1 GC, оскільки дозволяє ефективно керувати зборами сміття без необхідності сканувати всю купу. Завдяки Write Barriers, які оновлюють Remembered Sets під час зміни посилань, JVM може точно відстежувати та управляти живими об’єктами у різних регіонах, що допомагає оптимізувати процес збору сміття і зменшити зупинки виконання програми.

Для використання додайте до параметрів запуску -XX:+UseG1GC

Особливості

  • Регіональний підхід.
  • Змішані збори сміття.
  • Паузи на збір сміття.
  • Видалення фрагментації.

Робочі фази G1 GC:

  • Початкове маркування (Initial Mark).
  • Конкурентне маркування (Concurrent Marking).
  • Завершення маркування (Final Mark).
  • Очищення (Cleanup).

Shenandoah GC

Суть цього збирача сміття полягає у тому, що він виконує дефрагментацію без зупинки потоків, використовуючи таку штуку як self-pointer. Self-pointer це додаткове проміжне посилання, яке вказує на сам обʼєкт в той час, коли інші посилання цього обʼєкту звертаються саме до self-pointer. Тобто ми на кожен обʼєкт створюємо додаткове проміжне посилання, що дозволяє уникати великих пауз, але більше засмічує памʼять і використання ресурсів.

Для використання додайте до параметрів запуску -XX:+UseShenandoahGC.

Особливості:

  • Виконує compacting без зупинки потоків.
  • Паузи не залежать від розміру heap.
  • Потребує більше ресурсів CPU.
  • Використовує brooks pointers (self-pointer) для оновлення посилань.

Робочі фази:

  • Concurrent Mark — виявлення живих об’єктів.
  • Concurrent Evacuate — переміщення об’єктів (compacting).
  • Update References — оновлення посилань на переміщені об’єкти.
  • Concurrent Cleanup — очищення регіонів.

ZGC

Цей дядько чисто ліпить всюди свої різнокольорові стікери-підказки. Його унікальність полягає у використанні так званих load barrierscolored pointers. Він створює на кожне посилання кольоровий покажчик, який вказує стан обʼєкту. А за допомогою load barriers перед тим, як працювати з обʼєктом ZGC, перевіряє стан цього обʼєкта, тобто colored pointer. Він орієнтується на велику купу і готовий працювати з великими наборами даних швидко. В Java 21 Oracle випустили генеративну версію ZGC, який вже працює, як і G1GC, ділячи купу на генерації. А у Java 23 ZGC став Generational за замовчуванням.

Для використання додайте до параметрів запуску -XX:+UseZGC .

Або для генеративного -XX:+UseGenerationalZGC.

При дефрагментації основна програма зупиняється, кольорові покажчики уникають цього. ZGC додає мета-інформацію (кольори) прямо в покажчик на об’єкт, тобто в саму адресу об’єкта. Це дозволяє йому визначати статус об’єкта без необхідності окремої таблиці.

Що означають кольори

  1. Marked0 (0001) та Marked1 (0010) — використовуються для позначення покажчиків на різних фазах збирання.
  2. Remapped (0100) — вказівник позначається цим бітом у випадку, якщо адреса в покажчику є остаточною і не повинна модифікуватися в рамках поточного циклу складання.
  3. Finalizable (1000) — цим бітом позначаються об’єкти, які можна досягти тільки з фіналізатора.

Ось як працює load barrier у ZGC:

  1. Прочитати об’єкт: коли програма хоче прочитати об’єкт в пам’яті, ZGC застосовує load barrier, щоб перевірити, чи об’єкт переміщений.
  2. Перевірка: якщо об’єкт переміщений, load barrier оновить посилання на нову адресу об’єкта.
  3. Оновлення посилання: після того, як посилання на об’єкт оновлено, програма може продовжити свою роботу з правильним об’єктом.

Наприклад:

public class ZGCDemo {
    public static void main(String[] args) {
        // Створення простого об'єкта класу Object
        Object myObject = new Object();
        // Збережемо посилання на об'єкт
        Object anotherReference = myObject;
        // Операція, де ми можемо викликати збір сміття
        System.out.println("Before GC: " + myObject);
        // Припускаємо, що в цей момент виконується збір сміття, наприклад ZGC.
        // ZGC може перемістити об'єкт у нове місце пам'яті.
        // Після збору сміття, load barrier забезпечує, що посилання 'myObject'
        // буде оновлене до нової адреси об'єкта, якщо той був переміщений.
        // Отже, коли ми виведемо myObject знову, ми отримуємо коректне посилання на об'єкт
        System.out.println("After GC: " + myObject);
    }
}

Особливості:

  • Використовує colored pointers та load barriers.
  • Паузи збору сміття ≤ 10 мс, незалежно від розміру heap.
  • Підтримує великі heap (до 16 ТБ).
  • Використовує compacting для зменшення фрагментації.

Робочі фази:

  • Mark Start — швидка ініціалізація процесу збору сміття.
  • Concurrent Mark — паралельне маркування живих об’єктів.
  • Concurrent Relocate — пересування живих об’єктів у нові місця.
  • Concurrent Remap — оновлення посилань (colored pointers).

Epsilon GC

Цей технічний працівник знову не вийшов на роботу, геть нічого не робить. Він використовується суто для тестування без впливу збирачів сміття, йому все одно на ті генерації, обʼєкти і всю ту лабуду, він живе своє краще життя і відмовляється взагалі щось робити.

Для використання додайте до параметрів запуску -XX:+UseEpsilonGC.

Особливості:

  • Взагалі не виконує збору сміття.
  • Мінімальні накладні витрати.
  • Використовується для тестування без впливу GC.

Робочі фази:

Немає фаз, оскільки не виконує збору сміття.

Бенчмарки

Принципи роботи: кожен із збирачів розрахований на досягнення певних показників. Наприклад, Parallel GC орієнтований на максимальну пропускну здатність, G1 GC намагається збалансувати затримку і пропускну здатність, а ZGC і Shenandoah — мінімізувати час пауз. Різні підходи призводять до компромісів: покращення одного показника може негативно впливати на інший («трикутник продуктивності»).

Нижче подано зведену таблицю результатів бенчмарків для кожного збирача:

Примітка: наведені результати базуються на даних з різних джерел (бенчмарки SPECjbb® 2015, навантажувальних тестах для Cassandra, мікробенчмарках JMH та реальних сценаріях) для Java 21/23 із зазначеними параметрами GC. Конкретні результати залежать від навантаження, але ці дані дають репрезентативне порівняння.

Аналіз використання пам’яті

Сучасні збирачі сміття відрізняються за тим, скільки додаткової пам’яті вони потребують для обліку і як ефективно використовують heap:

  • Serial та Parallel GC: Використовують просту генераційну модель (молода/стара генерації) із алгоритмом mark-compact. Накладні витрати тут мінімальні — немає складних таблиць або конкурентних структур. Serial GC є особливо ефективним для невеликих heap, а Parallel GC додає лише незначні структури для багатопотоковості. Недолік — вони не повертають пам’ять ОС так агресивно, як G1 чи Shenandoah.
  • G1 GC: Розбиває heap на багато регіонів і використовує пам’ятні множини (Remembered Sets) для відстеження посилань між регіонами. Це додавало накладні витрати (наприклад, два набори бітів для маркування, метадані на регіон), але оптимізації останніх релізів зробили G1 одним із найбільш пам’ятєв ефективних збирачів. Наприклад, за SPECjbb-тестами G1 показував найнижчі пікові накладні витрати пам’яті в JDK 21. Крім того, G1 може поступово повертати невикористану пам’ять ОС.
  • Shenandoah GC: Також регіональний, але без генерацій — кожен регіон обробляється конкурентно. Він не використовує пам’ятні множини, а натомість записує «форвардинг-пойнтер» у кожен об’єкт для переміщення. Це додає приблизно 8 байт на об’єкт (що може становити 5–10% накладних витрат), але Shenandoah швидко повертає вільні регіони ОС, що може зменшити загальний footprint при зниженому навантаженні.
  • ZGC: Застосовує інший підхід для мінімізації пауз — використовує кольорові покажчики та конкурентну дефрагментацію, що дозволяє працювати з великими heap, не розбиваючи їх на фіксовані покоління (до появи generational mode у JDK 21). Проте, щоб уникнути зупинок через алокацію, ZGC може вимагати трохи більшого запасу вільної пам’яті. Крім того, деякі структури зберігаються поза heap (native memory).
  • Generational ZGC: додавання молодої генерації дозволяє частіше збирати молоді об’єкти, що зменшує потребу у великому запасі вільної пам’яті. Компенсацією є додаткові накладні витрати на відстеження посилань між поколіннями через пам’ятні множини, які займають native memory. Деякі експерименти показали, що generational ZGC може використовувати до 75% менше heap-пам’яті для того ж навантаження, що робить його ефективним навіть за рахунок трохи вищої загальної витрати пам’яті.

Аналіз часу пауз (latency)

Stop-the-world (STW) паузи є критичним показником для відгуку застосунків. Порівняємо типові значення часу пауз та їх вплив на latency:

  • Serial GC. При кожній збірці всі потоки зупиняються. Навіть для невеликих heap (кілька ГБ) паузи можуть тривати сотні мілісекунд або навіть секунди, що робить цей збирач непридатним для систем, де важлива швидка реакція.
  • Parallel GC. Використовує багатопоточність для скорочення часу зупинок. Наприклад, якщо для Serial GC невелика збірка може займати 200 мс, то з Parallel GC вона може завершитись за 50 мс за умови розподілу роботи між потоками. Проте оскільки під час GC застосунок зупиняється, паузи все одно можуть бути значними — від десятків мілісекунд для молодих збірок до секунд для повних збірок при великому heap.
  • G1 GC. Розроблений для уникнення надто тривалих пауз шляхом поступового виконання роботи. G1 проводить конкурентне маркування та обробляє heap регіонально, що дозволяє встановлювати цільовий час пауз (наприклад, параметром -XX:MaxGCPauseMillis=100). На практиці це означає, що молоді збірки тривають 10–50 мс, а змішані — 50–200 мс (залежно від розміру heap). При цьому медіанні значення пауз часто значно нижчі за встановлений ліміт.
  • Shenandoah GC. Головний принцип Shenandoah — паузи практично не залежать від розміру heap. Практично всі операції виконуються конкурентно, а зупинки потрібні лише для коротких етапів (початкове маркування, фінальні оновлення посилань), які вимірюються у мікросекундах або низьких мілісекундах (<10 мс). Це робить його ідеальним для застосунків з великими обсягами пам’яті та критичною latency.
  • ZGC. Розроблений для наднизької latency, ZGC практично не припиняє роботу застосунка — паузи зазвичай становлять 0,1–1,0 мс. Навіть при збільшенні розміру heap час зупинок залишається незмінним, що є величезною перевагою для масштабованих систем.
  • Generational ZGC. Додавання генерацій не вплинуло суттєво на час пауз — показники залишаються в субмілісекундному діапазоні. Можуть спостерігатись незначні покращення (порядку 20–30 мкс у P99), що робить його особливо ефективним для застосунків із високими вимогами до latency.

Підсумок затримок. Для систем, де важлива низька затримка, Shenandoah та ZGC (зокрема Generational ZGC) є кращими варіантами — їх паузи зазвичай менше 1 мс, навіть при великих heap. G1 забезпечує контрольовані, передбачувані паузи (десятки мілісекунд), що може бути прийнятним для багатьох сервісів. Serial і Parallel GC мають занадто великі паузи для latency-чутливих застосувань.

Аналіз пропускної здатності (throughput)

Пропускна здатність визначає, скільки роботи може виконати застосунок, і часто вимірюється як обернений показник накладних витрат GC. Ось як впливає кожен збирач на пропускну здатність:

  • Serial GC. Не призначений для високої пропускної здатності на сучасному апаратному забезпеченні. Оскільки GC виконується лише одним потоком і всі інші потоки зупиняються, ефективність сильно страждає на багатоядерних системах.
  • Parallel GC. Часто є чемпіоном у зупинених збірках завдяки використанню всіх ядер. Після завершення зупинки застосунок працює на повну потужність, тому для тривалих робочих навантажень Parallel GC може демонструвати дуже високу пропускну здатність. Недолік — відсутність контролю над часом пауз, що не є критичним, якщо вимірювати загальну кількість завершених операцій за певний проміжок часу.
  • G1 GC. Спрямований на баланс між пропускною здатністю та затримкою. Concurrent-маркування та підтримка пам’ятних множин додають деякі накладні витрати (CPU) порівняно з Parallel GC, проте на практиці пропускна здатність G1 дуже близька до Parallel — різниця всього кілька відсотків. Для багатьох сервісів компроміс є прийнятним, адже G1 забезпечує набагато кращу latency.
  • Shenandoah GC. Як і інші конкурентні збирачі, Shenandoah використовує частину CPU для виконання GC паралельно з роботою застосунка, що може трохи знизити пропускну здатність. Проте якщо система не є CPU-обмеженою, різниця може бути незначною (однозначні відсотки). Для CPU-навантажених застосунків G1 може використовувати CPU виключно для робочих процесів, що дає йому перевагу.
  • ZGC. Початковий ZGC мав схожі показники з Shenandoah — додаткове навантаження через операції з кольоровими покажчиками додавало витрат CPU, що іноді призводило до зниження пропускної здатності на 5–15% порівняно з G1. Netflix навіть повідомляв про випадки, коли негенераційний ZGC використовував до 36% більше CPU, що знижувало загальну продуктивність.
  • Generational ZGC. Завдяки розділенню збору молодих об’єктів generational ZGC суттєво покращив пропускну здатність — внутрішні тести показали приріст близько 10% порівняно з оригінальним ZGC, а у деяких реальних навантаженнях його пропускна здатність практично дорівнює G1 (різниця всього 2–3%). Це означає, що generational ZGC може забезпечувати пропускну здатність, конкурентну з G1, при цьому зберігаючи наднизькі паузи.

Підсумок пропускної здатності. Parallel GC і G1 GC зазвичай лідирують у чистій пропускній здатності при довготривалих робочих навантаженнях. Parallel GC максимально використовує CPU під час зупинок, а G1 — практично на рівні, проте з кращими показниками latency. Shenandoah і ZGC історично мали невеликі втрати через конкурентні операції, але з сучасними JDK ця різниця значно зменшилася. Особливо Generational ZGC відзначається — він забезпечує пропускну здатність майже на рівні G1 (різниця декілька відсотків), що робить його дуже привабливим вибором для майбутніх застосувань.

Рекомендації для різних навантажень

На основі даних бенчмарків та характеристик можна зробити наступні рекомендації:

  • Системи з критичною latency або реального часу.
    Найкращим вибором буде ZGC (зокрема Generational ZGC) або Shenandoah GC. Вони забезпечують паузи менше 1 мс навіть при великих обсягах пам’яті. Наприклад, для фінансових застосунків або високочастотних сервісів ZGC забезпечує паузи в десятках мікросекунд. Generational ZGC має перевагу в пропускній здатності та ефективно обробляє високу швидкість алокацій. Якщо latency є критичною, Serial і Parallel GC не підходять через занадто тривалі зупинки.
  • Пакетні або офлайн-обчислення з високою пропускною здатністю.
    Parallel GC зазвичай демонструє найвищу пропускну здатність для довготривалих завдань, де неважливі короткочасні зупинки. Якщо ви запускаєте обчислювальне завдання або обробку даних, де декілька сотень мілісекунд пауз не критичні, Parallel GC дозволить максимально використовувати доступні ядра. G1 GC також є хорошим вибором, адже забезпечує майже однакову пропускну здатність, при цьому зменшуючи час зупинок.
  • Застосунки з великим об’ємом пам’яті.
    Для застосувань з дуже великим heap (десятки гігабайт і більше) Shenandoah або ZGC є оптимальними — їх час пауз залишається стабільним незалежно від розміру heap. G1 може працювати з великими heap, але може вимагати тонкого налаштування та час від часу створювати довші паузи (наприклад, при 50+ ГБ). Parallel GC не підходить через дуже довгі зупинки.
  • Загальні, універсальні застосування.
    Для більшості вебсервісів та змішаних навантажень G1 GC залишається надійним вибором. Він забезпечує баланс між пропускною здатністю та latency, мінімальне налаштування і стабільність, що підтверджено роками використання. Проте з появою JDK 21+ Generational ZGC стає все більш конкурентоспроможним і може запропонувати переваги з точки зору як latency, так і пропускної здатності, що робить його дуже привабливим варіантом.
  • Невеликі застосунки або контейнери.
    Для невеликих Java-застосунків (наприклад, мікросервісів із heap у декілька сотень МБ) або застосунків з критичним часом запуску може підійти Serial GC або G1. Serial GC має найменші накладні витрати і дуже швидкий старт, проте в подальшому може створювати значні зупинки. G1 має трохи більші накладні витрати, але забезпечує кращу latency. Parallel GC для малих контейнерів може не розкрити свій потенціал, якщо контейнер має лише кілька ядер. ZGC традиційно мав мінімальний розмір heap (через внутрішні регіональні налаштування), тому для дуже малих сервісів він може бути надмірним.

Висновок

Java 23+ пропонує збирач сміття на будь-які потреби:

  • Якщо потрібно максимізувати пропускну здатність і можна допустити зупинки, тоді Parallel GC (або G1 GC для більш збалансованого підходу) буде гарним вибором.
  • Якщо критично важлива низька latency, Generational ZGC є одним із найкращих варіантів (з паузами нижче 1 мс та пропускною здатністю, що майже дорівнює найкращим збирачам).
  • Для загального застосування з балансом між затримкою та пропускною здатністю G1 GC залишається перевіреним вибором, але Generational ZGC стає все більш привабливим.
  • Serial GC варто використовувати лише для дуже специфічних сценаріїв (наприклад, невеликі вбудовані пристрої чи застосунки з обмеженим використанням ресурсів).

Завжди проводьте бенчмарки для вашого конкретного застосування, адже загальні тенденції можуть відрізнятись залежно від патернів алокації, розміру активного набору даних, вимог до latency та доступності CPU. Сучасні вдосконалення в GC дозволяють більшості збирачів працювати «з коробки» без надмірного налаштування.

Наприклад, якщо ви використовуєте JDK 21, варто експериментувати з Generational ZGC (активується параметром -XX:+UseZGC -XX:+ZGenerational) і перевірити вплив на продуктивність вашого застосунку. Багато користувачів повідомляють про значне зниження tail-latency без втрати пропускної здатності, що робить цей варіант дуже привабливим.

👍ПодобаєтьсяСподобалось11
До обраногоВ обраному9
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Без тестів продуктивності та опису досвіду роботи з кожним GC це не стаття, а просто скопійовані обрізки з документації JVM.

Стаття вийшла доволі обширною, тому було прийнято рішення розділити її на декілька частин, результати продуктивності та тюнінг буде в наступній частині

G1 (Garbage First) — це збирач сміття, який використовується за замовчуванням у версіях Java 9 — 23.

Nope )
docker run —rm -it eclipse-temurin:23 java “-Xlog:gc*” -version | grep Using
[0.015s][info][gc ] Using Serial

Більше того, в native image саме Serial GC дефолтний www.graalvm.org/...​ormance/MemoryManagement

Схоже тут я все таки помилився, G1GC досі збирач сміття за замовчуванням, ZGC просто тепер за замовчуванням Generational. Різні вендори можуть ставити різні дефолтні конфіги, тільки що перевірив на версії Oracle-23.0.2 G1GC дефолтний. Для Temurin-23.0.2 знову ж таки G1GC. Спробуйте перевірити саме jdk, не використовуючи докер.

Різні вендори можуть ставити різні дефолтні конфіги

Справа не у вендорах і не в докері (в докері це просто легше продемонструвати), а в коді хотспота

github.com/...​c/shared/gcConfig.cpp#L98
github.com/...​untime/os.cpp#L1880-L1881

G1GC дефолтний тільки на 2CPU і 2Гіга, інакше Serial )

Так з цього треба було починати)

> Використовує пам’ять операційної системи, а не heap

Що це значить у цьому протиставлені — OS memory vs heap?
В цьому аспекті можна розглядати статично розподілену пам’ять, динамічно розподілений heap та стекову пам’ять (все це в рамках якогось процесу(ів), в тому числі системних).

Підписатись на коментарі