JEP draft: Automatic Heap Sizing for the Serial Garbage Collector
Автоматический выбор размера кучи для сборщика мусора Serial GC
| Автор | Kirk Pepperdine |
| Тип | Feature |
| Область | Implementation |
| Статус | Draft |
| Компонент | hotspot / gc |
| Трудоёмкость | M |
| Длительность | M |
| Создан | 2025/02/15 19:55 |
| Обновлён | 2025/02/17 09:12 |
| Задача | 8350152 |
Аннотация
Это предложение добавить Automatic Heap Sizing (AHS, автоматический выбор размера кучи) в сборщик мусора Serial GC в Java. AHS позволит куче Java адаптироваться к изменениям рабочей нагрузки и нагрузки на память. Предложение во многом опирается на отчёт об ошибке OpenJDK https://bugs.openjdk.org/browse/JDK-8329758, в котором предлагается добавить AHS в ZGC.
Цели
При использовании Serial GC:
- По умолчанию позволить куче безопасно использовать столько памяти, сколько нужно.
- Сделать так, чтобы JVM вела себя добросовестно и возвращала память, если:
- память не нужна;
- свободной системной памяти мало.
- Уметь динамически адаптировать размер кучи к непредсказуемым обстоятельствам.
- Уметь динамически перераспределять пространства поколений, адаптируясь к изменениям жизненного цикла данных.
Что не является целью
Этот JEP не ставит целью:
- Поддерживать оптимальный размер кучи.
- Переопределять существующую возможность задавать статические границы кучи с помощью существующих параметров JVM для размера кучи.
Мотивация
JVM эргономически выбирает Serial GC при запуске, только если ей доступно менее 2 ядер CPU или менее 1792 МБ памяти. JVM, развёрнутые в таких ограниченных средах, часто работают без какой-либо настройки со стороны пользователя. В том числе не указывается размер кучи — самый важный параметр, который ожидается от пользователей. Поэтому куча Java настраивается значениями по умолчанию, которые исторически часто неоптимальны. Неоптимальная конфигурация увеличивает накладные расходы GC, а это ухудшает хвостовые задержки и увеличивает стоимость развёртывания. Пользователи не задают конфигурацию потому, что выбрать хороший размер кучи, как известно, очень трудно. Диапазон размеров кучи, при которых приложение работает производительно, зависит от множества технических деталей по всему технологическому стеку, например:
- Акторы: создают нагрузку, на которую система должна реагировать. Колеблющаяся нагрузка может создавать условия, с которыми статически настроенным системам трудно справиться.
- Приложение: объём памяти, нужный для хранения долгоживущих структур данных, а также объём рабочей памяти, нужный для преобразования входных данных в выходные.
- Библиотеки: используют прямые отображённые байтовые буферы или запускают потоки, из-за чего для кучи остаётся меньше памяти. Рассчитать это заранее, как известно, крайне трудно или невозможно. И трудность такого расчёта, и последствия ошибки возрастают, когда приложение работает в контейнере.
- JVM: использует память не только для кучи, но и для другого: метаданных GC, metaspace, кэша кода, потоков JIT, буферов ZIP и так далее.
- Другие процессы: могут требовать, чтобы потребности приложения в памяти были умеренными.
- ОС: её политики определяют, когда начинать сжатие памяти или подкачку.
- Память: её доступность сильно влияет на отзывчивость приложения. Время выполнения GC зависит от доступности CPU, пропускной способности памяти, политик кэширования, реализации атомарных инструкций и т. д. Например, распространённая конфигурация развёртывания в Kubernetes — 1000 милликор. В таких условиях даже Serial GC будет испытывать нехватку CPU.
Почему JVM не настраивают, несмотря на то как сильно конфигурация кучи влияет на задержку, не вполне понятно. Возможно, существует (обоснованное?) ожидание, что JVM «просто заработает». При этом даже если те, кто развёртывает приложение, в ходе экспериментов найдут оптимальную конфигурацию, эта конфигурация будет статической. Статически настроенные JVM вряд ли смогут адаптироваться ко всем условиям, которые обычно встречаются в крупных развёртываниях. Например, даже в однородных развёртываниях всегда действуют своего рода гармоники, из-за которых отдельным JVM часто нужны разные конфигурации. Невозможно заранее знать, какой JVM какая конфигурация понадобится, даже если они выполняют один и тот же код на одном и том же оборудовании и обслуживают одну и ту же нагрузку от одних и тех же акторов. Всё становится ещё сложнее, когда на одном сервере развёрнут разнородный набор приложений.
Среди условий, которые ухудшают производительность GC:
- Непредсказуемые или ожидаемые всплески рабочей нагрузки.
- Непредсказуемые или ожидаемые изменения характера выделения памяти из-за смены режимов работы или разовых событий.
- Непредсказуемое или ожидаемое изменение жизненного цикла данных из-за смены режимов работы или разовых событий.
- Непредсказуемые профили приложения после обновления программного обеспечения (приложения, библиотек, JVM, ОС и т. д.).
- Непредсказуемое потребление памяти прямыми отображёнными байтовыми буферами.
- Непредсказуемое потребление памяти из-за деталей реализации JVM (метаданные GC, metaspace, кэш кода, стеки потоков).
- Непредсказуемое потребление памяти другими процессами.
- Непредсказуемые проактивные политики сжатия памяти в ОС, которые сжимают мусор вместо того, чтобы его собирать.
- Непредсказуемое потребление памяти из-за фрагментации внутренней памяти JVM, которое медленно растёт в течение долгого времени.
- Непредсказуемое расположение памяти при запуске.
Найти статический размер кучи для всех возможных динамических обстоятельств невозможно. Чтобы справляться с такими ситуациями, приходится идти на дорогостоящий шаг — держать в резерве дополнительную память. Но и тогда любая статически заданная конфигурация будет неоптимальной для всех случаев, кроме основного режима работы приложения. Поскольку процесс настройки Serial GC хорошо известен, а у среды выполнения есть метрики, необходимые для управления этим процессом, решение перечисленных проблем состоит в том, чтобы научить JVM автоматически подстраивать кучу Java и тем самым минимизировать влияние GC на (хвостовую) задержку приложения.
Описание
Вслед за AHS для ZGC в этом документе предлагается политика Automatic Heap Sizing для Serial GC. Эта политика будет автоматически находить размер кучи, при котором влияние GC минимально, динамически адаптируясь к меняющимся обстоятельствам на сервере. Она выбирает размер кучи в пределах минимальной и максимальной границ, которые пользователи по-прежнему могут задавать как обычно параметрами командной строки -Xms и -Xmx. Однако при использовании Serial GC максимальный и минимальный размеры кучи по умолчанию будут изменены, чтобы по умолчанию дать автоматическому выбору размера кучи как можно больше свободы. Изменения будут следующими:
- Статические минимальный и начальный размеры кучи по умолчанию (
-Xms) меняются на 16 МБ. - Статический максимальный размер кучи по умолчанию (
-Xmx) меняется на 100 % доступной оперативной памяти компьютера или лимита CGroup. - Новый динамический максимальный размер кучи динамически адаптируется к изменениям доступной памяти компьютера. Им можно управлять с помощью нового управляемого (manageable) флага SoftMaxHeapSize.
- Агрессивностью GC по умолчанию, которая влияет на размер кучи, можно управлять флагом JVM
-XX: GCTimeRatio. Ожидается, что при меньших значениях потребуется куча большего размера, а при больших значениях куча сможет быть меньше. Значение по умолчанию пытается найти разумный баланс между MMU и объёмом занимаемой памяти. Флаг управляемый (manageable), то есть при желании его можно изменить во время выполнения.
Автоматический выбор размеров
Куча Java у Serial GC разделена на поколения: она состоит из пространств поколений Young и Old. Young, в свою очередь, делится на Eden и два пространства Survivor. У каждого из этих пространств своя роль. Эти роли таковы:
- Eden: используется для первоначального размещения данных в куче Java.
- Survivor: активное пространство Survivor хранит данные, которые ещё недостаточно стары, чтобы перейти в Tenured.
- Tenured: хранит долгоживущие объекты (данные).
Поскольку у каждого пространства в куче Java своя особая роль, из этого следует, что каждое пространство нужно настраивать так, чтобы оно наилучшим образом выполняло эту роль.
Автоматическая настройка должна учитывать общую глобальную нагрузку на память. В таблице ниже приведены условия и ожидаемые реакции.
| Нагрузка на память | Накладные расходы GC | Действие с размером кучи | |-----------------|----------------------|---------------------| | Высокая | Выше порога | Уменьшить | | Высокая | На уровне порога | Уменьшить | | Высокая | Ниже порога | Уменьшить | | Умеренная | Выше порога | Увеличить | | Умеренная | На уровне порога | Сохранить | | Умеренная | Ниже порога | Уменьшить | | Низкая | Выше порога | Увеличить | | Низкая | На уровне порога | Сохранить | | Низкая | Ниже порога | Уменьшить |
Как видно из таблицы, если глобальная нагрузка на память высокая, нужно возвращать память ОС. В остальных случаях решение увеличить, уменьшить или сохранить размер определяется накладными расходами GC. Проблема в том, что простое увеличение или уменьшение кучи Java может оставить приложению слишком мало памяти в одном из пространств кучи. Например, при уменьшении кучи пространства Survivor часто оказываются слишком маленькими. Следствием этого становится преждевременный перевод объектов в старшее поколение, а вместе с ним рост числа циклов Full GC. Больше циклов Full GC — больше накладных расходов GC. Часто правильный выбор размеров Eden, Survivor и Tenured в соответствии с их ролями не только снижает накладные расходы GC, но и уменьшает кучу. Поэтому более эффективная стратегия настройки — сосредоточиться не на общем размере кучи, а на потребностях Eden, Survivor и Tenured. При такой стратегии общий размер кучи — это сумма размеров отдельных пространств, а не пространства — доли от общего размера. В следующих разделах описаны метрики и стратегии настройки, нужные для каждого пространства.
Выбор размера Eden
В обычных условиях потоки-мутаторы выделяют объекты в Eden. Цикл сборки молодого поколения запускается, когда мутатору не удаётся выделить память в Eden и при этом не выполнены условия для полной сборки. Время, затраченное на цикл GC, называют накладными расходами GC. Общие накладные расходы GC вычисляются как отношение суммарного времени сборки мусора к общему времени работы приложения. На накладные расходы GC влияют два фактора: частота GC и длительность GC. Из сказанного выше можно заключить, что частота GC зависит от размера Eden и скорости выделения памяти. Следовательно, это два рычага, с помощью которых мы можем влиять на накладные расходы GC. Из этих двух рычагов напрямую мы управляем размером Eden. Поэтому здесь предлагается регулировать накладные расходы GC, изменяя размер Eden. Далее приведён пример соответствующих расчётов. Обратите внимание: предполагается, что изменение размера Eden никак не влияет на длительность GC. В действительности между частотой GC и длительностью пауз сложная связь. В рамках этого упражнения этой сложностью можно пренебречь, поскольку мы знаем, что для стабильной кучи время пауз у полностью копирующих сборщиков постоянно. Хотя Serial GC сочетает копирующую сборку и сборку на месте, обычно наблюдается, что время пауз сборки молодого поколения группируется вокруг медианного значения.
Let Eden = 500MB
Let Allocation Rate = 50MB/Second
GC Interval = Eden / Allocation Rate
= 500MB / 50MB/second
= 10 seconds
GC Frequency = 6 cycles / minute
Let window = time for 60 GC cycles to run.
= 10 minutes (for this example)
Let GC duration = average GC duration over the window
= 200 ms (for this example)
Total GC Time = window * GC Frequency * GC duration
= 10 minutes * 6 cycles / minute * 200 ms
= 12000 ms (12 seconds of GC pause time)
GC Overhead = Total GC Time / window
= 12 secs / 600 secs
= 2% GC overhead
GC Overhead is 2% or application throughput is 98%.
Если целевые накладные расходы GC составляют 1 %, можно рассчитать, что при прочих равных для достижения этой цели Eden нужно увеличить до 1000 МБ. В действительности снижение частоты GC обычно приводит к росту скорости выделения памяти. К сожалению, общее влияние этого эффекта непредсказуемо. Однако время пауз, как правило, группируется вокруг медианного значения. Также стоит отметить, что обычно требуется несколько циклов настройки, чтобы стабилизировать накладные расходы GC на приемлемом уровне.
Одна из проблем приведённых выше расчётов в том, что скорость выделения памяти считается постоянной. В действительности скорость выделения памяти зависит от нагрузки, которую внешние участники создают на приложение. Поскольку эта нагрузка колеблется, колеблется и скорость выделения памяти. Обычно эти колебания происходят в настолько узком диапазоне, что реагировать на них нет смысла. Чтобы размер кучи не менялся постоянно, потребуется какой-либо механизм сглаживания. Здесь рекомендуется задавать целевые накладные расходы GC с помощью GCTimeRatio.
Выбор размера Survivor
Задача Survivor — удерживать временные объекты с более долгим временем жизни. Его размер должен быть достаточно большим, чтобы вместить все недавно выделенные объекты, пережившие сборку. Число циклов GC, которые пережил объект, называется возрастом объекта. Когда возраст объекта достигает порога перевода в старшее поколение (tenuring threshold), объект копируется в Tenured. Максимальный порог перевода в JVM равен 15.
Таблица возрастов отслеживает объём данных каждого возраста. Эти данные помогают оценить, сколько данных останется в Survivor по окончании следующей сборки. Если объём данных, которые, как ожидается, переживут сборку, превышает размер области Survivor, порог перевода снижается до значения, при котором удерживаемый объём данных поместится в область Survivor. Такая ситуация называется преждевременным переводом в старшее поколение (premature promotion).
При преждевременном переводе данных происходит несколько вещей. Самое вредное из них то, что преждевременный перевод повышает частоту полных сборок, а это увеличивает накладные расходы GC. Рост преждевременного перевода делает более вероятным появление зомби. Зомби — это мёртвый объект в Tenured, который вынужденно считается живым. Зомби приводят к переводу ещё большего объёма данных, что, в свою очередь, ещё сильнее повышает частоту Full GC и, разумеется, ещё больше увеличивает накладные расходы GC.
Есть и другая форма преждевременного перевода — скрытый преждевременный перевод. Скрытый преждевременный перевод происходит, когда возраст временных данных достигает порога перевода. Обычно его причина — ускоренное старение из-за высокой частоты сборок молодого поколения. Хотя оптимальное решение — снизить скорость выделения памяти, здесь можно реализовать другое решение: обеспечить достаточный размер областей Survivor и Eden, чтобы объекты не переводились преждевременно. Для необходимого анализа можно использовать порог перевода и данные таблицы возрастов.
Данные в таблице возрастов представляют хвостовую часть кривой, описываемой гипотезой Weak Generational Hypothesis. Именно эта гипотеза обосновывает использование областей поколений. Она утверждает, что большинство объектов умирают молодыми. Таблица возрастов это подтверждает: почти всегда она показывает, что от возраста 0 освобождается много памяти, от возраста 1 — немного меньше, от возраста 2 — ещё меньше и так далее. Освобождение продолжается, пока на каком-то возрасте не остаются только долгоживущие данные. Именно в этом возрасте данные следует переводить в старшее поколение, так как это снижает затраты на копирование. Порог перевода следует установить равным этому возрасту: так освобождение кучи будет максимальным, а затраты на копирование — минимальными. Кроме того, это сводит к минимуму перевод временных объектов в область Tenured.
После краткого описания шагов по настройке Survivor следует более подробное объяснение.
- Вычислить желаемую заполненность Survivor, достаточно большую, чтобы свести к минимуму преждевременный перевод, но не больше.
- Вычислить порог перевода так, чтобы сбалансировать освобождение памяти от временных объектов и затраты на копирование долгоживущих данных.
Ниже приведён пример анализа таблицы возрастов, взятой из данных рабочей среды.
Max Tenuring Threshold = 15
Desired Survivor Occupancy = 67108864 Bytes
Number of Collection Cycles = 1384
Number of Collections that prematurely promoted = 856
Premature promotion rate = 62.8%
| Возраст | Средний объём (Б) | Максимальный объём (Б) | |-----|--------------------|----------------| | 1 | 48944511 | 189296194 | | 2 | 21333235 | 64342000 | | 3 | 6156835 | 53250208 | | 4 | 1090482 | 45645728 | | 5 | 269845 | 43009496 | | 6 | 65743 | 43009496 | | 7 | 11744 | 5973464 | | 8 | 2791 | 3807256 | | 9 | 0 | 0 | | 10 | 0 | 0 | | 11 | 0 | 0 | | 12 | 0 | 0 | | 13 | 0 | 0 | | 14 | 0 | 0 | | 15 | 0 | 0 |
Средняя общая заполненность Survivor составляет 77,875,176B, что больше желаемой заполненности Survivor в 67,108,864B. Это указывает на то, что проблема преждевременного перевода серьёзна. Следовательно, область Survivor нужно агрессивно увеличивать. В таких случаях рекомендуется увеличение в 2 раза. Цель — увеличивать размер Survivor до тех пор, пока преждевременный перевод не станет минимальным или не исчезнет. После этого желаемую заполненность Survivor можно вычислить, сложив значения 90-го перцентиля заполненности для каждого возраста. Обратите внимание, что по умолчанию желаемая заполненность Survivor составляет 50 % от общего размера области Survivor. Желаемую заполненность Survivor пользователь может настраивать, и иногда её устанавливают вплоть до 90 %. Эту настройку нужно не только соблюдать, но и знать, поскольку она влияет на расчёт размеров.
Значение порога перевода должно максимизировать освобождение памяти от временных объектов и при этом минимизировать общие затраты на копирование. Для этого порог перевода следует установить равным возрасту данных в Survivor, начиная с которого память почти или совсем перестаёт освобождаться. С точки зрения алгоритма это происходит, когда наклон кривой по точкам, представляющим объём данных, равен 0 или близок к 0. В приведённой выше таблице этому определению соответствует порог перевода 6. Проблема в данном случае в том, что данные в таблице возрастов искажены интенсивностью преждевременного перевода. Это мешает выполнить такой анализ. В этом случае порог перевода следует установить в MaxTenuringThreshold.
Выбор размера Tenured
Задача Tenured — предоставить область для хранения долгоживущих объектов. В обычных условиях в Tenured выделяются только объекты из Young, которые нужно перевести в старшее поколение. Эту работу выполняет поток GC. Объекты, слишком большие для выделения в Eden (или Survivor), выделяются непосредственно в Tenured потоком-мутатором. Однако это должно происходить редко. Цикл полной сборки запускается, когда определено, что в Tenured больше недостаточно памяти для перевода ожидаемого объёма данных. Отсюда можно заключить, что частота Full GC зависит от размера Tenured и скорости перевода. Более высокая скорость перевода означает более частые циклы Full GC, а это означает гораздо более высокие накладные расходы GC. Это одна из причин, по которым важно свести к минимуму перевод временных объектов.
Область Tenured должна быть достаточно большой, чтобы вместить набор живых данных и дополнительное место для будущих переводов. Размер набора живых данных определяется как объём данных, которые стабильно остаются живыми после Full GC.
Заполнение Tenured запускает полную сборку. Аналогично тому, что происходит в Eden, частоту Full GC задают скорость выделения памяти в Tenured и размер Tenured. Хотя большинство выделений в Tenured связано с переводом данных, мутаторы выделяют память непосредственно в Tenured, если выделяемый объект слишком велик, чтобы поместиться в Eden (когда Eden пуст). Однако это редкий случай, которым предлагаемый подход к выбору размера Tenured может пренебречь.
Если размер Young выбран достаточным, чтобы переводилось лишь минимальное количество временных объектов, единственный рычаг, напрямую доступный для снижения накладных расходов GC из-за полных сборок, — это размер Tenured. Поэтому размер Tenured следует выбирать так, чтобы вмещать набор живых данных и некоторое рабочее пространство. Это рабочее пространство, вероятно, должно составлять от 50 % до 100 % размера набора живых данных. Tenured должна расти по мере необходимости, чтобы снижать частоту Full GC, что должно снижать накладные расходы GC. Это помогает JVM достичь цели GCTimeRatio. Для начала должно быть достаточно недавнего медианного значения размера набора живых данных после Full GC.
Выбор размера кучи Java
Как видно из приведённого выше описания, общий размер кучи Java равен сумме её отдельных частей. Поскольку каждая из этих частей играет свою роль в работе GC, у каждой свои потребности в памяти, и каждая из этих потребностей описывается собственным набором метрик. Если наблюдаемые накладные расходы GC меньше цели GCTimeRatio, часть выделенной памяти кучи следует вернуть системе, используя описанные выше расчёты для перебалансировки областей Eden, Survivor и Tenured.
Сложный случай — когда наблюдаемые накладные расходы GC превышают GCTimeRatio, а системной памяти для увеличения кучи недостаточно. Сейчас мы считаем, что если область Tenured слишком мала для набора живых данных, будет выброшено исключение OutOfMemoryError и JVM завершит работу. Поэтому лучше увеличить Tenured, даже рискуя получить недостаточный размер Young, чтобы избежать выброса OOME.
Скорость расширения
Вопрос, который нужно решить здесь, — насколько быстро следует расширять (или сжимать) область памяти. Исходное предложение: область следует сразу расширять до размера, вычисленного эвристикой изменения размера. Основной принцип: пожар нужно тушить до того, как он станет слишком большим, чтобы с ним справиться.
Фиксация памяти кучи
Аналогично замечаниям в AHS ZGC JEP, фиксация (commit) и подкачка памяти могут вызывать проблемы с задержкой, если их выполняют потоки приложения. Сейчас, когда пользователь устанавливает -Xms и -Xmx в одно и то же значение, куча Serial GC фиксирует память заранее. Кроме того, если пользователь указывает параметр -XX:+AlwaysPreTouch, память кучи подкачивается до запуска main. Здесь есть компромисс между производительностью запуска и прогрева. Флаг AlwaysPreTouch по умолчанию отключён, что благоприятствует запуску, но снижает производительность прогрева. С предлагаемыми значениями по умолчанию пользователи не получат выгоды от фиксации памяти заранее или подкачки памяти кучи. Единственная оговорка: по умолчанию куча Serial GC будет небольшой, а значит, затраты при запуске могут быть незначительными.
Нехватка памяти
С описанными до сих пор приёмами процесс JVM может автоматически находить подходящий размер кучи при заданной по умолчанию нагрузке GC. Однако может оказаться, что одновременно работает несколько процессов, и если позволить JVM использовать столько памяти, сколько она хочет, памяти компьютера не хватит на всех.
Это предложение выступает за механизм, аналогичный предложенному для ZGC: он определяет, как должно изменяться значение размера кучи на сервере, у которого заканчивается память. Небольшая часть памяти компьютера считается резервом, который мы предпочитаем не использовать. Нагрузка GC в эвристиках автоматического выбора размера кучи масштабируется в зависимости от того, какая часть этого резерва памяти израсходована. Использование памяти компьютера отслеживается непрерывно, и по мере того как у компьютера заканчивается память, эвристики GC будут работать интенсивнее, чтобы уменьшить кучу. Прежде чем в куче закончится память, GC будет работать очень интенсивно. По мере расходования резерва памяти нехватка памяти нарастает сначала линейно, а затем экспоненциально.
Важно, что этот механизм даёт всем JVM единое представление о том, насколько критична нехватка памяти. Благодаря этому процессы, управляемые этими эвристиками, могут прийти к равновесию нагрузки GC, а не реагировать случайным образом на реакции других JVM без общей цели и стратегии.
При использовании macOS или Windows с включённым сжатием памяти непрерывно отслеживается соотношение сжатой и несжатой используемой памяти. Воспринимаемый размер резерва памяти масштабируется в соответствии с этим коэффициентом сжатия. Поэтому, когда ОС начинает сжимать больше памяти, GC будет работать интенсивнее, чтобы освобождать мусор и возвращать память ОС, снижая для неё необходимость сжатия.
Максимальный размер кучи динамически подстраивается так, чтобы быть не больше доступной на компьютере памяти плюс небольшой критический резерв памяти. Превышение этого порога приведёт к OutOfMemoryError, если ситуацию не удастся своевременно разрешить.
Выбор размеров поколений
При обновлении размера кучи нужно пересматривать распределение памяти между молодым и старым поколениями. В Serial GC между двумя поколениями есть жёсткая граница. Один из способов ослабить это ограничение — изменить порядок пространств поколений. Это должно позволить переустанавливать границу после полной сборки (Full collection).
Альтернативы
Давнее решение — устанавливать максимальный размер кучи Java в 25 % доступной памяти. Когда это значение по умолчанию было установлено, все развёртывания выполнялись на физических машинах. Предполагалось, что JVM придётся делить память. Сегодня многие, если не большинство, приложения используют контейнеризацию, где это допущение уже не выполняется. Поэтому в интересах приложения потреблять столько памяти, сколько нужно, пока она доступна. Было много обсуждений того, как настраивать JVM. Флаг MaxRAMPercentage — одна из альтернатив. Хотя он позволяет JVM задать максимальный размер кучи как долю памяти, выделенной контейнеру, это лишь догадка, точно так же как значение 25 % — лишь догадка. Другое решение, которое обсуждалось в сообществе, — увеличить максимальный размер кучи с 25 % до какого-то другого значения. И снова это значение — лишь догадка.
Опасность изменения значения 25 % в том, что все уже подстроились под это ограничение. Изменение повлияет на требования к памяти каждого приложения, которое работает без настройки. Это изменение несёт риск более частых срабатываний OOM Killer. Кроме того, при текущей реализации весьма вероятно, что развёртывания, которым не нужно больше 25 %, станут использовать больший объём реальной RAM. Это тоже повышает риск более частых срабатываний OOM Killer.
Тестирование
Это улучшение в первую очередь влияет на показатели производительности. Поэтому оно будет тщательно протестировано на самых разных нагрузках. Установленные критерии успеха будут основаны на способности сборщика адаптироваться к изменениям нагрузки и доступности памяти во время выполнения.
Риски и допущения
При изменении максимального размера кучи по умолчанию с 25 % доступной памяти на бо́льшую часть доступной памяти возникает риск, что новые эвристики будут использовать больше памяти, чем текущая реализация, и другим процессам не хватит памяти. Однако при политике размера кучи в 25 % и небольшом числе эвристик, пытающихся ограничить размер кучи, такой риск уже существует, когда на одном компьютере работают несколько JVM с размером кучи по умолчанию. Кроме того, при динамически обновляемом максимальном размере кучи весьма вероятно, что OOM удастся выбросить до превышения лимитов памяти компьютера.
Зависимости
Известных зависимостей нет.