W3docs

Алгоритмы сборки мусора в Java

Сравнение основных сборщиков мусора Java — Serial, Parallel, G1, ZGC и Shenandoah — с анализом их компромиссов.

JVM избавляет вас от ручного освобождения памяти: сборщик мусора (GC) работает в фоне, находит объекты, к которым ваша программа больше не может обратиться, и освобождает занимаемую ими память. Но «сборщик мусора» — это не единая сущность. В HotSpot JVM реализовано несколько коллекторов, каждый из которых предлагает разный баланс между пропускной способностью (какой процент CPU отдаётся приложению, а не GC), задержкой (как долго приложение простаивает во время работы GC) и объёмом накладных расходов (сколько памяти потребляет сам коллектор).

Правильный выбор имеет значение. Пакетное задание, которое всю ночь обрабатывает числа, хочет максимальную пропускную способность и не обращает внимания на паузы; торговая система или веб-сервер хочет как можно более коротких пауз, даже если общая пропускная способность немного снизится. В этой главе сравниваются производственные коллекторы и показывается то, что у них есть общего: объект живёт ровно столько, сколько он достижим.

Как коллекторы решают, что сохранить

Каждый коллектор HotSpot отвечает на один и тот же вопрос — какие объекты всё ещё используются — одинаковым способом: он отслеживает достижимость от множества корней GC (локальные переменные на стеке, статические поля, активные потоки). Любой объект, достижимый из корня, является живым; всё остальное — мусор. Область видимости и возраст значения не имеют; имеет значение только достижимость. Для более широкого понимания того, как это вписывается в среду выполнения, см. Сборка мусора в Java и Стек и куча.

public class Reachability {
    public static void main(String[] args) {
        String a = new String("kept");      // reachable via local variable 'a'
        String b = new String("dropped");   // reachable via 'b'...
        b = null;                            // ...until now: "dropped" is unreachable
        System.out.println(a);               // 'a' is still a GC root reference
    }
}

В момент выполнения b = null объект "dropped" не имеет пути ни от одного корня и становится доступным для сборки. Коллектор может освободить его немедленно, значительно позже или — если программа завершится раньше — никогда. Вы никогда не вызываете free; вы просто прекращаете ссылаться на объект.

Структура поколенческой кучи

Большинство Java-объектов умирают молодыми. Коллекторы используют это с помощью поколенческой кучи: новые объекты попадают в молодое поколение (Eden и два пространства выживших), а объекты, пережившие несколько сборок, продвигаются в старое поколение. Частая сборка небольшого, перегруженного мусором молодого поколения — дешёвая операция; большое старое поколение собирается значительно реже.

РегионЧто здесь находитсяКак часто собирается
EdenТолько что выделенные объектыПри каждой малой GC
Survivor (S0/S1)Объекты, пережившие малую GCПри каждой малой GC
Old (tenured)Долгоживущие, продвинутые объектыПри основной / полной GC
MetaspaceМетаданные классов (вне кучи)При выгрузке классов

Малая GC очищает молодое поколение и выполняется быстро; основная или полная GC затрагивает старое поколение и является источником длительных пауз, которых все боятся.

Сравнение коллекторов

HotSpot позволяет выбрать коллектор с помощью одного флага, и каждый настроен на различную цель. Алгоритм в коде вы меняете редко — вы задаёте его в командной строке.

java -XX:+UseSerialGC     MyApp   # single-threaded, tiny heaps
java -XX:+UseParallelGC   MyApp   # throughput-oriented, multi-threaded
java -XX:+UseG1GC         MyApp   # balanced, the default since Java 9
java -XX:+UseZGC          MyApp   # sub-millisecond pauses, huge heaps
java -XX:+UseShenandoahGC MyApp   # low pause, concurrent compaction

Таблица ниже — это модель для запоминания:

КоллекторСильная сторонаПоведение при паузахТипичное применение
SerialПростейший, минимальные накладные расходыStop-the-world, один потокМалые кучи, контейнеры, CLI
ParallelНаибольшая пропускная способностьStop-the-world, много потоковПакетная / обработка данных
G1Сбалансированный, предсказуемыйВ основном конкурентный, целевая паузаУниверсальный по умолчанию
ZGCОчень низкая задержкаМенее миллисекунды, конкурентныйКучи от нескольких ГБ до ТБ
ShenandoahОчень низкая задержкаПаузы не зависят от размера кучиОтзывчивые сервисы

