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

Чи можна вручну викликати GC

Однак у реалізації HotSpot JVM за замовчуванням виклик System.gc() дійсно ініціює негайний, важкий та синхронний цикл Full GC з повною зупинкою всіх потоків застосунку (Stop-the...


🟢 Junior Level

Так, програмно запросити збирання сміття можна, викликавши стандартний метод:

System.gc();
// або еквівалентний:
Runtime.getRuntime().gc();

Чи гарантує цей виклик збирання сміття?

Ні, згідно зі специфікацією Java (JLS §12.8), виклик System.gc() — це лише побажання (hint, рекомендація) віртуальній машині, а не безумовний наказ. Специфікація стверджує: «Виклик лише передбачає, що JVM докладе зусиль для переробки невикористовуваних об’єктів».

Однак у реалізації HotSpot JVM за замовчуванням виклик System.gc() дійсно ініціює негайний, важкий та синхронний цикл Full GC з повною зупинкою всіх потоків застосунку (Stop-the-World).

Зовнішній виклик через утиліту jcmd

Запустити збирання сміття можна з консолі операційної системи без зміни вихідного коду застосунку:

jcmd <PID> GC.run

🟡 Middle Level

1. Способи програмного та зовнішнього виклику

У платформі Java передбачено кілька точок ініціації збирання сміття:

Спосіб виклику Де використовується Поведінка за замовчуванням у HotSpot
System.gc() Код застосунку / бібліотеки Синхронний Full GC Stop-The-World усієї купи
Runtime.getRuntime().gc() Базовий нативний метод JVM Те саме, що System.gc() (викликається ним напряму)
MemoryMXBean.gc() JMX / MBeans моніторинг Делегує до Runtime.getRuntime().gc()
jcmd <pid> GC.run Консоль ОС (CLI) Виконує VM_GC_Operation через Attach API

2. Керування викликом: Прапорці HotSpot JVM

Поведінку System.gc() можна кардинально перевизначити на рівні параметрів запуску JVM:

# 1. Повне ігнорування System.gc() (перетворює виклик на порожню операцію no-op)
java -XX:+DisableExplicitGC -jar app.jar

# 2. Виконання збирання сміття в конкурентному фоновому режимі (для G1, ZGC, Shenandoah)
java -XX:+ExplicitGCInvokedConcurrent -jar app.jar
  • -XX:+DisableExplicitGC: блокує будь-які виклики System.gc() з коду. Метод миттєво повертає керування, не виконуючи жодних дій.
  • -XX:+ExplicitGCInvokedConcurrent: «золотий стандарт» для production. Замість руйнівного синхронного Full GC з довгою паузою Stop-the-World віртуальна машина запускає звичайний фоновий конкурентний цикл розмітки (Concurrent Marking), не зупиняючи бізнес-потоки.

3. Легітимні сценарії виклику System.gc()

У прикладному коді ручний виклик GC вважається антипатерном, проте існують рідкісні інженерні винятки:

  1. Мікробенчмаркінг (JMH): перед вимірюванням продуктивності методу необхідно привести купу до стабільного базового стану та видалити артефакти прогріву.
  2. Діагностика витоків (Delta Analysis): перед зняттям перевірочного дампу купи для відсікання короткочасного сміття (хоча надійніше використовувати GC.heap_dump -all=false).
  3. Пакетна обробка в CLI-утилітах: у короткоживучих пакетних скриптах після завершення важкої разової операції (наприклад, парсингу 10-гігабайтного XML) перед стартом наступної фази.

🔴 Senior Level

1. Трагедія -XX:+DisableExplicitGC та Direct Byte Buffers (NIO)

Багато інженерів, прагнучи захистити сервіс від викликів System.gc() у сторонніх бібліотеках, бездумно встановлюють прапорець -XX:+DisableExplicitGC. Це одна з найпідступніших пасток у production:

[Потік NIO: allocateDirect()] ──► [Off-Heap пам'ять вичерпана]
                 │
                 ▼
[Bits.reserveMemory() викликає System.gc()]
                 │
                 ├──► Прапорець -XX:+DisableExplicitGC увімкнено!
                 │    (JVM ігнорує виклик, збирання не відбувається)
                 ▼
[java.lang.OutOfMemoryError: Direct buffer memory]
 (Купа Java Heap при цьому порожня на 90%!)

Механіка збою:

  1. Буфери ByteBuffer.allocateDirect() виділяють пам’ять поза купою через нативний malloc().
  2. Java Heap не зазнає тиску пам’яті, оскільки самі Java-об’єкти DirectByteBuffer важать лише ~64 байти. Збирач сміття «не бачить» причин запускатися.
  3. Очищення нативної пам’яті покладено на клас Cleaner (PhantomReference), який відпрацьовує тільки після того, як GC збере сам Java-об’єкт буфера.
  4. Коли ліміт -XX:MaxDirectMemorySize вичерпано, вихідний код JDK (jdk.internal.misc.VM / Bits.reserveMemory()) здійснює останню спробу врятувати систему: викликає явний System.gc(), щоб форсувати очищення клінерів.
  5. За наявності -XX:+DisableExplicitGC цей рятівний виклик ігнорується, очищення не відбувається, і сервіс миттєво падає з OutOfMemoryError: Direct buffer memory.

[!TIP] Production Best Practice: Замість небезпечного -XX:+DisableExplicitGC завжди використовуйте прапорець -XX:+ExplicitGCInvokedConcurrent. Він дозволяє NIO ініціювати очищення позакучевих буферів, але робить це без згубного Stop-the-World Full GC.

2. Пошук прихованих викликів System.gc()

Якщо в логах моніторингу з’являються регулярні паузи Full GC, спричинені зовнішнім кодом, їх необхідно локалізувати:

# 1. Пошук за логами збирача сміття:
-Xlog:gc*,gc+phases=debug:file=/var/log/gc.log:time,uptime,pid:filecount=5,filesize=100M
# Шукайте рядки виду: "Pause Full (System.gc())" або "Pause Young (System.gc())"

# 2. Локалізація точного місця в коді за допомогою JFR:
# Запустіть профілювання з подією jdk.GarbageCollection
# У JMC відобразиться подія з повним стеком виклику аж до класу та рядка!

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

30-секундна відповідь для співбесіди

«Викликати збирання сміття вручну можна методами System.gc(), Runtime.getRuntime().gc() або через консоль jcmd <PID> GC.run. Згідно зі специфікацією Java, це лише рекомендація віртуальній машині, але в HotSpot за замовчуванням цей виклик провокує важкий синхронний Full GC з довгою паузою Stop-The-World. У бойовому коді викликати System.gc() заборонено. Поведінку можна налаштувати прапорцями: -XX:+DisableExplicitGC (повне відключення) або -XX:+ExplicitGCInvokedConcurrent (виконання у фоновому конкурентному режимі, що є кращим для запобігання збоям у роботі Direct Memory)».


Питання підвищеної складності з відповідями

  1. Чи гарантує специфікація Java, що після виконання System.gc() сміття буде зібране?
    • Відповідь: Ні, специфікація мови Java (JLS) та документація JavaDoc явно вказують, що виклик System.gc() лише висловлює побажання віртуальній машині докласти зусиль щодо звільнення невикористовуваної пам’яті. JVM має повне право проігнорувати цей запит. На практиці в HotSpot виклик збирає пам’ять, але може бути повністю заблокований прапорцем -XX:+DisableExplicitGC.
  2. Чому ввімкнення -XX:+DisableExplicitGC може призвести до OutOfMemoryError у застосунку, що використовує Netty або gRPC?
    • Відповідь: Netty та мережевий стек Java активно використовують нативні буфери ByteBuffer.allocateDirect(). Нативна пам’ять звільняється під час збирання сміття через механізм Cleaner / PhantomReference. Якщо нативна пам’ять вичерпана, а купа вільна, стандартний механізм JDK (Bits.reserveMemory()) намагається врятувати систему, викликаючи System.gc(). Якщо цей виклик заблоковано прапорцем -XX:+DisableExplicitGC, JVM не запускає збирання сміття, очищення нативних буферів не відбувається, і застосунок падає з фатальною помилкою java.lang.OutOfMemoryError: Direct buffer memory.
  3. Що робить прапорець -XX:+ExplicitGCInvokedConcurrent і чому він кращий за -XX:+DisableExplicitGC?
    • Відповідь: Цей прапорець перенаправляє виклики System.gc() на конкурентний алгоритм збирача (наприклад, Concurrent Mark у G1 або фонові фази ZGC/Shenandoah) замість традиційного синхронного Full GC Stop-the-World. Це виключає руйнівні багатосекундні паузи в застосунку, але при цьому зберігає можливість для підсистем на кшталт Java NIO вчасно очищати пов’язані нативні ресурси та Cleaner-и.
  4. Як запустити збирання сміття на віддаленому сервері без перезапуску JVM та зміни коду?
    • Відповідь: За допомогою утиліти jcmd <PID> GC.run. Команда передається через HotSpot Attach API безпосередньо в працюючий процес JVM, ініціюючи збирання сміття. Також виклик можна виконати віддалено через JMX за допомогою будь-якого інструменту моніторингу, викликавши операцію gc() на керуючому біні java.lang.management.MemoryMXBean.

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

  • 🚩 Стверджувати, що System.gc() зобов’язаний миттєво звільнити 100% сміття. (Red Flag: нерозуміння специфікації JLS та рекомендаційної природи виклику).
  • 🚩 Рекомендувати викликати System.gc() у блоках finally для «звільнення пам’яті». (Red Flag: грубе порушення практик написання Java-застосунків, руйнування адаптивних евристик збирача сміття).
  • 🚩 Вважати -XX:+DisableExplicitGC абсолютно безпечним прапорцем за замовчуванням для будь-якого проєкту. (Небезпека: ризик падіння сервісу через витік Direct Memory в NIO/Netty/Kafka).

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