← назад к разделу

В языках вроде C память приходится освобождать вручную: забыл вызвать free — утечка, освободил дважды — крах. В Java этим занимается сборщик мусора (garbage collector, GC): он сам находит объекты, которые больше никому не нужны, и возвращает их память. Разберёмся, как это устроено и какими настройками можно на это влиять.

Зачем нужна автоматическая сборка мусора

Когда вы пишете new Order(), объект создаётся в области памяти под названием куча (heap). Пока на объект есть хоть одна живая ссылка — он нужен. Как только ссылок не осталось (например, переменная вышла из области видимости), объект становится мусором — до него уже не добраться из работающего кода.

Сборщик мусора периодически проходит по живым объектам, начиная от «корней» (локальные переменные на стеке, статические поля), и всё, до чего не дотянулся, считает мусором и освобождает.

Короткая формула: жив тот объект, до которого есть путь по ссылкам от корней; остальное — мусор.

Что это даёт на практике:

  • не нужно вручную освобождать память — меньше класса ошибок (утечки, двойное освобождение, обращение к освобождённой памяти);
  • можно сосредоточиться на логике, а не на учёте, кто и когда удалит объект.

Цена — в том, что момент сборки выбирает не программист, а runtime, и иногда сборка приостанавливает приложение. Об этом ниже.

Куча и поколения

Большинство объектов живут очень недолго: создались внутри метода, отработали и стали мусором. На этом наблюдении построена поколенческая (generational) модель кучи. Куча делится на две основные части:

  • Young Generation (молодое поколение) — сюда попадают все новые объекты. Заполняется быстро.
  • Old Generation (старое поколение) — сюда «переезжают» объекты, пережившие несколько сборок в молодом поколении, то есть те, что живут долго.

Соответственно и сборки бывают двух видов:

  • minor GC — сборка только в молодом поколении. Случается часто, проходит быстро, потому что большинство молодых объектов уже мусор и проверять надо мало живых.
  • major GC (или full GC) — затрагивает старое поколение. Случается реже, но идёт дольше.

Паузы stop-the-world

Чтобы безопасно пересчитать ссылки и переместить объекты, сборщику иногда нужно на мгновение остановить весь прикладной код. Это и есть пауза stop-the-world: приложение замирает, ничего не отвечает, пока GC делает свою работу, потом продолжает.

Простыми словами: представьте библиотекаря, который наводит порядок на полках. Пока читатели ходят и переставляют книги, точно посчитать ничего нельзя. Поэтому на короткое время вход закрывают, наводят порядок и снова открывают. Чем больше книг (объектов) и чем дольше уборка — тем заметнее задержка для читателей.

Вся борьба разных сборщиков мусора — это борьба за то, чтобы такие паузы были как можно короче и реже, не слишком жертвуя общей производительностью.

Основные сборщики и когда какой

В Java 21 в одной JVM (HotSpot) встроено несколько сборщиков. Выбор — это всегда компромисс между двумя величинами:

  • пропускная способность (throughput) — какую долю времени машина тратит на полезную работу, а не на сборку;
  • длина пауз (latency) — насколько коротки остановки stop-the-world.
СборщикСильная сторонаКогда уместен
Serialминимум накладных расходовмаленькие приложения, мало памяти, одно ядро
Parallelмаксимальная пропускная способностьпакетная обработка, где паузы не критичны
G1баланс пауз и пропускной способностипо умолчанию, подходит большинству сервисов
ZGC / Shenandoahочень короткие паузыбольшие кучи, требования к стабильно низким задержкам

Serial GC — самый простой. Делает всю работу в одном потоке и всегда с паузой stop-the-world. Хорош там, где данных немного, а лишние потоки и память ни к чему.

Parallel GC (его ещё называют throughput collector) — собирает мусор в несколько потоков. Паузы есть, но в сумме за время работы тратит на сборку меньше всего — то есть отдаёт максимум процессорного времени самому приложению. Подходит для задач, где важна общая скорость обработки, а короткие подвисания терпимы.

G1 GC (Garbage-First) — сборщик по умолчанию начиная с Java 9. Делит кучу на множество небольших регионов и собирает в первую очередь те, где больше всего мусора (отсюда название). Старается уложить паузы в заданный бюджет. Это разумный баланс, который подходит большинству серверных приложений без всякой настройки.

