(GC) в Java



Garbage Collector

1. Основные области памяти JVM
прощённо память JVM делится на несколько областей:

  • Heap (Куча)
  • Stack (Стек)
  • Metaspace
  • Code Cache
  • Native Memory
Для начинающего разработчика особенно важно понимать различие между Heap и Stack.
1.2. Heap (Куча)
Heap --- это область памяти, где хранятся объекты.

Пример:
User user = new User();
  • Объект User создаётся в Heap.
  • Переменная user хранит ссылку на этот объект.

Особенности Heap:
  • Общий для всех потоков.
  • Управляется Garbage Collector.
  • Объекты живут здесь до тех пор, пока они достижимы.
1.3. Stack (Стек)
Stack используется для выполнения методов.
В нём хранятся:
  • Локальные переменные
  • Параметры методов
  • Ссылки на объекты
  • Информация о вызовах методов

Пример:
public void process() {
    User user = new User();
}
Во время выполнения метода:
  • Ссылка user находится в Stack.
  • Сам объект User находится в Heap.
После завершения метода стековый фрейм уничтожается, ссылка исчезает, и объект может стать недостижимым.
1.4. Metaspace
Metaspace хранит:
  • Информацию о классах
  • Структуру методов
  • Описание полей
  • Метаданные
При загрузке класса его описание помещается в Metaspace.
2. Основы работы Garbage Collector
2.1. Почему вообще нужен сборщик мусора
Память --- это ограниченный ресурс.
Даже если у сервера десятки гигабайт оперативной памяти, она всё равно конечна. Если программа будет создавать объекты и никогда их не освобождать, рано или поздно произойдёт ошибка:
java.lang.OutOfMemoryError
В ранних языках программирования управление памятью было полностью ответственностью разработчика. Нужно было:
  • вручную выделять память,
  • вручную освобождать её,
  • следить, чтобы не возникли утечки,
  • не обращаться к уже освобождённой памяти.

Это приводило к:
  • сложному и хрупкому коду,
  • трудноуловимым ошибкам,
  • уязвимостям безопасности.

Java изначально выбрала другой подход:
Управление памятью должно быть автоматизировано.
Так появился Garbage Collector (GC) --- механизм внутри JVM, который автоматически освобождает память, занятую ненужными объектами.

2.2. Что делает GC
Garbage Collector выполняет две основные задачи:
  1. Находит объекты, которые больше не используются.
  2. Освобождает память, которую они занимали в Heap.

Важно:
GC работает только с объектами в Heap. Он не управляет стеком и не удаляет локальные переменные.
2.3. Понятие достижимости (Reachability)
Ключевая идея GC --- это достижимость объекта.

Объект считается живым (live), если к нему можно добраться по цепочке ссылок из специальных корневых точек.

Если к объекту нельзя добраться --- он считается мусором (garbage).
2.4. Корневые точки (GC Roots)
GC начинает анализ с набора корневых объектов --- GC Roots.

К ним относятся:
  • Локальные переменные в Stack
  • Активные потоки (Threads)
  • Статические поля классов
  • JNI-ссылки
Если объект достижим из GC Roots --- он считается живым.
2.5. Пример достижимости
public void example() {
    User user = new User();
}
Во время выполнения метода:
  • user находится в Stack
  • объект User находится в Heap
  • объект достижим из GC Root (через стек)

После выхода из метода:
  • стековый фрейм уничтожается,
  • ссылка user исчезает,
  • если других ссылок нет --- объект становится недостижимым.
В этот момент объект становится кандидатом на удаление.
2.6. GC не работает «построчно»
GC не удаляет объект сразу, как только ссылка стала null или метод завершился.

Сборка мусора происходит:
  • периодически,
  • при нехватке памяти,
  • в соответствии с внутренними алгоритмами JVM.
Момент удаления объекта недетерминирован.
2.7. Упрощённый алгоритм работы GC
На базовом уровне процесс выглядит так:

  1. GC находит все GC Roots.
  2. Обходит граф объектов, начиная с корней.
  3. Помечает все достижимые объекты.
  4. Всё, что не было помечено --- считается мусором.
  5. Память под этими объектами освобождается.
Этот принцип называется tracing garbage collection.
2.8. Почему нельзя полагаться на finalize ()
Ранее в Java существовал метод finalize ().

Сегодня он считается устаревшим, потому что:
  • нет гарантии, когда он будет вызван,
  • нет гарантии, что он будет вызван вообще,
  • он ухудшает работу GC.
Современный подход --- использовать try-with-resources и явное закрытие ресурсов.
3. Поколенческая модель памяти (Generational Model)
3.1. Главная идея
В основе большинства современных сборщиков мусора лежит
поколенческая гипотеза (Generational Hypothesis).

Она формулируется просто:
Большинство объектов живут очень недолго.

В реальных приложениях создаётся большое количество временных объектов:
  • строки,
  • объекты-обёртки,
  • промежуточные результаты вычислений,
  • объекты внутри методов.

