Сборщики мусора в JVM: как выбрать между G1, CMS и ZGC
Зачем вообще разбираться в GC
На собесе вопрос про GC редко звучит как «расскажи алгоритм пометки». Чаще спрашивают: «У вас растут паузы под нагрузкой — что будете делать?» или «Почему приложение зависает на 2 секунды раз в 10 минут?». Без понимания, как устроена куча и какие сборщики за что отвечают, ответить по существу не получится.
Куча не монолитна
Heap делится на поколения, потому что большинство объектов живёт очень недолго — это эмпирический факт, названный weak generational hypothesis.
- Young Generation — Eden + два Survivor-региона. Сюда попадают новые объекты, сборка (minor GC) происходит часто, но быстро.
- Old Generation — объекты, пережившие несколько minor GC, попадают сюда через promotion. Сборка (major/full GC) реже, но дороже.
- Metaspace — метаданные классов, вынесены из хипа начиная с Java 8.
Stop-The-World — главный враг латентности
Любой GC периодически должен остановить все потоки приложения, чтобы безопасно пройтись по графу объектов — это STW-пауза. Вопрос собеса «какой GC выбрать» на самом деле означает «какой компромисс между throughput и паузами вам нужен».
Serial и Parallel GC
Serial — однопоточный, подходит для маленьких приложений или контейнеров с 1 CPU. Parallel GC (по умолчанию до Java 8) использует несколько потоков для сборки, но паузы всё ещё длинные и растут с размером кучи — это классический throughput-collector, не для latency-sensitive сервисов.
CMS — устаревший, но спрашивают историю
Concurrent Mark Sweep пытался делать mark-и-sweep конкурентно с работой приложения, снижая паузы. Проблема — фрагментация памяти, отсутствие compaction, и как следствие — риск promotion failure и полного STW Full GC как fallback. CMS deprecated с Java 9, удалён в Java 14.
G1 GC — дефолт с Java 9
G1 (Garbage First) делит хип не на два больших поколения, а на множество равных регионов (обычно 1–32 МБ). Сборщик приоритизирует регионы с наибольшим количеством мусора — отсюда название. Ключевая идея — предсказуемость: вы задаёте целевую паузу через -XX:MaxGCPauseMillis, и G1 старается её выдержать, инкрементально собирая часть регионов за раз, а не всю Old Gen целиком.
-XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:InitiatingHeapOccupancyPercent=45
ZGC и Shenandoah — паузы в единицы миллисекунд
Оба сборщика делают почти всю работу конкурентно, включая compaction, за счёт цветных указателей (ZGC) или барьеров чтения/записи (Shenandoah). Паузы не зависят от размера кучи — можно держать хип в сотни гигабайт с паузами <10мс. Плата — дополнительный CPU overhead на барьеры и более сложная диагностика.
Promotion failure и Full GC как symptom
Если Old Gen заполняется быстрее, чем GC успевает освобождать место, JVM делает Full GC — самую дорогую операцию, останавливающую всё приложение на секунды. Типичные причины: memory leak, слишком маленький хип, объекты-«долгожители» из-за неудачного кэширования (например, статический список, который никто не чистит).
Как отвечать под давлением
Хороший ответ на «у нас паузы GC» строится так: сначала спросить, какой сборщик используется и какая версия Java, затем — смотреть GC-логи (-Xlog:gc*), искать паттерн: частые minor GC — проблема с allocation rate, редкие Full GC — проблема с утечкой или недостатком памяти в Old Gen. Дальше — конкретные тюнинги: увеличить Young Gen, поднять heap, или сменить сборщик на G1/ZGC в зависимости от требований к паузам.