Shenandoah — это высокопроизводительный сборщик мусора с низкими задержками, разработанный компанией Red Hat и впервые внедрённый в OpenJDK. Он был создан с целью минимизировать задержки на протяжении работы приложения, что особенно важно для приложений с высокими требованиями к производительности и отзывчивости. В новом переводе от команды Spring АйО рассмотрим основные особенности Shenandoah, его работу, настройку и примеры использования.


Эта статья представляет собой краткое введение в Shenandoah — высокопроизводительный сборщик мусора с низкими задержками, разработанный компанией Red Hat. В ней рассматриваются основные функции Shenandoah, варианты использования, ведение журналов сборки мусора и базовые методы устранения неполадок.

Этот материал предназначен для общего ознакомления. Для более подробного изучения рекомендуется обратиться непосредственно к документации в апстриме. Адрес списка рассылки апстрима: shenandoah-dev@openjdk.org.

О сборщике мусора Shenandoah


Вот краткое описание ключевых характеристик Shenandoah:

  • Параллельная работа: приложение продолжает выполняться одновременно со сборкой мусора.

  • GC на основе расположения: указатели перенаправления (forwarding pointers) позволяют Shenandoah собирать отдельные регионы независимо друг от друга, без использования наборов запоминания (remembered sets).

  • Не использует поколений: heap не содержит условного деления на молодое, зрелое и старое поколения. Соответственно, в логах не следует искать упоминаний о молодых или старых поколениях.

    Комментарий от эксперта Spring АйО, Михаила Поливахи: Какое-то время назад это действительно было так - Shenandoah, изначально разработанный в основном инженерами из RedHat, был коллектором без поколений. Но уже в 2021 г. с JEP 404 был в экспериментальном режиме предложен Shenandoah с разбивкой на поколения. В Java 25, кстати, которая вышла совсем недавно, в JEP 521 Generational Shenandoah становится стабильный фичей и выходит из статуса экспериментальной.

    Поскольку это перевод, коверкать автора мы не стали, но пояснить эту деталь, считаем, нужно.

    Комментарий от эксперта Spring АйО, Павла Кислова: к моменту java 25 Shenandoah обрел возможность работы с поколениями и это заявлено, как стабильная фича, но по-умолчанию используется single generation mode. Для использования поколений стоит использовать флаг java -XX:+UseShenandoahGC -XX:ShenandoahGCMode=generational (упоминается в статье далее)

  • Работает в 2 или 3 параллельных фазах: Shenandoah функционирует в 2 или 3 параллельных фазах. Фаза обхода (или режим обхода) устарела.

  • Поддерживается в OpenJDK 1.8, 11, 17 и 21 от Red Hat.

  • Выполняет compaction параллельно: благодаря этому избегается проблема фрагментации памяти.

  • Цель — паузы менее 10 мс: это цель, аналогичная целям сборщика мусора ZGC от Oracle. Отметим, что ZGC и genZGC также вполне себе пригодны для использования в продакшене.

Эти концепции могут показаться сложными, особенно если вы ранее не сталкивались с внутренним устройством GC, но процесс работы Shenandoah во многом похож на поведение G1 GC от Oracle. Подробнее:

Как и G1 GC, Shenandoah основан на региональной структуре памяти — она разбивается на отдельные области, как сетка. Однако, в отличие от G1 GC, в котором регионы ассоциированы с молодыми, старыми или зрелыми объектами, в Shenandoah у регионов нет такой привязки к поколениям.

Параллельная компактация (compaction) использует механизм снимка в начале (SATB — snapshot-at-the-beginning), который также применяется в CMS и G1. Вместе с этим используется указатель перенаправления (forwarding pointer), добавляющий одно слово к двум уже существующим в OpenJDK. Shenandoah выполняет компактацию, чтобы избежать фрагментации, как это происходило в CMS (устаревший в JDK 11 и удалённый в JDK 17).

