Вопрос 4 · Раздел 3

Что такое Garbage Collection?

GC основан на двух наблюдениях:

Версии по языкам: English Russian Ukrainian

🟢 Junior Level

Garbage Collection (GC) — автоматическая «уборка мусора» в памяти Java.

«Мусор» — объекты, которые больше не используются программой (недостижимы из GC Roots). GC находит их и освобождает память. Без GC разработчику пришлось бы вручную освобождать память (как в C/C++), что чревато утечками и dangling pointer.

Простая аналогия: Представьте, что вы работаете за столом. Со временем стол заваливается бумажками. GC — это уборщик, который приходит и выбрасывает ненужные бумажки, освобождая место.

Зачем нужен:

  • В C/C++ программист сам удаляет память → часто забывает → утечки
  • В Java GC сам находит и удаляет ненужные объекты → нет утечек (почти)

Как GC понимает, что объект ненужный:

  • Если на объект нет ссылок → он мусор
  • Если до объекта нельзя добраться из кода → он мусор

Пример:

public void example() {
    String s1 = "hello";   // Объект "hello" используется
    s1 = "world";          // "hello" больше не нужен → GC удалит!
}

Основные GC в Java:

  • G1 GC — по умолчанию (Java 9+)
  • ZGC — минимальные паузы (< 1 мс), но throughput на 5-10% ниже, чем у G1.
  • Parallel GC — для пакетной обработки

🟡 Middle Level

Weak Generational Hypothesis

Термины:

  • STW (Stop-The-World) — полная остановка всех потоков приложения на время сборки мусора.
  • SATB (Snapshot-At-The-Beginning) — алгоритм concurrent marking: запоминает объекты, которые были достижимы на момент начала GC, и отслеживает только их удаление из графа достижимости.
  • Card Table — битовая карта, где каждый бит показывает, есть ли ссылки из Old Gen в Young Gen. Позволяет GC не сканировать весь Old Gen.

GC основан на двух наблюдениях:

  1. Для большинства приложений верно: большинство объектов умирают молодыми — временные переменные, буферы. Но для workload-ов с большими долгоживущими кэшами это наблюдение не работает.
  2. Объекты, пережившие несколько сборок, живут долго — синглтоны, кэши

Следствие: Разделить Heap на поколения:

  • Young Generation — частые, быстрые сборки
  • Old Generation — редкие, медленные сборки

Основные алгоритмы

Алгоритм Как работает Где применяется
Copying Копирует живые в новую область, старую очищает Young Gen
Mark-Sweep Помечает живых, удаляет мёртвых Old Gen
Mark-Compact Помечает + сдвигает живых (убирает фрагментацию) Full GC

Фазы GC

1. Mark (Разметка)
   → Обход графа объектов от GC Roots
   → Помечаются все достижимые объекты

2. Sweep (Очистка)
   → Удаление непомеченных объектов

3. Compact (Уплотнение) — не всегда
   → Сдвиг живых объектов для устранения фрагментации

Stop-The-World (STW)

Во время GC приложение останавливается!
  → Все потоки ждут
  → Приложение "замирает"
  → Длительность: от 1 мс (ZGC) до нескольких секунд (Full GC)

Типы GC

G1 GC (по умолчанию):

  • Делит Heap на регионы
  • Целевая пауза: -XX:MaxGCPauseMillis=200
  • Хороший баланс latency/throughput

ZGC (Java 21+):

  • Паузы < 1 мс (не зависят от размера Heap)
  • До 16 ТБ памяти
  • -5-10% throughput vs G1

Parallel GC:

  • Максимальный throughput
  • Длинные паузы (500 мс+)
  • Для Big Data, batch processing

Типичные ошибки

  1. System.gc() в коде
    // ❌ Вызов GC вручную — антипаттерн
    System.gc();  // Останавливает всё приложение!
    
  2. Слишком маленький Heap
    -Xmx256m для Spring Boot → постоянный GC
    → Приложение тормозит
    → Решение: -Xmx1g минимум
    
  3. Утечки памяти
    // ❌ Статическая коллекция растёт бесконечно
    static List<Data> cache = new ArrayList<>();
    cache.add(data);  // GC не может удалить!
    

Когда НЕ использовать каждый GC

  • Serial GC — НЕ для серверов (один поток, долгие паузы)
  • Parallel GC — НЕ когда важны паузы (SLA < 100 мс)
  • G1 GC — НЕ для SLA < 10 мс (берите ZGC), НЕ для batch без latency-требований (берите Parallel)
  • ZGC — НЕ на Java < 15 (нет стабильной версии), НЕ когда важен максимальный throughput

🔴 Senior Level

GC Trade-offs (Треугольник GC)

         Latency
          / \
         /   \
  Throughput ── Footprint

Нельзя оптимизировать все три одновременно!

G1: баланс latency + throughput
ZGC: минимальная latency (ценой throughput: ZGC жертвует 5-15% throughput ради пауз < 1 мс)
Parallel: максимальный throughput (ценой latency)

Safepoints и STW механика

Потоки не могут остановиться мгновенно!
→ Должны дойти до Safepoint (точка остановки)

Где расставлены Safepoints:
  - Перед выходом из метода
  - В концах циклов
  - Перед вызовами методов

Механизм: Safepoint Poll
  → JVM помечает страницу как недоступную
  → Поток читает → SIGSEGV → переходит в ожидание

TTSP (Time To Safepoint):
  → Время от команды "стоп" до последнего потока
  → Counted Loops могут задержать на секунды!
  → -XX:+UseCountedLoopSafepoints

Write Barriers и Card Tables

