Что такое сборка мусора?
Сборка мусора — это процесс восстановления заполненной памяти среды выполнения путём уничтожения неиспользуемых объектов.
Проблема утечек памяти в unmanaged-языках
В таких языках, как C и C++, программист отвечает и за создание, и за уничтожение объектов. Иногда программист может забыть уничтожить бесполезный объект, и память не освобождается. В результате приложение страдает от утечек памяти, и в конечном итоге падает с OutOfMemoryError.
В C++ для освобождения памяти используется delete, в C — free(). В managed-языках сборка мусора происходит автоматически.
Почему утечки памяти — серьёзная проблема
Не допускать утечек памяти на первый взгляд несложно — нужно «класть на место всё, что взяли». Но на практике это сильно осложняется:
- хитрой архитектурой
- нелинейным порядком выполнения (из-за исключений)
- человеческим фактором
Помимо утечек важна и противоположная проблема — обращение к уже высвобожденной памяти. Если указатель остался, а память освобождена, обращение по нему ведёт к Segmentation fault.
Автоматическое управление памятью
Java-код транслируется в байт-код, который исполняет JVM. Таким образом, JVM играет ключевую роль и предоставляет важнейшую возможность — автоматическое управление памятью.
Инструмент, освобождающий неиспользуемую память, называется Garbage Collector (GC). У GC две задачи:
- Обнаружение мусора
- Очистка мусора
«Мусор» — это структура данных (объект) в памяти, к которому из программного кода больше невозможно получить доступ.
Подходы к обнаружению мусора
1. Reference Counting (подсчёт ссылок)
Каждый объект имеет счётчик ссылок. Когда ссылка уничтожается — счётчик уменьшается. Если счётчик равен нулю — объект считается мусором.
Используется в: Python и других языках.
Плюсы:
- Простой
- Не требует долгих пауз для сборки
Минусы:
- Плохо сочетается с многопоточностью
- Сложно выявлять циклические зависимости — когда два объекта ссылаются друг на друга, но никто живой на них не ссылается; в итоге утечка
- Влияет на производительность — каждое чтение/запись ссылки требует обновления счётчика
2. Tracing (отслеживание)
Подход вводит понятие GC Root.
Живые объекты — это те, до которых мы можем добраться от корня (GC Root). Всё остальное — мусор. Всё, что доступно с живого объекта — также живое.
Если представить все объекты и ссылки как дерево, нужно пройти от корневых узлов по всем достижимым. До чего не дошли — мусор. Проблема циклических зависимостей решается автоматически.
Типы GC Root в Java 8
- Локальные переменные и параметры методов
- Потоки Java
- Статические переменные
- Ссылки из JNI (Java Native Interface — механизм для запуска кода из unmanaged-языков)
Даже самое простое Java-приложение имеет GC Root:
- Локальные переменные внутри
main - Статические переменные класса
main - Параметры
main(args) - Поток, выполняющий
main
Scope жизни GC Root
Компилятор вычисляет для каждой переменной live range — путь от определения переменной до последнего использования. Если переменная вне своего live range, она перестаёт быть корнем для GC.
Алгоритмы очистки памяти
1. Копирующая сборка
Память делится на области from-space и to-space. Все объекты создаются в from-space. Когда место заканчивается, происходит stop-the-world — все «живые» объекты копируются в to-space, from-space очищается полностью, и области меняются местами.
Stop-the-World
Простые сборщики полностью останавливают выполнение программы во время сборки. Это гарантирует, что новые объекты не выделяются и старые не становятся внезапно недоступными.
Преимущества паузы:
- Проще определять достижимость (граф объектов заморожен)
- Проще перемещать объекты в куче
Для задач с критичными к паузам системами существует инкрементальная сборка — много кратковременных пауз вместо одной длинной.
2. Mark-and-Sweep
Похож на копирующий, но с улучшениями:
- Приложение полностью останавливается
- Проходим по всем объектам и помечаем (mark) «живые»
- Делаем sweep — чистим всё непомеченное
Минус: память становится фрагментированной (остаются «дыры»).
3. Mark-and-Sweep Compact
Улучшение Mark-and-Sweep:
- Ищем «мёртвые» объекты и помечаем их для переноса параллельно работе приложения
- Останавливаем приложение для очистки
- Делаем compact — дефрагментируем память, объекты сдвигаются к более близким адресам
Плюсы:
- Нет фрагментации памяти
- Эффективно при большом количестве «живых» объектов
Минусы:
- Плохо работает при большом количестве «мёртвых» объектов
- Compact — дорогостоящая операция
Сборка мусора на поколениях
«Слабая гипотеза о поколениях»
Анализ работающих систем выявил две закономерности:
- Большинство объектов живут либо очень долго, либо очень недолго. Долгоживущих очень мало.
- Между «старыми» и «новыми» объектами мало связей.
Отсюда — идея разделения объектов на поколения:
- Young Generation (молодое поколение)
- Old Generation (старое поколение)
Сборки тоже делятся:
- Minor GC — затрагивает только младшее поколение, выполняется часто
- Full GC — затрагивает оба поколения, выполняется редко
Характеристики сборщиков мусора
При оценке GC учитываются три фактора:
- Максимальная задержка (STW pause) — максимальное время, на которое сборщик приостанавливает выполнение программы.
- Пропускная способность — отношение общего времени работы к времени простоя из-за сборки.
- Потребляемые ресурсы — объём CPU и дополнительной памяти, потребляемых сборщиком.
Достичь идеала по всем трём показателям одновременно — невозможно. При выборе реализации сосредотачиваются на двух конкретных характеристиках.
Структура памяти JVM
Поколения в Heap
- Young Generation — здесь создаются новые объекты. Делится на три части:
- Eden — область, где изначально создаются объекты через
new Object() - Survivor S0 / From Space — куда попадают пережившие «изгнание из Эдема»
- Survivor S1 / To Space — последнее место перед переходом в Tenured
- Eden — область, где изначально создаются объекты через
- Old Generation (Tenured) — здесь хранятся давно живущие объекты
Метаданные
До Java 8 существовал PermGen — раздел для метаданных классов, интернированных строк и т.д. Был сложен в настройке размера, часто давал OutOfMemoryError.
Начиная с Java 8, эта область убрана. Информация переехала:
- Интернированные строки — в heap
- Остальные метаданные — в область Metaspace в native memory
Максимальный размер Metaspace по умолчанию ограничен только объёмом нативной памяти.
Типы сборок
Minor сборка
- Очищает только Young Generation (Eden + Survivor)
- «Живые» молодые объекты, пережившие достаточное число циклов, перемещаются в Tenured
- Остальные «живые» отправляются в пустую Survivor-область
- Eden и очищенная Survivor могут быть переиспользованы
- Происходит часто, быстро, уничтожает много мусора
Major сборка
- Очищает Old Generation
- Тесно связана с minor, поэтому обычно рассматривается в составе full
Full сборка
- Очищается и Young, и Old Generation
- Запускается, когда minor не может перенести объект (недостаточно места)
- Происходит редко, но занимает много времени
Сборщики, работающие с поколениями, называются Generational Garbage Collection.
Реализации GC в HotSpot VM
| Сборщик | Версия Java | JVM-аргумент | Назначение |
|---|---|---|---|
| Serial GC | Все | -XX:+UseSerialGC | Небольшие приложения, однопоточные среды |
| Parallel GC | По умолчанию | -XX:+UseParallelGC | Средние/большие данные, многопоточное оборудование |
| CMS GC | До Java 9 | -XX:+UseConcMarkSweepGC | Веб-приложения, минимальные паузы |
| G1 GC | Java 9+ default | -XX:+UseG1GC | Крупный размер кучи (>4 ГБ) |
| Epsilon GC | Java 11+ | -XX:+UseEpsilonGC | Эксперименты, сверхнизкая задержка |
| Shenandoah GC | Java 15+ | -XX:+UseShenandoahGC | Параллельная сборка, минимальные паузы |
| ZGC | Java 15+ | -XX:+UseZGC | Низкая задержка (<10 мс), терабайтные кучи |
Краткие характеристики
- Serial GC — все события сборки в одном потоке, всегда вызывает stop-the-world.
- Parallel GC — несколько потоков для minor GC, один для major. Также вызывает stop-the-world. Подходит для пакетных задач.
- CMS — Concurrent Mark and Sweep. Работает одновременно с приложением, минимизируя паузы. Потребляет больше CPU. В CMS не выполняется уплотнение.
- G1 — Garbage First. Разбивает кучу на области (1–32 МБ), сканирует их параллельно. Имеет дополнительные области: Humongous (для объектов >50% размера кучи) и Available (неиспользуемое пространство).
- Epsilon — не реализует никакого реального механизма сборки. Как только куча исчерпана — JVM завершает работу. Используется для приложений, чувствительных к сверхвысокой задержке, или для тестирования.
- Shenandoah — большая часть цикла выполняется параллельно с приложением, объекты перемещаются на лету. Сильнее нагружает процессор.
- ZGC — паузы менее 10 мс даже на терабайтных кучах. Только для больших серверных решений.
Заключение
Отдавайте предпочтение настройкам по умолчанию. Если у вас небольшое автономное Java-приложение, скорее всего, настраивать GC не нужно. Это не то, на что должен ежедневно обращать внимание разработчик.