ГлавнаяБлог › Сборщики мусора в JVM: как выбрать между G1, CMS и ZGC

Сборщики мусора в 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.
Это как склад временного хранения (Eden) рядом со стеллажом для товаров, которые залежались (Old Gen). Большая часть коробок разбирается сразу на входе, и только редкие товары едут на долгосрочное хранение — там уборка сложнее и дольше.

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 на барьеры и более сложная диагностика.

Частая ошибка на собесе — сказать «G1 не делает Stop-The-World». Это не так: G1 конкурентен частично, фазы young collection и часть mixed collection всё равно требуют полной остановки приложения, просто они короче и предсказуемее, чем Full GC у Parallel/CMS.

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 в зависимости от требований к паузам.

Чем G1 принципиально отличается от Parallel GC?
G1 делит хип на регионы и собирает мусор инкрементально, ориентируясь на целевую паузу, тогда как Parallel GC чистит всё поколение целиком, и пауза растёт линейно с размером хипа.
Почему ZGC почти не делает длинных пауз даже на больших хипах?
Потому что compaction и mark-фазы выполняются конкурентно с работающим приложением с помощью цветных указателей и барьеров на чтение, а не в единой STW-фазе.

Ещё разборы