JEP draft: Automatic Heap Sizing for G1
Автоматический выбор размера кучи для G1
| Ответственный | Thomas Schatzl |
| Тип | Feature |
| Область | Implementation |
| Статус | Submitted |
| Компонент | hotspot / gc |
| Обсуждение | hotspot dash gc dash dev at openjdk dot org |
| Трудоёмкость | L |
| Длительность | L |
| Рецензенты | Dan Heidinga, Ivan Walulya, Vladimir Kozlov |
| Создан | 2025/06/11 08:32 |
| Обновлён | 2026/04/06 12:57 |
| Задача | 8359211 |
Аннотация
Автоматически и динамически подстраивать максимальный размер кучи Java под окружение при использовании сборщика мусора G1 (G1).
Цели
При использовании G1:
- Динамически подстраивать максимальный размер кучи Java под изменения объёма доступной памяти в окружении.
- Типичная производительность не должна заметно отличаться от той, которой конечный пользователь добивается, правильно настроив размер кучи Java вручную.
Что не является целью
Целью этого JEP не является:
- Найти статический оптимальный максимальный размер кучи Java.
- Убрать существующую возможность задавать статические границы кучи с помощью существующих параметров размера кучи Java.
- Изменить существующую динамическую подстройку текущего размера кучи Java под нагрузку.
Мотивация
Когда пользователи выбирают сборщик мусора G1, максимальный размер кучи — единственный параметр, связанный с размером памяти, который им должно быть нужно задавать. К сожалению, задать хороший максимальный размер кучи общеизвестно трудно. Потребление памяти приложением и его потребности в памяти зависят от входных данных и меняются на разных этапах выполнения, но пользователя просят заранее задать максимальный размер кучи на всё время работы приложения. Помимо памяти кучи Java, сама JVM выделяет для своих нужд некоторый, нефиксированный объём памяти, который также зависит от выбранного максимального размера кучи, и пользователю нужно учитывать это при его выборе. Если максимальный размер кучи Java задан слишком маленьким, приложению может не хватить памяти; если слишком большим, памяти может не хватить окружению. Чтобы найти хороший максимальный размер кучи, нужно измерить потребление памяти и пропускную способность приложения при разных максимальных размерах кучи Java в экспериментальной среде, которая подаёт приложению репрезентативную нагрузку. Уже одна подготовка такой среды — сложная задача для большинства пользователей.
Мы утверждаем, что JVM может автоматически управлять максимальным размером кучи Java и соблюдать баланс между потреблением CPU и памяти лучше, чем пользователь может вручную с помощью явной настройки. JVM должна отслеживать свободную память, доступную в окружении, и по этой всегда актуальной информации автоматически определять максимальный размер кучи. Пользователям не должно быть нужно задавать -Xmx при выборе G1. G1 должно быть разрешено при необходимости использовать всю память окружения, как это делает приложение на C, и при этом своевременно реагировать на уменьшение свободной памяти. Наконец, пользователи должны иметь возможность выразить предпочтение между меньшим использованием CPU сборщиком мусора (но большим потреблением памяти) и большим использованием CPU сборщиком мусора (но меньшим потреблением памяти), аналогично тому, что JEP XXX предоставляет для сборщика ZGC.
При автоматическом выборе размера кучи потребление памяти может дополнительно зависеть от того, сколько свободной памяти доступно в окружении. Одно и то же приложение может потреблять больше памяти, если она не нужна другим приложениям, и меньше памяти, когда другие приложения, работающие в том же окружении, используют больше памяти.
Описание
G1 дорабатывается так, чтобы автоматически выбирать максимальный размер кучи Java, который динамически подстраивается под меняющиеся условия в программе и в окружении.
Выбранный размер кучи будет лежать между минимальным (-Xms) и максимальным (-Xmx) значениями из настроек кучи, но если одно из них или оба не заданы пользователем, то при использовании G1 максимальный и минимальный размеры кучи по умолчанию будут изменены, чтобы дать автоматическому выбору размера кучи как можно больше свободы, следующим образом:
- Минимальный и начальный размеры кучи по умолчанию (
-Xms) меняются на 4 МБ. - Максимальный размер кучи по умолчанию (
-Xmx) меняется на 100 % доступной оперативной памяти компьютера или на границу compressed oops, если используются compressed oops, в зависимости от того, что меньше (см. раздел Определение максимального размера кучи), за вычетом небольшого резерва.
В этих границах G1 будет динамически подстраивать размер кучи Java, как описано ниже:
- Эргономика G1 будет автоматически и динамически настраивать максимальный размер кучи в соответствии с изменениями объёма свободной памяти в окружении. Эта реакция на нехватку памяти в окружении будет состоять либо в уменьшении максимального размера кучи и сокращении потребления памяти JVM за счёт возврата памяти операционной системе, либо в увеличении максимального размера кучи и использовании большего объёма памяти по мере роста свободной памяти в окружении, чтобы лучше достигать целей сборщика мусора G1.
- Внезапный рост запросов приложения на выделение памяти обрабатывается особо, за счёт быстрого расширения кучи Java.
- Новый способ (
-XX:G1GCIntensity) указать намерение пользователя относительно компромисса между производительностью и объёмом занимаемой памяти, чтобы влиять на этот динамический максимальный размер кучи.
Благодаря этим изменениям необходимость настраивать максимальный размер кучи при использовании G1 должна значительно снизиться.
Эта возможность будет включена по умолчанию при использовании сборщика мусора G1. Поскольку сборщик мусора G1 сейчас является сборщиком по умолчанию, конечному пользователю, как правило, ничего не нужно делать, чтобы получить преимущества от этих изменений. Если заданы ограничения минимального и максимального размера кучи (через -Xms и -Xms), они зададут рабочие пределы, в которых будут действовать эти новые возможности. Пользователям рекомендуется убрать эти параметры.
Автоматическая динамическая настройка
Определив начальный максимальный размер кучи, G1 непрерывно отслеживает поведение сборщика мусора, приложения и окружения, корректирует максимальный размер кучи и постепенно настраивает фактический размер кучи с помощью существующих механизмов.
Определение максимального размера кучи
Если пользователь не задал максимальный размер кучи, эргономика G1 определяет его как 100 % доступной оперативной памяти в окружении с учётом текущих настроек compressed oops, за вычетом небольшого резерва свободной памяти.
Без дополнительных параметров граница compressed oops находится примерно на 32 ГБ. Если по умолчанию оставаться в этих пределах, G1 продолжает пользоваться преимуществами в производительности, которые дают compressed oops, в типичном случае приложений, которым требуется меньше 32 ГБ. При отключении compressed oops G1 может использовать всю доступную память окружения за вычетом небольшого резерва.
Резерв системной памяти, которую JVM не использует, как правило, полезен даже при развёртываниях с одним приложением. Например, он позволяет заполнять файловые кэши, что обычно повышает производительность системы. В то же время, как объясняется в разделе Быстрое расширение, эта неиспользуемая память служит страховочным буфером, с помощью которого можно избежать неожиданной реакции алгоритма сборки мусора в виде сборок всей кучи, если интенсивность выделения памяти приложением резко возрастёт.
Измерение накладных расходов CPU
G1 — сборщик мусора с поколениями. Молодые объекты размещаются в молодом поколении, которое собирается чаще во время фаз, где собирается только молодое поколение. Старые объекты переводятся в старое поколение, поначалу без его сборки. Старое поколение собирается реже, в фазе освобождения пространства (в которой собираются оба поколения), в соответствии с циклом сборки мусора G1.
Чтобы решить, увеличивать или уменьшать кучу, G1 отслеживает использование CPU сборщиком мусора, стараясь удерживать целевое использование CPU сборкой мусора, которое выводится из текущего значения -XX:GCTimeRatio и корректируется с учётом -XX:G1GCIntensity, текущего максимального размера кучи и других существующих переменных. В существующих эвристиках G1 проверяет общее использование CPU сборщиком мусора при каждой сборке молодого поколения. Если серия сборок мусора потребляет больше CPU, чем целевое значение, куча расширяется. И наоборот, если серия сборок потребляет меньше CPU, чем это целевое значение, куча уменьшается.
Сборка мусора — не единственные накладные расходы CPU, которые вносит GC. Частые циклы сборки налагают на приложение и другие издержки CPU, например более частое выполнение барьеров GC (инструкций, выполняемых при обращении к объектам Java, пока идёт цикл сборки мусора). Существующие эвристики автоматического изменения размера кучи учитывают это и расширяют кучу, чтобы минимизировать такое влияние. Это влияние может быть тем больше, чем больше CPU потребляет само приложение, и это тоже учитывается.
Реакция на нехватку памяти в окружении
Процесс JVM может автоматически найти подходящий размер кучи для заданного целевого использования CPU сборщиком мусора. Однако если позволить JVM использовать столько памяти, сколько она хочет, в окружении может не хватить памяти для работы других процессов. Поэтому, помимо отслеживания поведения Java-приложения, G1 будет также непрерывно отслеживать общий объём доступной свободной памяти. Если свободной памяти в окружении становится меньше, G1 скорректирует своё внутреннее целевое использование CPU и попытается уменьшить кучу. Аналогично, в ответ на увеличение свободной памяти в окружении G1 может увеличить потребление памяти JVM, чтобы лучше достигать целей по использованию CPU сборщиком мусора и времени паузы с учётом текущего и прогнозируемого будущего поведения приложения.
Это уменьшение и расширение кучи Java выполняется параллельно с работой приложения; добавляемая память упреждающе и параллельно освобождается (uncommit) или резервируется (commit) и подгружается в страницы, чтобы минимизировать замедления из-за операций ОС.
Этот новый максимальный размер кучи будет автоматически учитывать использование внутренней нативной памяти JVM: если JVM потребуется больше нативной памяти, объём свободной памяти в окружении изменится, а вместе с ним и автоматически определяемый максимальный размер кучи.
Другие приложения, работающие в том же окружении, тоже могут исчерпывать свободную память окружения, что приведёт к увеличению частоты сборок GC: JVM будет потреблять меньше памяти, уменьшая кучу ценой большего расхода CPU. Несколько JVM, использующих G1 и работающих в одном окружении, придут к равновесию, а не будут бороться друг с другом за память.
В MacOS или Windows с включённым сжатием памяти непрерывно отслеживается соотношение сжатой и несжатой памяти. Воспринимаемый размер резерва памяти масштабируется в соответствии с этим коэффициентом сжатия. Когда ОС начинает сжимать больше памяти, GC будет активнее освобождать мусор и возвращать память ОС, снижая нагрузку на её механизм сжатия.
Быстрое расширение
Приемлемая производительность при запуске — важная цель для сбалансированного сборщика мусора, такого как G1. Когда JVM запускается с начальным размером кучи 4 МБ на большом компьютере с множеством ядер, она быстро окажется в ситуации, когда такого размера кучи Java недостаточно. Приложению может потребоваться куча размером, например, 20 ГБ, и в этом случае сборщику мусора нужно будет расширить кучу, причём очень быстро.
Именно при запуске, когда куча изначально мала, сборка мусора, скорее всего, будет запускаться рано и часто, поскольку приложению, вероятно, потребуется больше памяти быстрее, чем сборщик мусора сможет её освободить. Чтобы справиться с такими всплесками выделения памяти приложением, G1 будет продолжать расширять кучу согласно существующим эвристикам.
Помимо расширения кучи при всплесках выделения памяти, сборщик мусора будет использовать данные о недавнем использовании CPU сборщиком мусора и об активности приложения, чтобы при необходимости увеличивать кучу. Увеличение кучи позволяет G1 снизить частоту сборок, что, в свою очередь, снижает использование CPU сборщиком мусора и поэтому может повысить пропускную способность приложения.
G1GCIntensity
У пользователей могут быть разные предпочтения относительно того, как для разных приложений соотносить использование CPU сборщиком мусора и объём занимаемой памяти.
Новый параметр -XX:G1GCIntensity влияет на этот компромисс между использованием CPU сборщиком мусора и объёмом занимаемой памяти. Он принимает целое значение от 0 до 10, по умолчанию 5. Это значение соответствует балансу между использованием CPU сборщиком мусора и объёмом занимаемой памяти, который дают существующие значения по умолчанию. Увеличьте значение, чтобы CPU использовался больше, а куча была меньше; уменьшите его, чтобы CPU использовался меньше, а куча была больше.
Параметр -XX:G1GCIntensity является управляемым (manageable), то есть при желании его можно изменить во время выполнения.
Тестирование
Это улучшение в первую очередь влияет на показатели производительности. Поэтому его производительность будет тщательно протестирована на самых разных нагрузках. Заданные критерии успеха будут проверены на этих нагрузках.
Риски и допущения
Из-за того что максимальный размер кучи по умолчанию меняется с 25 % доступной памяти на всю доступную память, есть риск, что новые эвристики будут использовать больше памяти, чем текущая реализация, и поэтому другим процессам может не хватить памяти. Однако и при политике максимального размера кучи в 25 % по умолчанию такой риск уже есть, когда в одном окружении работает несколько JVM с этим значением по умолчанию. Кроме того, при динамически обновляемом максимальном размере кучи весьма вероятно, что ошибка нехватки памяти (out-of-memory error) будет выброшена до того, как будут превышены лимиты памяти окружения.