Java: Обновлённый сборщик мусора ZGC в JDK 21

Generational ZGC

ZGC, хорошо масштабируемый сборщик мусора Java с минимальной задержкой, был обновлен в JDK 21 (JEP 439). Итак, как мы используем ZGC? И какую производительность мы получим от перехода на Generational ZGC? Давайте повнимательнее посмотрим!

Что такое ZGC?

Изначально ZGC появился в JDK 11 в виде экспериментальной функции, а уже в JDK 15 он был переведён до продуктивной. ZGC разработан с учетом высокой масштабируемости, поддерживая кучи размером до 16 ТБ, сохраняя при этом паузы менее миллисекунды.

ZGC достигает этих целей, работая почти полностью параллельно. Это говорит нам, что ZGC выполняет свою работу по выделению новых объектов, сканированию недоступных объектов, уплотнению кучу и т. д. пока приложение работает.

Но есть и недостатки у такого подхода, они заключаются в том, что приложение снижает пропускную способность, поскольку ресурсы ЦПУ, которые приложение может использовать, вместо этого используются ZGC.

Что значат Generational ZGC?

Сборщик мусора разделяет кучу по поколениям логически на два поколения: молодое поколение и старое поколение. Когда создаётся новый объект, он изначально помещается в молодое поколение, которое сканируется чаще. Если объект существует достаточно долго, он перемещается в старое поколение.

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

Поэтому, ZGC чаще сканирует молодое поколение и может наиболее эффективно использовать ресурсы ЦПУ.

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

Пропускная способность Generational ZGC имеет улучшение около 10% в сравнении с Single-Generational ZGC в JDK 17 и чуть более 10% по сравнению с Single-Generational ZGC в JDK21, в котором наблюдался небольшой регресс.

Хотя на диаграмме всё выглядит почти идентично предыдущей, она рассказывает другую историю. На этой диаграмме показано, что в Generational ZGC наблюдается небольшой регресс средней задержки по сравнению с Single-Generational ZGC.

А вот если мы взглянем на реальные цифры, то увидим, что разница составляет всего 2–3 МИКРОсекунлы.

ZGC выглядит значительно лучше, если посмотреть на максимальное время паузы. На диаграммах ниже видно улучшение времени паузы P99 от 10 до 20%, при этом реальное улучшение составляет от 20 до 30 микросекунд по сравнению с JDK 21 и JDK 17 ZGC Single-Generational ZGC.

Самым большим преимуществом Generational ZGC является то, что он значительно снижает вероятность возникновения самой большой проблемы Single-Generational ZGC — это задержки с распределением ресурсов, задержка выделения — это когда скорость выделения новых объектов превышает скорость, с которой ZGC может освободить память.

Эту проблему можно увидеть, если мы переключим GC на Apache Cassandra и посмотрим на 99,999-й процентиль. На диаграмме ниже показано, что до 75 одновременных клиентов, Single-Generational ZGC и Generational ZGC имеют одинаковую производительность. Однако при наличии более 75 одновременных клиентов Single-Generational ZGC становится перегруженным и сталкивается с проблемой остановки распределения. С другой стороны, Generational ZGC не испытывает этого и поддерживает постоянное время пауз даже при наличии 275 одновременных клиентов.

Использование ZGC

Поскольку внедрение подхода поколений в ZGC было значительным изменением, то команда ZGC установила переходный период от Single-Generational ZGC к Generational ZGC. В JDK 21 реализация с одним поколением остается реализацией по умолчанию при использовании ZGC, но в конечном итоге Generational ZGC станет реализацией по умолчанию в следующем релизе, при этом Single-Generational ZGC планируется объявить устаревшим, а затем удалить. Но когда это произойдёт пока не известно.

В JDK 21 для использования Generational ZGC необходимы следующие два аргумента JVM:

$java -XX:+UseZGC -XX:+ZGenerational

Тюнинг ZGC

