В языках вроде 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 хорошо подходит большинству приложений, и тюнинг чаще вредит, чем помогает: подкрученный «на глаз» флаг легко делает хуже.
Разумный порядок действий:
- Запустите приложение как есть (G1 по умолчанию).
- Задайте
-Xmxпод реальную доступную память — это самый влиятельный параметр. - Если есть проблема с производительностью — сначала измерьте: включите
-Xlog:gcи посмотрите, действительно ли дело в сборке мусора, а не в коде или базе данных. - Только если паузы реально мешают — пробуйте другой сборщик (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 и современный синтаксис — компактные объекты и современные возможности языка.