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

JEP 346: Promptly Return Unused Committed Memory from G1

Своевременный возврат неиспользуемой выделенной памяти из G1

AuthorsRodrigo Bruno, Thomas Schatzl, Ruslan Synytsky
ОтветственныйThomas Schatzl
ТипFeature
ОбластьImplementation
СтатусClosed / Delivered
Выпуск12
Компонентhotspot / gc
Обсуждениеhotspot dash gc dash dev at openjdk dot java dot net
ТрудоёмкостьM
ДлительностьS
РецензентыMikael Vidstedt, Stefan Johansson
ОдобренVladimir Kozlov
Создан2018/05/30 14:23
Обновлён2023/10/09 20:39
Задача8204089

Аннотация

Доработать сборщик мусора G1 так, чтобы в периоды простоя он автоматически возвращал память кучи Java операционной системе.

Что не является целью

  • Совместное использование выделенных (committed), но пустых страниц несколькими процессами Java. Память должна возвращаться операционной системе (с отменой выделения, uncommit).

  • Процесс возврата памяти не обязан экономно расходовать ресурсы процессора и не обязан быть мгновенным.

  • Использование для возврата памяти других способов, кроме имеющейся отмены выделения памяти (uncommit).

  • Поддержка других сборщиков мусора, кроме G1.

Критерии успеха

G1 должен освобождать неиспользуемую память кучи Java в течение разумного времени, если активность приложения очень низкая.

Мотивация

Сейчас сборщик мусора G1 может возвращать выделенную память кучи Java операционной системе несвоевременно. G1 возвращает память из кучи Java только при полной сборке мусора (full GC) или во время конкурентного цикла. Поскольку G1 всеми силами старается полностью избегать полных сборок мусора, а конкурентный цикл запускает только исходя из заполненности кучи Java и активности выделения памяти, во многих случаях он не вернёт память кучи Java, если его не вынудить к этому извне.

Такое поведение особенно невыгодно в контейнерных средах, где ресурсы оплачиваются по мере использования. Даже в периоды, когда VM из-за простоя использует лишь часть выделенных ей ресурсов памяти, G1 удерживает всю кучу Java. В результате клиенты всё время платят за все ресурсы, а облачные провайдеры не могут полностью задействовать своё оборудование.

Если бы VM умела обнаруживать периоды недоиспользования кучи Java (периоды «простоя») и автоматически сокращать на это время использование кучи, выиграли бы и те и другие.

Shenandoah и сборщик GenCon в OpenJ9 уже предоставляют похожую функциональность.

Испытания прототипа в работе Bruno et al., раздел 5.5, показывают, что при реальной нагрузке сервера Tomcat, который днём обслуживает HTTP-запросы, а ночью в основном простаивает, это решение позволяет сократить объём памяти, выделенной Java VM, на 85%.

Описание

Чтобы вернуть операционной системе как можно больше памяти, G1 во время бездействия приложения будет периодически пытаться продолжить или запустить конкурентный цикл, чтобы определить общее использование кучи Java. В результате он будет автоматически возвращать неиспользуемые части кучи Java операционной системе. При желании, по выбору пользователя, можно выполнять полную сборку мусора, чтобы вернуть как можно больше памяти.

Приложение считается бездействующим, и G1 запускает периодическую сборку мусора, если выполнены оба условия:

  • С момента предыдущей паузы сборки мусора прошло более G1PeriodicGCInterval миллисекунд, и в данный момент не выполняется конкурентный цикл. Нулевое значение означает, что периодические сборки мусора для своевременного освобождения памяти отключены.

  • Средняя загрузка системы за одну минуту, возвращаемая вызовом getloadavg() в хост-системе (например, в контейнере), где работает JVM, ниже G1PeriodicGCSystemLoadThreshold. Это условие не учитывается, если G1PeriodicGCSystemLoadThreshold равно нулю.

Если хотя бы одно из этих условий не выполнено, текущая намеченная периодическая сборка мусора отменяется. Вопрос о периодической сборке мусора снова рассматривается, когда в следующий раз пройдёт время G1PeriodicGCInterval.

