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

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

Виняток: ExplicitGCInvokedConcurrent для планового Concurrent GC (вимагає глибокого розуміння вашого GC).

Мовні версії: English Russian Ukrainian

🟢 Junior Level

Так, можна: System.gc() — запит до JVM на запуск збирання сміття.

System.gc();  // "Будь ласка, прибери сміття"

АЛЕ це антипатерн!

  • GC сам знає, коли прибирати
  • Ручний виклик = зупинка додатку
  • У production НЕ використовуйте System.gc() для регулярного очищення. Виняток: ExplicitGCInvokedConcurrent для планового Concurrent GC (вимагає глибокого розуміння вашого GC).

Коли можна:

  • У тестах
  • У бенчмарках
  • Перед зняттям дампа

🟡 Middle Level

Чому це погано

1. Full GC = Stop-The-World
   → Всі потоки зупиняються
   → Пауза: секунди на великих Heap

2. GC краще знає, коли збирати
   → Само-навчальні алгоритми (G1, ZGC)
   → Ручний виклик скидає статистику

3. Неефективно
   → В Eden ще багато місця
   → Марна трата CPU

Direct Buffers — виняток

// NIO виділяє пам'ять поза Heap
ByteBuffer buf = ByteBuffer.allocateDirect(100_000_000);
// 100 МБ поза -Xmx!

// JVM не бачить навантаження на Native Memory
// → GC не запускається (Heap порожній)
// → Native Memory закінчується
// → NIO викликає System.gc()!

// Якщо вимкнути: -XX:+DisableExplicitGC
// → OOM у Native Memory при порожньому Heap!

Прапорці управління

# Повністю ігнорувати System.gc()
-XX:+DisableExplicitGC

# Виконувати конкурентно (G1)
-XX:+ExplicitGCInvokedConcurrent

Альтернатива: jcmd

# Зовнішній виклик GC (без зміни коду)
jcmd <PID> GC.run

🔴 Senior Level

ExplicitGCInvokedConcurrent

Замість Full GC STW:
  → G1 запускає фоновий цикл
  → Додаток працює
  → Мінімальні паузи

«Золота середина» для production

GC Thrashing маскування

// ❌ Спроба «полікувати» витік
try {
    allocate();
} catch (OutOfMemoryError e) {
    System.gc();  // Маскує проблему!
    allocate();   // Все одно впаде
}

// Додаток входить у Thrashing:
// 95% часу на GC, 5% на роботу

JIT деоптимізація

Full GC може призвести до class unloading, що викликає деоптимізацію
гарячих методів. Code Cache очищується окремо (CodeCache sweeper),
але class unloading при Full GC впливає на JIT-скомпільовані методи.

  → Після GC: інтерпретація
  → JIT перекомпілює заново
  → Throughput падає

Діагностика

# Хто викликає System.gc()?

# 1. GC Logs
-Xlog:gc*
# → Шукайте: "Pause Full (System.gc())"

# 2. JFR
# → Подія "System GC"
# → Повний стек-трейс виклику

# 3. jcmd
jcmd <PID> GC.run  # Ручний виклик для тестів

Best Practices

  1. НЕ використовуйте System.gc() у бізнес-коді
  2. ExplicitGCInvokedConcurrent для production
  3. jcmd GC.run для діагностики
  4. DisableExplicitGC обережно (Direct Buffers!)
  5. Шукайте того, хто викликає через JFR
  6. Бенчмарки — єдине виправдання

Резюме для Senior

  • System.gc() = антипатерн у production
  • Direct Buffers = єдиний виняток
  • ExplicitGCInvokedConcurrent = компроміс
  • JFR = знайти того, хто викликає
  • JIT деоптимізація = прихований overhead
  • jcmd = зовнішній виклик для тестів

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

Обов’язково знати:

  • System.gc() — запит JVM на Full GC (STW, секунди на великих Heap); антипатерн у production
  • GC краще знає, коли збирати: само-навчальні алгоритми (G1, ZGC) адаптуються до Allocation Rate; ручний виклик скидає статистику
  • Direct Buffers — виняток: NIO пам’ять поза Heap; GC не бачить навантаження (Heap порожній) → Native Memory закінчується → NIO викликає System.gc()
  • -XX:+DisableExplicitGC — повністю ігнорує System.gc(); небезпечно для Direct Buffers (OOM у Native Memory при порожньому Heap)
  • -XX:+ExplicitGCInvokedConcurrent — «золота середина»: G1 запускає фоновий конкурентний цикл замість Full GC STW
  • JIT деоптимізація: Full GC → class unloading → деоптимізація гарячих методів → інтерпретація → throughput падає
  • Діагностика: -Xlog:gc* (шукайте “Pause Full (System.gc())”), JFR (подія “System GC” зі стек-трейсом)

Часті уточнюючі запитання:

  • Чому Direct Buffers — виняток?ByteBuffer.allocateDirect() виділяє native memory поза Heap; GC не бачить навантаження на Heap → не запускається → Native Memory закінчується → NIO викликає System.gc()
  • Що таке GC Thrashing? — Додаток входить у цикл: 95% часу на GC, 5% на роботу; System.gc() маскує проблему, не вирішує
  • Чому JIT деоптимізація — проблема? — Full GC → class unloading → гарячі методи перекомпілюються заново → throughput падає на секунди
  • Коли легітимно викликати System.gc()? — Бенчмарки (JMH), тести, перед зняттям дампа для очищення сміття

Червоні прапорці (НЕ говорити):

  • «Викликаю System.gc() після великих операцій для оптимізації» — GC сам знає; ручний виклик = STW + скидання статистики
  • «DisableExplicitGC завжди безпечний» — Direct Buffers: OOM у Native Memory при порожньому Heap
  • «System.gc() лікує витік пам’яті» — маскує проблему; додаток все одно увійде у GC Thrashing

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

  • [[28. Чому не варто викликати System.gc()]]
  • [[4. Що таке Garbage Collection]]
  • [[19. Що станеться при OutOfMemoryError]]
  • [[13. Що таке G1 GC]]
  • [[16. Що таке stop-the-world]]