openjdk.ruOpenJDK на русском

JEP 546: Adaptive Heap Sizing for ZGC

Адаптивный выбор размера кучи в ZGC

ОтветственныйErik Österlund
ТипFeature
ОбластьJDK
СтатусCandidate
Компонентhotspot / gc
Обсуждениеhotspot dash gc dash dev at openjdk dot org
Связан сJEP 545: Faster Startup and Warmup with ZGC
РецензентыAlex Buckley, Dan Heidinga, Mark Reinhold
Создан2026/02/05 16:47
Обновлён2026/09/21 15:48
Задача8377305

Аннотация

Доработать сборщик мусора Z Garbage Collector (ZGC), чтобы он адаптивно подстраивал размер кучи с учётом доступных ресурсов, потребностей приложения и потребностей соседних приложений, которые конкурируют за те же ресурсы.

Цели

  • Адаптивно подстраивать размер кучи при изменении нагрузки и хост-системы.

  • Обеспечить такие же пропускную способность и задержку, как у хорошо настроенной кучи, сконфигурированной вручную.

  • Сохранить работу существующих параметров командной строки для задания размера и настройки кучи.

Мотивация

Виртуальная машина Java создаёт иллюзию бесконечной кучи. Задача сборщика мусора, например ZGC, — реализовать эту иллюзию на ограниченной физической памяти компьютера, на котором работает JVM, с помощью средств аппаратуры и операционной системы.

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

ZGC запускает цикл сборки мусора, когда обнаруживает, что куча почти заполнена. Поскольку затраты процессорного времени на один цикл сборки не зависят от размера кучи, из этого следует, что — при прочих равных — с большей кучей GC со временем использует меньше процессорного времени, потому что циклы сборки происходят реже. И наоборот, с меньшей кучей GC со временем использует больше процессорного времени, потому что циклы сборки происходят чаще. Иначе говоря, использование процессора сборщиком мусора со временем обратно пропорционально размеру кучи.

Настроить максимальный размер кучи сложно

Чтобы свести к минимуму использование процессора, ZGC занимает столько памяти, сколько может, вплоть до максимального размера кучи. По умолчанию HotSpot JVM берёт в качестве максимального размера кучи 25 % физической памяти хост-системы. Например, в системе со 128 ГБ физической памяти JVM отведёт под кучу 32 ГБ.

Хотя 25 % физической памяти — разумное значение по умолчанию, нет такого максимального размера кучи, который был бы оптимален для всех приложений. Поэтому максимальный размер кучи можно задать параметром -Xmx. Чтобы выбрать хороший максимальный размер кучи, нужно глубоко понимать поведение приложения. Лучше всего для этого измерить использование памяти, пропускную способность и задержку приложения при разных максимальных размерах кучи в экспериментальной среде с репрезентативной нагрузкой. Однако на практике такие эксперименты трудно провести правильно, поэтому их проводят редко. Хуже того, даже если их провели, их нужно повторять при каждом обновлении приложения или JVM. Кроме того, их результаты нельзя перенести на другие конфигурации оборудования.

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

Настроить JVM так, чтобы она была хорошим соседом, сложно

Если максимальный размер кучи слишком велик, возможны другие неблагоприятные последствия: операционной системе может не хватить физической памяти, и она начнёт выгружать часть страниц виртуальной памяти на диск, из-за чего снизится производительность всей системы. В худшем случае ОС может завершать процессы, чтобы вернуть часть физической памяти.

Максимальный размер кучи — жёсткий предел, который нельзя превысить. Чтобы ZGC был хорошим соседом для других процессов в хост-системе, можно задать мягкий максимальный размер кучи, который меньше жёсткого максимального размера. ZGC старается держать размер кучи ниже мягкого максимума, выполняя сборки мусора чаще, но может расширить кучу сверх мягкого максимума, вплоть до жёсткого, если иначе выделение памяти остановится или приложению не хватит памяти. (Остановки выделения памяти губительны для чувствительных к задержкам приложений, для которых и предназначен ZGC.) Мягкий максимум позволяет ZGC оставлять память другим процессам и при этом сохранять запас памяти, которым он может воспользоваться, если интенсивность выделения памяти непредсказуемо резко вырастет.

Например, рассмотрим приложение, которое хорошо работает с кучей в 2 ГБ, но иногда сталкивается со всплесками нагрузки, требующими кучи в 5 ГБ. Можно задать максимальный размер кучи 5 ГБ (-Xmx5G), но это грозит повлиять на другие процессы. Если же задать максимальный размер кучи 5 ГБ и мягкий максимальный размер 2 ГБ (-Xmx5G -XX:SoftMaxHeapSize=2G), ZGC будет держать кучу меньше 2 ГБ, пока не случится всплеск нагрузки; тогда он расширит кучу максимум до 5 ГБ, а после окончания всплеска снова сократит её до 2 ГБ.

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