ZGC и Shenandoah — сборщики с упором на минимальные паузы. Большую часть работы они делают параллельно с приложением, почти не останавливая его, поэтому паузы остаются очень короткими даже на кучах в десятки и сотни гигабайт. Цена — чуть больше расхода процессора и памяти. Нужны там, где недопустимы заметные подвисания (например, сервис с жёсткими требованиями к времени ответа).

Короткая формула: по умолчанию — G1; нужна максимальная пропускная способность — Parallel; нужны стабильно крошечные паузы на большой куче — ZGC.

Ключевые параметры запуска

Поведение сборщика и размер кучи задаются флагами при запуске java.

Размер кучи:

# начальный размер кучи 512 МБ, максимальный 2 ГБ
java -Xms512m -Xmx2g -jar app.jar
  • -Xms — начальный размер кучи;
  • -Xmx — максимальный размер кучи.

Частый приём для серверов — задать -Xms равным -Xmx. Тогда JVM сразу резервирует всю кучу и не тратит время на её постепенное расширение под нагрузкой.

Выбор сборщика:

java -XX:+UseG1GC   -jar app.jar   # G1 (и так по умолчанию)
java -XX:+UseZGC    -jar app.jar   # ZGC, короткие паузы
java -XX:+UseParallelGC -jar app.jar   # Parallel, максимум пропускной способности

Подсказка цели по паузам (для G1):

# просим G1 стараться удерживать паузы в районе 100 мс
java -XX:MaxGCPauseMillis=100 -jar app.jar

-XX:MaxGCPauseMillis — это цель, а не гарантия. JVM будет стараться её соблюдать, балансируя размеры регионов и частоту сборок, но обещать точное значение не может.

Логи сборки мусора:

# писать события GC в консоль
java -Xlog:gc -jar app.jar

# подробнее и с записью в файл
java -Xlog:gc*:file=gc.log:time,uptime -jar app.jar

-Xlog:gc включает журнал событий сборки: видно, когда и какой сборщик отработал, сколько длилась пауза, сколько памяти освободилось. Это первое, на что стоит смотреть, если есть подозрение, что приложение подвисает из-за GC.

Как выбрать и не переусердствовать

Главный совет — начинать с настроек по умолчанию. G1 в Java 21 хорошо подходит большинству приложений, и тюнинг чаще вредит, чем помогает: подкрученный «на глаз» флаг легко делает хуже.

Разумный порядок действий:

  1. Запустите приложение как есть (G1 по умолчанию).
  2. Задайте -Xmx под реальную доступную память — это самый влиятельный параметр.
  3. Если есть проблема с производительностью — сначала измерьте: включите -Xlog:gc и посмотрите, действительно ли дело в сборке мусора, а не в коде или базе данных.
  4. Только если паузы реально мешают — пробуйте другой сборщик (ZGC для коротких пауз) или цель -XX:MaxGCPauseMillis, по одному изменению за раз и с замером до/после.

Короткая формула: не настраивайте GC, пока не доказали логами, что проблема именно в нём.

Коротко

  • Сборщик мусора сам освобождает память объектов, до которых больше нет пути по ссылкам от корней, — ручной free не нужен.
  • Куча делится на молодое и старое поколения; minor GC чистит молодое (часто и быстро), major GC — старое (реже и дольше).
  • Пауза stop-the-world — короткая остановка приложения на время сборки; задача сборщиков — делать паузы короче и реже.
  • Serial — для маленьких приложений, Parallel — для максимальной пропускной способности, G1 — баланс и значение по умолчанию, ZGC и Shenandoah — крошечные паузы на больших кучах.
  • -Xms/-Xmx задают начальный и максимальный размер кучи; -XX:+UseG1GC/-XX:+UseZGC выбирают сборщик; -XX:MaxGCPauseMillis — цель по паузам; -Xlog:gc включает логи.
  • Начинайте с настроек по умолчанию и не тюньте GC, пока логами не доказали, что узкое место именно в нём.

Что почитать дальше

  • Инструменты разработчика Java — как запускать, собирать и профилировать приложение.
  • Коллекции — где живут объекты, которые потом собирает GC.
  • Record и современный синтаксис — компактные объекты и современные возможности языка.