Факт параллельности означает, что очистка и компактация происходят одновременно с выполнением приложения, что позволяет избежать полной приостановки работы (Stop The World). Чем больше процессов выполняется параллельно, тем меньше остаётся задач для фаз, выполняемых параллельно. Параллельная компактация — ключевой элемент алгоритма Shenandoah, наряду с тремя основными параллельными фазами: маркировкой/mark, оценкой и обновлением ссылок. Фаза маркировки (marking phase) всё равно требует две небольшие паузы - init и final mark, но они крайне непродолжительные.

Изначальной целью Shenandoah была минимизация пауз до <10 мс, что позволяет свести к минимуму влияние на производительность приложений. Впоследствии, благодаря внедрению параллельных механизмов и улучшений, паузы удалось сократить до субмиллисекундного уровня. За более чем десять лет разработки производительность Shenandoah значительно выросла, и теперь он считается зрелым решением, пригодным для продакшн-сред.

Поколенческий аспект будет рассмотрен отдельно ниже.

Наконец, стоит отметить эволюцию алгоритма Shenandoah. Первая версия — Shenandoah1 — имела больший объём занимаемой памяти и описана в большинстве ранних публикаций. Впоследствии её сменила реализация Shenandoah2 — вторая версия алгоритма, реализованная в текущем коде.

Использование и настройка Shenandoah GC

Просто добавьте флаг -XX:+UseShenandoahGC, и приложение будет его использовать.

Для настройки см. наше основное решение по настройке Shenandoah Collector, которое охватывает настройку слабых ссылок и эвристический выбор.

Очистка и выделение регионов

Подобно G1 GC, Shenandoah делит память на регионы, как в сетке. Однако, в отличие от G1 GC, каждому региону не назначается конкретное поколение; вместо этого каждому региону назначаются определённые потоки, которые работают параллельно с приложением. Shenandoah основан на расположении, поэтому есть регионы и размеры регионов, и, следовательно, гомологичная аллокация объектов является особым случаем, который необходимо обрабатывать.

Для конкретного сравнения см. Shenandoah vs G1 GC в OpenJDK.

Коллектор без поколений (пока)

Реализация Shenandoah от Red Hat является non-generational, в то время как Corretto от Amazon предоставляет поддержку поколений. На момент написания Shenandoah с поддержкой поколений является экспериментальной, в то время как generational ZGC является production-ready решением. Ожидается, что будущие релизы OpenJDK 21 принесут GenShen (Generational Shenandoah) в качестве production-ready.

Комментарий от команды Spring АйО

Интерпретация логов GC

OpenJDK 64-Bit Server VM (25.302-b08) for linux-amd64 JRE (1.8.0_302-b08), built on Jul 17 2021 18:13:18 by "mockbuild" with gcc 4.8.5 20150623 (Red Hat 4.8.5-44)
Memory: 4k page, physical 15908268k(2468964k free), swap 8388604k(8186876k free)
CommandLine flags: -XX:CompressedClassSpaceSize=260046848 -XX:GCLogFileSize=3145728 -XX:InitialHeapSize=1366294528 -XX:MaxHeapSize=1366294528 -XX:MaxMetaspaceSize=268435456 -XX:MetaspaceSize=100663296 -XX:NumberOfGCLogFiles=5 -XX:+PrintGC -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:-TraceClassUnloading -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseGCLogFileRotation -XX:+UseShenandoahGC
Regions: 2606 x 512K
Humongous object threshold: 512K
Max TLAB size: 65536B
GC threads: 4 parallel, 2 concurrent
Heuristics ergonomically sets -XX:+ExplicitGCInvokesConcurrent
Heuristics ergonomically sets -XX:+ShenandoahImplicitGCInvokesConcurrent
Shenandoah GC mode: Snapshot-At-The-Beginning (SATB)
Shenandoah heuristics: Adaptive
Pacer for Idle. Initial: 26685K, Alloc Tax Rate: 1.0x
Initialize Shenandoah heap: 1303M initial, 1303M min, 1303M max

Сценарии использования Shenandoah GC

За годы использования и тестирования Shenandoah я столкнулся с множеством ситуаций, когда его использование значительно улучшало производительность, а в других случаях – не так сильно. Ниже я изложил свои рекомендации по тому, когда стоит использовать Shenandoah, а когда нет.

