Лекция 2

01.01.0001

Что такое сборка мусора?

Сборка мусора — это процесс восстановления заполненной памяти среды выполнения путём уничтожения неиспользуемых объектов.

Проблема утечек памяти в unmanaged-языках

В таких языках, как C и C++, программист отвечает и за создание, и за уничтожение объектов. Иногда программист может забыть уничтожить бесполезный объект, и память не освобождается. В результате приложение страдает от утечек памяти, и в конечном итоге падает с OutOfMemoryError.

В C++ для освобождения памяти используется delete, в C — free(). В managed-языках сборка мусора происходит автоматически.

Почему утечки памяти — серьёзная проблема

Не допускать утечек памяти на первый взгляд несложно — нужно «класть на место всё, что взяли». Но на практике это сильно осложняется:

  • хитрой архитектурой
  • нелинейным порядком выполнения (из-за исключений)
  • человеческим фактором

Помимо утечек важна и противоположная проблема — обращение к уже высвобожденной памяти. Если указатель остался, а память освобождена, обращение по нему ведёт к Segmentation fault.

Автоматическое управление памятью

Java-код транслируется в байт-код, который исполняет JVM. Таким образом, JVM играет ключевую роль и предоставляет важнейшую возможность — автоматическое управление памятью.

Инструмент, освобождающий неиспользуемую память, называется Garbage Collector (GC). У GC две задачи:

  1. Обнаружение мусора
  2. Очистка мусора

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

Подходы к обнаружению мусора

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

Похож на копирующий, но с улучшениями:

  1. Приложение полностью останавливается
  2. Проходим по всем объектам и помечаем (mark) «живые»
  3. Делаем sweep — чистим всё непомеченное

Минус: память становится фрагментированной (остаются «дыры»).

3. Mark-and-Sweep Compact

Улучшение Mark-and-Sweep:

  1. Ищем «мёртвые» объекты и помечаем их для переноса параллельно работе приложения
  2. Останавливаем приложение для очистки
  3. Делаем compact — дефрагментируем память, объекты сдвигаются к более близким адресам

Плюсы:

  • Нет фрагментации памяти
  • Эффективно при большом количестве «живых» объектов

Минусы:

  • Плохо работает при большом количестве «мёртвых» объектов
  • Compact — дорогостоящая операция

Сборка мусора на поколениях

«Слабая гипотеза о поколениях»

Анализ работающих систем выявил две закономерности:

  1. Большинство объектов живут либо очень долго, либо очень недолго. Долгоживущих очень мало.
  2. Между «старыми» и «новыми» объектами мало связей.

Отсюда — идея разделения объектов на поколения:

  • Young Generation (молодое поколение)
  • Old Generation (старое поколение)

Сборки тоже делятся:

  • Minor GC — затрагивает только младшее поколение, выполняется часто
  • Full GC — затрагивает оба поколения, выполняется редко

Характеристики сборщиков мусора

При оценке GC учитываются три фактора:

  1. Максимальная задержка (STW pause) — максимальное время, на которое сборщик приостанавливает выполнение программы.
  2. Пропускная способность — отношение общего времени работы к времени простоя из-за сборки.
  3. Потребляемые ресурсы — объём CPU и дополнительной памяти, потребляемых сборщиком.

Достичь идеала по всем трём показателям одновременно — невозможно. При выборе реализации сосредотачиваются на двух конкретных характеристиках.

Структура памяти JVM

Поколения в Heap

  • Young Generation — здесь создаются новые объекты. Делится на три части:
    • Eden — область, где изначально создаются объекты через new Object()
    • Survivor S0 / From Space — куда попадают пережившие «изгнание из Эдема»
    • Survivor S1 / To Space — последнее место перед переходом в Tenured
  • 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

СборщикВерсия JavaJVM-аргументНазначение
Serial GCВсе-XX:+UseSerialGCНебольшие приложения, однопоточные среды
Parallel GCПо умолчанию-XX:+UseParallelGCСредние/большие данные, многопоточное оборудование
CMS GCДо Java 9-XX:+UseConcMarkSweepGCВеб-приложения, минимальные паузы
G1 GCJava 9+ default-XX:+UseG1GCКрупный размер кучи (>4 ГБ)
Epsilon GCJava 11+-XX:+UseEpsilonGCЭксперименты, сверхнизкая задержка
Shenandoah GCJava 15+-XX:+UseShenandoahGCПараллельная сборка, минимальные паузы
ZGCJava 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 не нужно. Это не то, на что должен ежедневно обращать внимание разработчик.