Что такое Garbage Collection?
GC основан на двух наблюдениях:
🟢 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 основан на двух наблюдениях:
- Для большинства приложений верно: большинство объектов умирают молодыми — временные переменные, буферы. Но для workload-ов с большими долгоживущими кэшами это наблюдение не работает.
- Объекты, пережившие несколько сборок, живут долго — синглтоны, кэши
Следствие: Разделить 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
Типичные ошибки
- System.gc() в коде
// ❌ Вызов GC вручную — антипаттерн System.gc(); // Останавливает всё приложение! - Слишком маленький Heap
-Xmx256m для Spring Boot → постоянный GC → Приложение тормозит → Решение: -Xmx1g минимум - Утечки памяти
// ❌ Статическая коллекция растёт бесконечно 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
- Не вызывайте System.gc() вручную
- G1 GC — хороший выбор по умолчанию
- ZGC (Java 21+) — если SLA < 10 мс
- Parallel GC — для batch processing
- Мониторьте GC логи через
-Xlog:gc* - Safepoint логи для диагностики “зависаний”
- Избегайте Allocation Stall → достаточный Heap
- Лучший мусор — тот, что не создан → оптимизируйте аллокации
Резюме для 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]]