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 и всего, что с ним связано
ant1free2e
добро напомню, что джава была создана чтобы разработчикам не приходилось думать о выделениях памяти и подсчете ссылок