Адаптивный выбор размера кучи

Настроить жёсткий и мягкий максимальные размеры кучи ZGC так, чтобы достичь хорошего баланса между использованием памяти и использованием процессора сборщиком мусора, непросто. Было бы гораздо лучше, если бы ZGC мог сам автоматически поддерживать хороший баланс.

ZGC мог бы делать это, адаптивно подстраивая размер кучи на основе измеренных потребностей в ресурсах и их доступности и пользуясь тем, что использование процессора сборщиком мусора, как правило, обратно пропорционально размеру кучи. Он мог бы, когда это возможно, использовать больше памяти, чтобы сократить использование процессора сборщиком мусора; и так же мог бы, когда нужно, использовать меньше памяти ценой увеличения использования процессора сборщиком мусора. Это уменьшило бы потребность в ручной настройке GC, позволило бы лучше справляться с неожиданными изменениями нагрузки, было бы надёжнее при обновлениях приложения и JVM и лучше переносилось бы на разные конфигурации оборудования. Кроме того, ZGC мог бы адаптивно подстраивать размер кучи в зависимости от объёма памяти и процессорного времени, доступных в хост-системе, чтобы быть хорошим соседом для других приложений.

Описание

Мы предлагаем необязательную политику адаптивного выбора размера кучи для ZGC. Если она включена, ZGC будет

  • использовать по умолчанию максимальный размер кучи, равный 100 % физической памяти за вычетом небольшого резерва;

  • адаптивно подстраивать размер кучи, чтобы поддерживать хороший баланс между использованием памяти и использованием процессора сборщиком мусора; и

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

Вместе с сопутствующими улучшениями запуска и прогрева эти изменения сделают задание максимального размера кучи (-Xmx) или мягкого максимального размера кучи (-XX:SoftMaxHeapSize) ненужным и нежелательным. При запуске приложения нужно лишь выбрать ZGC и включить адаптивный выбор размера кучи:

$ java -XX:+UseZGC -XX:+ZAdaptiveHeapSizing ...

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

Настройка использования ресурсов

Для большинства приложений настраивать использование ресурсов ZGC не нужно: значение по умолчанию подойдёт.

Однако при необходимости можно вручную настроить баланс между использованием памяти и использованием процессора сборщиком мусора, задав целевую интенсивность сборки мусора параметром командной строки. Интенсивность — положительное целое число, значение по умолчанию — 5. При меньших значениях ZGC работает менее интенсивно: реже запускает циклы сборки и потому тратит меньше процессорного времени ценой увеличения размера кучи и, следовательно, большего использования физической памяти. При больших значениях ZGC работает интенсивнее: чаще запускает циклы сборки и потому тратит больше процессорного времени, чтобы уменьшить размер кучи и, следовательно, использовать меньше физической памяти.

Например, чтобы тратить немного меньше процессорного времени, но немного больше памяти, чем при настройке по умолчанию:

$ java -XX:+UseZGC -XX:+ZAdaptiveHeapSizing -XX:ZGCIntensity=4 ...

Интенсивность — абстрактная мера использования процессора сборщиком мусора, которая выражает намерение пользователя. Связь между интенсивностью и фактическим использованием процессора сборщиком мусора намеренно не специфицирована. Это позволит нам менять эту связь в будущих выпусках по мере улучшения политик автоматической настройки ZGC. Дополнительное преимущество абстрактной меры интенсивности — она лучше переносится на разные конфигурации оборудования.

Бережное использование процессора и памяти

Объём памяти кучи, занятой живыми объектами, постоянно меняется: приложение создаёт объекты, а ZGC их собирает. Пока приложение прогревается, ZGC агрессивно расширяет кучу, чтобы опережать потребности приложения. Затем, во время работы приложения, ZGC итеративно подстраивает размер кучи, чтобы достичь целевой интенсивности:

  • Если использование процессора сборщиком мусора поднимается выше уровня, заданного целевой интенсивностью, ZGC расширяет кучу в конце следующего цикла GC. Использование процессора сборщиком мусора снизится к целевому уровню, а пропускная способность приложения улучшится.

  • И наоборот, если использование процессора сборщиком мусора опускается ниже уровня, заданного целевой интенсивностью, ZGC сокращает кучу в конце следующего цикла GC. Использование процессора сборщиком мусора вырастет к целевому уровню, а пропускная способность приложения снизится.