Проблема: Minor GC не хочет сканировать весь Old Gen

Решение: Card Table
  → Old Gen разбит на карточки по 512 байт
  → При записи oldObj.field = youngObj
    → Write Barrier помечает карточку как Dirty
  → Minor GC сканирует только Dirty карточки

Write Barrier (вставляется JIT):
  card_table[address >> 9] = DIRTY
  → ~1-2 ns overhead на каждую запись ссылки

Современные GC (Java 17-21+)

G1 GC:

  • Регионы 1-32 МБ (динамические)
  • Remembered Sets (RSet) для межрегиональных ссылок
  • SATB (Snapshot-At-The-Beginning) для конкурентной разметки
  • Mixed GC: Young + самые “грязные” регионы Old

ZGC:

  • Colored Pointers (метаданные в указателях)
  • Load Barriers (проверка при чтении)
  • Multi-mapping виртуальной памяти
  • Generational ZGC (Java 21+) — разделение на поколения

Shenandoah:

  • Brooks Pointers → Load Reference Barriers
  • Конкурентное уплотнение (Evacuation)
  • Альтернатива ZGC для OpenJDK

Epsilon GC:

  • “No-op GC” — только аллокация
  • Для тестов производительности
  • Для микросервисов с коротким жизненным циклом

Allocation Stall

Опаснейший сценарий для конкурентных GC:

Приложение создаёт мусор быстрее, чем GC собирает
→ Память заканчивается
→ Поток приложения ЗАМИРАЕТ и ждёт GC
→ Пауза: сотни миллисекунд или секунды

Решение:
  → Увеличить Heap
  → Увеличить ConcGCThreads
  → Уменьшить Allocation Rate

Production Experience

Реальный сценарий #1: Full GC каждую минуту

  • Spring Boot, -Xmx512m → слишком мало
  • Full GC каждые 60 секунд, пауза 2 секунды
  • SLA нарушен
  • Решение: -Xmx2g → Minor GC каждые 5 минут, пауза 50 мс

Реальный сценарий #2: ZGC спас HFT систему

  • High-frequency trading: SLA < 10 мс
  • G1 GC: паузы 50-200 мс → пропуски сделок
  • ZGC: паузы < 1 мс → SLA выполнен
  • Цена: -8% throughput (принято)

Best Practices

  1. Не вызывайте System.gc() вручную
  2. G1 GC — хороший выбор по умолчанию
  3. ZGC (Java 21+) — если SLA < 10 мс
  4. Parallel GC — для batch processing
  5. Мониторьте GC логи через -Xlog:gc*
  6. Safepoint логи для диагностики “зависаний”
  7. Избегайте Allocation Stall → достаточный Heap
  8. Лучший мусор — тот, что не создан → оптимизируйте аллокации

Резюме для Senior

  • GC = система управления ресурсами, не просто “очистка”
  • Треугольник: Latency ↔ Throughput ↔ Footprint
  • Safepoints = механизм STW, TTSP может быть проблемой
  • Write Barriers = Card Tables для оптимизации Minor GC
  • ZGC = Colored Pointers + Load Barriers → < 1 мс паузы
  • Allocation Stall = катастрофа для latency
  • Выбор GC = архитектурное решение на основе SLA
  • Лучший мусор = не созданный → оптимизируйте код до настройки GC

🎯 Шпаргалка для интервью

Обязательно знать:

  • GC автоматически удаляет недостижимые из GC Roots объекты — разработчику не нужно управлять памятью вручную
  • Weak Generational Hypothesis: большинство объектов умирают молодыми → Heap делится на Young/Old Generation
  • Основные алгоритмы: Copying (Young Gen), Mark-Sweep/Mark-Compact (Old Gen)
  • G1 GC — по умолчанию с Java 9, баланс latency/throughput; ZGC — паузы < 1 мс, но throughput на 5-15% ниже
  • Stop-The-World: все потоки останавливаются на время GC; ZGC < 1 мс, G1 20-200 мс, Full GC — секунды
  • Safepoints — точки в коде, где потоки могут остановиться; TTSP может быть дольше самого GC
  • Card Table + Write Barriers оптимизируют Minor GC, не сканируя весь Old Gen
  • Лучший мусор — тот, что не создан; оптимизируйте аллокации до настройки GC

Частые уточняющие вопросы:

  • Почему нельзя оптимизировать latency, throughput и footprint одновременно? — Треугольник GC: улучшение одного ухудшает другое (например, ZGC жертвует throughput ради пауз < 1 мс)
  • Что такое Allocation Stall? — Приложение создаёт мусор быстрее, чем concurrent GC успевает → поток замирает и ждёт
  • Почему ZGC медленнее G1 по throughput? — Load Barriers при каждом чтении ссылки добавляют 5-15% CPU overhead
  • Когда вызывать System.gc()? — Никогда в production. Только в тестах/бенчмарках.

Красные флаги (НЕ говорить):

  • «Я вызываю System.gc() после больших операций для оптимизации» — это антипаттерн, GC умнее
  • «GC использует Reference Counting» — Java использует Reachability Analysis, а не подсчёт ссылок
  • «ZGC всегда лучше G1» — ZGC жертвует throughput; для batch processing G1/Parallel лучше
  • «GC можно полностью отключить» — Epsilon GC не собирает мусор вообще, но это не production-решение для долгоживущих приложений

Связанные темы:

  • [[5. Когда объект становится кандидатом на удаление GC]]
  • [[8. Что такое поколения в GC (young, old, metaspace)]]
  • [[12. Какие алгоритмы GC существуют]]
  • [[13. Что такое G1 GC]]
  • [[16. Что такое stop-the-world]]