Когда использовать Shenandoah GC

Вы можете использовать non-generational Shenandoah в таких ситуациях, как рабочие нагрузки, которые имеют четко выделенный distribution распределения запросов и также не имеют большую нагрузку от постоянной большой аллокации объектов.

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

Другими примерами могут быть приложения, где объекты старые, а накладные расходы на создание новых объектов малы, в процентном отношении к размеру и количеству регионов, которые необходимо очистить.

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

Когда не использовать Shenandoah GC

Shenandoah (в своей non-generational форме) не подходит для всех ситуаций и рабочих нагрузок, и в некоторых случаях производительность приложения может просесть существенно, нивелируя плюсы от отсутствия пауз. Быть non-generational — это ключевая особенность алгоритма, которая существенно помогает в нескольких аспектах, но может ухудшить ситуацию в более специфичных случаях.

Примером такого случая является создание большого количества очень короткоживущих объектов в случайные моменты времени. Это приводит к тому, что все потоки начинают работать одновременно, что может вызвать несколько последующих полных пауз. В таких случаях generational сборщик мусора, такой как G1GC или Parallel, вероятно, будет более эффективным, разбивая сборку на фазы. Для таких рабочих нагрузок с поколениями Amazon разработал generational Shenandoah — GenShen.

Следовательно, команде разработчиков необходимо внимательно проверить, как non-generational сборщик мусора справляется с задачами, с точки зрения latency, пропускной способности (throughput-а) и, не менее важного, memory footprint — который часто жертвуется в различных ситуациях при разработке на Java или при сборке мусора.

Однако этот non-generational аспект, вероятно, изменится с поддержкой generational Shenandoah, которое будет введено в более поздних релизах, вероятно, в OpenJDK 21+.

В любом случае, главная рекомендация — провести бенчмаркинг приложения с Shenandoah и другими сборщиками, такими как G1GC и ParallelGC, для более прямого сравнения. Я рекомендую сделать это даже до того, как начнете настраивать Shenandoah.

Generational Shenandoah GC

Amazon начал вносить вклад в OpenJDK и представил generational версию Shenandoah в Corretto. В 2021 году была анонсирована эта версия — для её использования нужно скачать Corretto и установить следующие параметры:

-XX:+UseShenandoahGC -XX:+UnlockExperimentalVMOptions

-XX:ShenandoahGCMode=generational

К слову, GenZGC (generational версия ZGC от Oracle) пока не поддерживается в продакшн-средах.

Заметки о бенчмаркинге приложений

Что касается бенчмаркинга, это важный процесс для сравнения производительности определенных настроек или изменений в окружении, который можно провести для изменений в сборке мусора. В данном случае пользователь развертывает приложение с разными сборщиками мусора (например, Shenandoah против G1 GC), подвергает их нагрузке и сравнивает три основных метрики: memory footprint, пропускную способность (throughput) и задержку (latency). Обычно приходится жертвовать footprint-ом ради пропускной способности или задержки.

Просто развернув Shenandoah или G1 GC, пользователь может заметить некоторые изменения в производительности, например, если приложение сильно зависит от поведения молодого или старого поколения (generational поведение) или использует случайные паттерны выделения/освобождения памяти, что может быть ситуацией, когда Shenandoah (в текущей реализации) может продемонстрировать худшую производительность.

Очевидно, что в большинстве случаев для приложений важнейшими будут пропускная способность и задержка, а не memory footprint, за исключением специфических случаев, когда нужно платить за память или процессорные ресурсы.

Дополнительные ресурсы

Чтобы узнать больше о сборщике мусора Shenandoah, я настоятельно рекомендую ознакомиться с документацией, предоставленной разработчиками, которая предоставляет дополнительные подробности и несколько полезных диаграмм.


Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм — Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано

Комментарии (1)


  1. ant1free2e
    25.09.2025 16:02

    добро напомню, что джава была создана чтобы разработчикам не приходилось думать о выделениях памяти и подсчете ссылок