Тип периодической сборки мусора определяется значением параметра G1PeriodicGCInvokesConcurrent: если он установлен, G1 продолжает или запускает конкурентный цикл, иначе G1 выполняет полную сборку мусора. В конце сборки любого типа G1 корректирует текущий размер кучи Java и может при этом вернуть память операционной системе. Новый размер кучи Java определяется существующими настройками корректировки размера кучи Java, в том числе (но не только) MinHeapFreeRatio, MaxHeapFreeRatio и настройками минимального и максимального размера кучи.

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

Любая сборка мусора, запущенная этим механизмом, помечается причиной G1 Periodic Collection. Пример того, как может выглядеть такой журнал:

(1) [6.084s][debug][gc,periodic ] Checking for periodic GC.
    [6.086s][info ][gc          ] GC(13) Pause Young (Concurrent Start) (G1 Periodic Collection) 37M->36M(78M) 1.786ms
(2) [9.087s][debug][gc,periodic ] Checking for periodic GC.
    [9.088s][info ][gc          ] GC(15) Pause Young (Prepare Mixed) (G1 Periodic Collection) 9M->9M(32M) 0.722ms
(3) [12.089s][debug][gc,periodic ] Checking for periodic GC.
    [12.091s][info ][gc          ] GC(16) Pause Young (Mixed) (G1 Periodic Collection) 9M->5M(32M) 1.776ms
(4) [15.092s][debug][gc,periodic ] Checking for periodic GC.
    [15.097s][info ][gc          ] GC(17) Pause Young (Mixed) (G1 Periodic Collection) 5M->1M(32M) 4.142ms
(5) [18.098s][debug][gc,periodic ] Checking for periodic GC.
    [18.100s][info ][gc          ] GC(18) Pause Young (Concurrent Start) (G1 Periodic Collection) 1M->1M(32M) 1.685ms
(6) [21.101s][debug][gc,periodic ] Checking for periodic GC.
    [21.102s][info ][gc          ] GC(20) Pause Young (Concurrent Start) (G1 Periodic Collection) 1M->1M(32M) 0.868ms
(7) [24.104s][debug][gc,periodic ] Checking for periodic GC.
    [24.104s][info ][gc          ] GC(22) Pause Young (Concurrent Start) (G1 Periodic Collection) 1M->1M(32M) 0.778ms

В приведённом примере, запущенном с G1PeriodicGCInterval равным 3000 мс, на шаге (1) после некоторого бездействия приложения G1 начинает конкурентный цикл, на что указывают (Concurrent Start) и (G1 Periodic Collection). Этот конкурентный цикл сначала возвращает часть памяти, что видно по уменьшению значений ёмкости (78M) и (32M) с (1) по (2). В интервале с (2) по (4) запускаются новые периодические сборки, на этот раз со смешанной сборкой для уплотнения кучи. Следующие периодические сборки мусора с (5) по (7) начинают конкурентный цикл, поскольку политика G1 определяет, что в этот момент в старом поколении недостаточно мусора для начала фазы смешанных сборок. В этом случае периодические сборки мусора с (5) по (7) не будут дальше уменьшать кучу, поскольку минимальный размер кучи уже достигнут.

Изменения в живости объектов во время бездействия приложения (например, из-за истечения срока мягких ссылок) могут вызвать дальнейшее сокращение выделенной кучи Java в этот период простоя.

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

Похожей функциональности можно добиться и извне VM, например с помощью инструмента jcmd или кода, внедрённого в VM. У этого есть скрытые издержки: если проверка выполняется задачей по расписанию cron, то при сотнях или тысячах контейнеров на одном узле уплотнение кучи может выполняться многими из этих контейнеров одновременно, что приводит к очень большим всплескам загрузки процессора на хосте.

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

Рассматриваемый сценарий — своевременное уменьшение кучи Java — считается достаточно распространённым, чтобы оправдать специальную поддержку в VM.

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

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

Когда эта возможность включена, VM выполняет эти периодические сборки при указанных выше условиях независимо от других параметров. Например, VM могла бы предполагать, что пользователь задаёт -Xms равным -Xmx и другие параметры (или их сочетания), чтобы получить минимальные и стабильные паузы сборки мусора. Ради единообразия этого не будет.

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