G1 («Garbage-First») является коллектором по умолчанию начиная с Java 9. Он делит кучу на регионы одинакового размера и сначала собирает регионы с наибольшим количеством мусора, ориентируясь на цель по времени паузы, которую вы задаёте с помощью -XX:MaxGCPauseMillis=200.

Конкурентный vs stop-the-world

Ключевой аспект — когда потокам приложения приходится останавливаться. Stop-the-world (STW) коллекторы (Serial, Parallel) приостанавливают все потоки приложения во время работы — просто и с высокой пропускной способностью, но пауза растёт вместе с кучей. Конкурентные коллекторы (ZGC, Shenandoah и большая часть G1) выполняют основную работу пока ваши потоки продолжают работать, поэтому паузы остаются короткими даже при кучах в гигабайты или терабайты.

# See exactly what the collector is doing and how long it pauses
java -Xlog:gc -XX:+UseG1GC MyApp
# Sample output line:
# [0.412s][info][gc] GC(3) Pause Young (Normal) (G1 Evacuation Pause) 24M->5M(64M) 1.832ms

Эту строку лога стоит расшифровать: 24M->5M(64M) означает, что используемая куча уменьшилась с 24 МБ до 5 МБ при общем объёме 64 МБ, а приложение простаивало 1.832ms. Чтение вывода -Xlog:gc — это единственный наиболее полезный навык настройки GC: измеряйте перед тем, как менять флаги.

Наблюдение за сборкой из кода

Вы не можете напрямую вызвать конкретный алгоритм из Java, но вы можете наблюдать за происходящей сборкой. WeakReference позволяет хранить указатель, который не удерживает свою цель живой, так что можно спросить: «был ли этот объект уже собран?» Класс Runtime сообщает об использовании кучи, а System.gc() — это подсказка, но никак не команда — запустить сборку прямо сейчас.

import java.lang.ref.WeakReference;

WeakReference<byte[]> ref = new WeakReference<>(new byte[1024]);
System.out.println("Before GC: " + (ref.get() != null)); // true
System.gc();
System.out.println("After GC:  " + (ref.get() != null)); // usually false

Запускаемый пример ниже объединяет эти части вместе: он выделяет волну мусора, удерживает одного выжившего достижимым, наблюдает за недостижимым объектом через слабую ссылку и измеряет кучу до и после сборки.

java— editable, runs on the server

Что можно извлечь из запуска:

  • Шаг 2 выводит true: наблюдаемый объект всё ещё строго достижим через переменную garbage, поэтому WeakReference может его прочитать.
  • Шаг 3 сообщает об используемой куче после 300 000 краткосрочных выделений. Точное число варьируется от запуска к запуску — малая GC уже могла очистить большую часть этой волны в середине цикла, — но именно такая нагрузка на молодое поколение и есть то, для обработки чего каждый поколенческий коллектор спроектирован дёшево.
  • Шаг 4 выводит true, подтверждая, что наблюдаемый объект был освобождён, когда garbage = null сделал его недостижимым, а System.gc() запустил сборку — доказательство того, что потеря достижимости, а не выход из области видимости, освобождает память.
  • Шаг 5 выводит true: survivor, по-прежнему ссылаемый живой локальной переменной (корнем GC), проходит через сборку нетронутым.
  • Шаг 6 показывает, что используемая куча возвращается к значению, близкому к базовому, демонстрируя, что коллектор возвращает освобождённое пространство для повторного использования, а не утечку.

Подробнее о типах ссылок, используемых здесь — сильных, мягких, слабых и фантомных — см. Ссылки в Java.

Практика

Практика
Какое единственное свойство определяет, сохранит ли сборщик мусора объект, независимо от того, какой алгоритм используется?
Какое единственное свойство определяет, сохранит ли сборщик мусора объект, независимо от того, какой алгоритм используется?
Was this page helpful?