ZGC по факту самонастраивающаяся система. В большинстве случаев единственная конфигурация, которую может определить пользователь — это максимальный размер кучи -Xmx<size>. Однако могут возникнуть ситуации, когда требуется тонкая настройка. Вот несколько основных конфигураций на которые стоит обратить внимание.

-XX:SoftMaxHeapSize=<size>: Этот аргумент указывает рекомендуемый максимальный размер кучи, который ZGC будет стараться удерживать. Однако ZGC МОЖЕТ превысит этот лимит, чтобы избежать проблем с распределением. ZGC постарается как можно скорее вернуть размер кучи в исходное состояние SoftMaxHeapSize и вернуть память ОС.

Если вашей основной проблемой является задержка, стоит рассмотреть несколько конфигураций:

Установка минимального размера кучи, -Xms, на то же значение, что и -Xmx. Это не позволит ZGC вернуть неиспользуемую память операционной системе, что может вызвать задержки.

-XX:-ZUncommit: Как альтернатива — это значение можно использовать для отключения возврата памяти в операционную систему.

-XX:ZUncommitDelay=<seconds>: на сколько долго ZGC будет ожидать, прежде чем вернуть обратно память операционной системе. 300 сек — значение по умолчанию.

-XX:+AlwaysPreTouch: это перемещает подготовку кучи на запуск. Это сделает запуск немного медленнее, зато уменьшится средняя задержка во время работы.

Профилирование ZGC

Вместо того чтобы оценить Generational ZGC, чтобы понять, хотите ли вы переключиться на него или измерить влияние изменений настроек, то вам может понадобиться профилировать ZGC, чтобы произвести наиболее точные оценки. 

Существует два основных подхода сбора диагностической информации о сборщике мусора: Логирование GC и JDK Flight Recorder.

Логирование GC

Начиная с JDK 9, использование журналирования JVM стало более объёмным и информативным, обеспечивая при этом данные еще более высокого качества. Это результат двух JEP, включенных в JDK 9, 158 и 271. Это делает ведение журнала JVM отличным вариантом при оценке и сравнении сборщиков мусора.

Ведение журнала JVM настраивается с помощью -Xlogаргумента, как в этом примере:

$ java -Xlog:gc:gen-zgc.log

Эта команда будет захватывать операторы журналирования, только помеченные тегом gc, и писать их в файл gen-zgc.log.

Для более широкого ведения журнала GC вы можете использовать следующее:

$ java -Xlog:gc*:gen-zgc.log

Эта команда будет фиксировать все операторы журналирования, содержащие этот тег gc. Эта команда также выводит таблицу статистики GC, как в этом примере.

Для получения дополнительной информации о ведении журналов JVM обязательно ознакомьтесь с официальной документацией.

JFR — JDK Flight Recorder

JDK Flight Recorder, JFR, — это платформа мониторинга Java, интегрированная непосредственно в JDK. Существует несколько вариантов запуска и настройки JFR, для оценки GC вам необходимо включить его при запуске с помощью -XX:StartFlightRecording, как в примере:

-XX:StartFlightRecording=filename=gen-zgc.jfr,settings=profile

Это приведет к записи данных JFR gen-zgc.jfr и использованию profile настроек, оверхед по нагрузке составляют менее 2%. В качестве альтернативы можно использовать настройки по умолчанию, с накладными расходами менее 1%.

После сбора данных JFR их можно оценить в JDK Mission Control (JMC). JMC предоставляет несколько вкладок для оценки поведения GC, включая обзор сборок мусора, конфигураций GC и общую сводку поведения GC:

Заключение

Generational ZGC делает ZGC отличным вариантом для еще большего количества приложений на Java. ZGC обеспечивает масштабируемость и сверхнизкую задержку, а с добавлением возможностей поколений проблема задержки распределения в значительной степени решена. При обновлении до JDK 21 воспользуйтесь Generational ZGC, чтобы определить, подходит ли он для ваших Java-приложений.

Leave a Comment

Ваш адрес email не будет опубликован. Обязательные поля помечены *