Пример:
public String buildMessage(String name) {
    return "Hello, " + name;
}
Во время выполнения создаются временные объекты (StringBuilder, промежуточные строки), которые живут очень короткое время.

Именно это наблюдение определило архитектуру Heap.
3.2. Разделение Heap на поколения
Heap делится на две основные части:
  • Young Generation (молодое поколение)
  • Old Generation (старшее поколение)

Новые объекты создаются в Young Generation.
Если объект переживает несколько сборок мусора, он перемещается в Old Generation.
Таким образом:
  • короткоживущие объекты быстро удаляются,
  • долгоживущие проверяются реже.
3.3. Структура Young Generation
Young Generation состоит из трёх областей:

  • Eden
  • Survivor 0 (S0)
  • Survivor 1 (S1)
3.4. Eden --- место создания объектов
Почти все новые объекты создаются в Eden:
User user = new User();
Особенности Eden:
  • сюда попадают новые объекты,
  • область быстро заполняется,
  • при заполнении запускается Minor GC,
  • большинство объектов умирают именно здесь.
3.5. Survivor-области --- зона выживших
Survivor 0 и Survivor 1 предназначены для объектов, которые пережили Minor GC.

Процесс упрощённо выглядит так:
  1. Eden заполняется.
  2. Запускается Minor GC.
  3. Живые объекты копируются в одну из Survivor-областей.
  4. Eden полностью очищается.

При следующей сборке:
  • живые объекты из Eden и текущей Survivor
  • копируются во вторую Survivor,
  • предыдущая очищается.

Схематично:
Eden → S0
Eden + S0 → S1
Eden + S1 → S0
...
Всегда одна Survivor используется как источник, другая --- как приёмник.
3.6. Возраст объекта и продвижение в Old Generation
Каждый объект имеет «возраст» --- счётчик, который увеличивается при каждом Minor GC, если объект продолжает быть достижимым.

Если объект:
  • пережил несколько Minor GC,
  • достиг порогового возраста,
он перемещается в Old Generation.

Old Generation предназначен для:
  • долгоживущих объектов,
  • кэшей,
  • синглтонов,
  • объектов уровня приложения.
3.7. Minor GC и Major GC
Minor GC:
  • работает только с Young Generation,
  • выполняется часто,
  • обычно быстрый.

Major GC / Full GC:
  • затрагивает Old Generation,
  • выполняется реже,
  • работает дольше,
  • может вызывать заметные паузы.
3.8. Почему поколенческая модель эффективна
Если бы GC каждый раз проверял весь Heap, это было бы дорого по времени.

Но благодаря разделению:
  • часто очищается только Young Generation,
  • большая часть мусора удаляется быстро,
  • Old Generation проверяется реже.
Поскольку большинство объектов живут мало, Minor GC освобождает значительную часть памяти при относительно небольших затратах.
4. Базовые алгоритмы сборки мусора
Прежде чем разбирать конкретные виды GC в JVM (G1, ZGC и другие), нужно понять фундаментальные алгоритмы, на которых они построены.
Практически все современные сборщики используют комбинацию нескольких базовых идей.
4.1. Mark-Sweep (Пометить и удалить)
Один из самых базовых алгоритмов.

Этапы:
  1. Mark (пометка)
  2. GC обходит граф объектов от GC Roots
  3. и помечает все достижимые объекты.
  4. Sweep (очистка)
  5. Все непомеченные объекты удаляются.

Плюсы:
  • Простота реализации.
  • Не требует дополнительной памяти.

Минусы:
  • Возникает фрагментация памяти.
  • После удаления остаются «дыры» в Heap.

Пример фрагментации:
[Obj][free][Obj][free][free][Obj]
Если нужно выделить большой объект, может не найтись непрерывного блока памяти.
4.2. Mark-Compact (Пометить и уплотнить)
Решение проблемы фрагментации.

Этапы:
  1. Mark --- как и раньше.
  2. После удаления мусора живые объекты сдвигаются (компактируются), чтобы убрать разрывы.

Результат:
[Obj][Obj][Obj][free][free][free]
Плюсы:
  • Нет фрагментации.
  • Память используется эффективнее.

Минусы:
  • Требует перемещения объектов.
  • Обновляются ссылки.
  • Обычно выполняется во время Stop-The-World.
4.3. Copying (Копирующий алгоритм)
Используется в Young Generation.

Идея:

Heap делится на две области:
  • активная (From)
  • пустая (To)
При сборке:
  • живые объекты копируются в пустую область,
  • старая область полностью очищается.

Плюсы:
  • Нет фрагментации.
  • Быстрая очистка.
  • Подходит для областей, где мало выживших объектов.

Минусы:
  • Требуется дополнительное пространство.
  • Неэффективен, если большинство объектов живые.
  • Именно этот алгоритм используется в Eden и Survivor областях.
4.4. Generational Collection
Это не отдельный алгоритм, а архитектурный принцип.

Идея:
  • разделить объекты по возрасту,
  • применять разные алгоритмы к разным поколениям.

Обычно:
  • Young → Copying
  • Old → Mark-Compact
4.5. Concurrent Marking
Более современный подход.