Изменения размера кучи сглаживаются во времени, чтобы избежать резких колебаний, как описали Tavakolisomeh и соавторы в статье Heap Size Adjustment with CPU Control.

ZGC — сборщик мусора с поколениями. Новые объекты помещаются в молодое поколение кучи, а долгоживущие объекты переводятся в старое поколение. Частые малые сборки выполняются в молодом поколении, а более редкие полные сборки — во всей куче. Помимо расширения и сокращения кучи, ZGC перераспределяет размеры молодого и старого поколений, чтобы достичь целевой интенсивности. Если из-за серии малых сборок использование процессора сборщиком мусора поднимается выше уровня, заданного целевой интенсивностью, ZGC расширяет кучу немедленно.

Хороший сосед

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

  • Если процесс JVM использует значительную долю памяти хост-системы, но небольшую долю процессорного времени, ZGC сокращает кучу и тем самым увеличивает использование процессора.

  • И наоборот, если процесс JVM использует небольшую долю памяти хост-системы, но большую долю процессорного времени, ZGC расширяет кучу и тем самым снижает использование процессора.

Для ZGC быть хорошим соседом важнее, чем достичь целевой интенсивности. Для этого ZGC смещает целевую интенсивность, заданную по умолчанию или указанную явно, вверх или вниз. Так несколько JVM с ZGC на одной хост-системе будут использовать память и процессорное время в соответствии со своими целевыми интенсивностями.

Пример адаптивного определения размера кучи

В этом примере мы создаём контейнер с 16 ГБ памяти. В этом контейнере мы запускаем экземпляр тестового приложения SPECjbb2015, а затем тестовое приложение с базой данных H2 в памяти. Когда тест базы данных завершается, мы запускаем ещё два экземпляра SPECjbb2015. В течение 42 минут мы отслеживаем использование физической памяти контейнером и четырьмя JVM.

В первые 12 минут первый экземпляр SPECjbb2015 обрабатывает 5000 запросов в секунду. ZGC находит разумный размер кучи, который обеспечивает хорошую производительность и укладывается в ограничения контейнера. Куча расширяется медленно, поскольку ZGC использует процессор умеренно.

В следующие 12 минут приложение с базой данных H2 работает параллельно с первым экземпляром SPECjbb2015. JVM базы данных в самом начале резко расширяет свою кучу; в ответ JVM SPECjbb2015 немедленно сокращает свою кучу, позволяя JVM базы данных, которой нужно много памяти, занять бо́льшую долю доступной памяти. Всё это время экземпляр SPECjbb2015 продолжает обрабатывать 5000 запросов в секунду, не загружая процессор полностью. Однако использование процессора сборщиком мусора у него выше, из-за чего ухудшаются и задержка, и пропускная способность.

После завершения приложения с базой данных, на отметке 24 минуты, первая JVM SPECjbb2015 расширяет свою кучу, чтобы использовать память, которую раньше занимала база данных. ZGC быстро расширяет кучу, чтобы снизить использование процессора, и тем самым улучшает и задержку, и пропускную способность.

На последнем этапе, начиная с отметки 30 минут, параллельно с первым экземпляром SPECjbb2015 работают ещё два экземпляра SPECjbb2015, каждый из которых обрабатывает 5000 запросов в секунду. В течение двух минут три JVM поровну делят доступную память, что обеспечивает хорошую производительность каждой из них.

Альтернативы

Сборщики мусора в реализациях некоторых других языков позволяют управлять балансом между использованием памяти и использованием процессора сборщиком мусора, задавая целевую заполненность памяти, т. е. целевое отношение объёма живых данных к общему размеру кучи. Однако при одной и той же заполненности использование процессора сборщиком мусора может сильно различаться. Кроме того, в сборщиках мусора с поколениями сложно измерять объём живых данных. Измерять использование процессора сборщиком мусора не только проще, но и это точнее отражает то, что пользователи отслеживают и что для них важно.

Риски и допущения

Когда включено адаптивное определение размера кучи, ZGC по умолчанию будет использовать максимальный размер кучи, равный 100 % физической памяти за вычетом небольшого резерва, а не прежние 25 % физической памяти. Поэтому есть риск, что для конкретного приложения ZGC будет использовать больше памяти, чем в прошлых выпусках, и другим процессам не хватит памяти. Однако и при прежнем максимальном размере кучи по умолчанию риск нехватки памяти для других процессов уже существовал, если параллельно работали несколько JVM с этим значением по умолчанию.