Основная идея:
  • выполнять большую часть работы параллельно с приложением,
  • минимизировать Stop-The-World паузы.

Используется в:

  • CMS
  • G1
  • ZGC
  • Shenandoah
4.6. Что важно понять
Все современные GC --- это комбинации:

  • Mark
  • Sweep
  • Compact
  • Copying
  • Concurrent phases

Различаются они тем:
  • как часто останавливается приложение,
  • какие части выполняются конкурентно,
  • как управляется память.
5. Stop-The-World (STW) и паузы GC
5.1 Что такое Stop-The-World
Stop-The-World (STW) --- это фаза, во время которой выполнение всех потоков приложения приостанавливается, чтобы GC мог выполнить критические операции с памятью.

Это необходимо для:

  • корректного перемещения объектов,
  • обновления ссылок,
  • уплотнения памяти,
  • обеспечения консистентности графа объектов.

Некоторые современные GC минимизируют STW, но полностью исключить его невозможно.
5.2 Minor и Full GC
Minor GC:
  • Работает только с Young Generation.
  • Обычно вызывает короткие паузы.
  • Происходит часто.

Full (Major) GC:
  • Затрагивает Old Generation.
  • Может включать компактизацию.
  • Вызывает более длительные паузы.

Частые Full GC --- признак проблем с памятью.
6. Виды Garbage Collector в JVM и их эволюция
Понимание версий JVM важно, потому что разные GC появлялись в разные годы и отражают эволюцию требований к производительности.

Ниже приведён исторический обзор.
6.1 Serial GC
📅 Появился: JDK 1.2 (1998)

Первый базовый сборщик мусора HotSpot JVM.

Характеристики:
  • Один поток GC.
  • Полностью Stop-The-World.
  • Young → Copying.
  • Old → Mark-Compact.
Используется по умолчанию в клиентском режиме (ранние версии JVM).

Параметр:
-XX:+UseSerialGC
6.2 Parallel GC (Throughput GC)
📅 Появился: JDK 1.4 (2002)

Цель --- повысить пропускную способность на многопроцессорных системах.

Особенности:
  • Несколько потоков GC.
  • Всё ещё STW.
  • Оптимизирован для throughput.

С 2006 года (Java 6) стал популярным выбором для серверов.
С Java 8 был GC по умолчанию.

Параметр:
-XX:+UseParallelGC
6.3 CMS (Concurrent Mark Sweep)
📅 Появился: JDK 1.4.2 (2003)

Первый серьёзный шаг к low-latency GC.

Особенности:
  • Большая часть работы выполняется конкурентно.
  • Использует Mark-Sweep.
  • Минимизирует паузы.

Проблемы:
  • Фрагментация памяти.
  • Возможны внезапные Full GC.
📅 Устарел: Java 9
📅 Удалён: Java 14
6.4 G1 GC (Garbage First)
📅 Появился: Java 7 (2012) --- экспериментально
📅 Стал стабильным: Java 8
📅 GC по умолчанию: Java 9 (2017)

Основная идея:
  • Heap делится на регионы.
  • Очистка происходит по приоритету регионов с максимальным количеством мусора.

Особенности:
  • Concurrent Marking.
  • Region-based compaction.
  • Предсказуемые паузы.

Сегодня --- стандартный выбор для большинства серверных приложений.

Параметр:
-XX:+UseG1GC
6.5 ZGC
📅 Появился: Java 11 (2018) --- экспериментально
📅 Production-ready: Java 15 (2020)

Цель:
  • Минимизировать паузы (<10 мс).
  • Поддержка больших Heap (до терабайтов).

Особенности:
  • Почти полностью конкурентный GC.
  • Использует colored pointers.
  • Перемещение объектов без долгих STW.

Параметр:
-XX:+UseZGC
6.6 Shenandoah
📅 Появился: Java 12 (2019) --- экспериментально
📅 Production-ready: Java 15+

Разработан Red Hat.

Особенности:
  • Конкурентное перемещение объектов.
  • Минимальные паузы.
  • Подходит для систем с жёсткими SLA.

Параметр:
-XX:+UseShenandoahGC
6.7 Сравнительная таблица эволюции

GC

Появление

По умолчанию

Основная цель

Serial GC

JDK 1.2

Нет

Простота

Parallel GC

JDK 1.4

Java 8

Throughput

CMS (Concurrent Mark Sweep)

JDK 1.4.2

Нет

Low latency

G1 (Garbage First)

Java 7

Java 9+

Баланс

ZGC

Java 11

Нет

Ultra low latency

Shenandoah

Java 12

Нет

Low latency

6.8 Практический вывод
Эволюция GC отражает изменение требований:

1998--2005 → важен throughput
2005--2015 → снижение пауз
2017+ → минимальная latency и масштабируемость

Сегодня:
  • Для большинства проектов → G1.
  • Для систем с жёсткими требованиями по задержке → ZGC или Shenandoah.
  • Parallel GC --- для задач с максимальной пропускной способностью.

Выбор зависит от нагрузки, размера Heap и SLA.