В большинстве случаев все серверные Java приложения в данный момент запускаются в контейнерах. Для того чтобы было проще управлять в том числе и выделенными ресурсами. Но JVM не всегда ведет себя в контейнере так как ожидается и не всегда удается ее правильно настроить. Дальше посмотрим один из примеров настройки JVM для работы в контейнере.
Представим что мы запускаем наше приложение в Docker / Podman, для которого выделено 12 ГБ памяти, в контейнере собранным из Dockerfile ниже
FROM maven:3.9.9-amazoncorretto-21-al2023 AS builder
ARG BUILD_DIR=/build
WORKDIR $BUILD_DIR
COPY . .
RUN mkdir -p $BUILD_DIR/ && mvn package
FROM eclipse-temurin:21.0.6_7-jre-ubi9-minimal AS app
ARG APP_DIR=/app
WORKDIR $APP_DIR
COPY --from=builder /build/target/*.jar $APP_DIR/
CMD ["java","-jar","app.jar"]
Запускаем его через docker run и смотрим на какой объем памяти рассчитывает Java. Для этого в терминале контейнера можем выполнить команду:
java -XshowSettings:vm -version
И увидим примерно следующее:
VM settings:
Max. Heap Size (Estimated): 2.71G
Using VM: OpenJDK 64-Bit Server VM
openjdk version "21.0.6" 2025-01-21 LTS
OpenJDK Runtime Environment Temurin-21.0.6+7 (build 21.0.6+7-LTS)
OpenJDK 64-Bit Server VM Temurin-21.0.6+7 (build 21.0.6+7-LTS, mixed mode, sharing)
Как видим Java рассчитывает на 2.71 ГБ памяти - четверть от выделенной всему Docker / Podman.
Как правило нам хочется ограничить память потребляемую нашим контейнером, потому что он у нас не один работает и надо как-то стимулировать сборщик мусора чтобы он не дожидался пока на нашей машине начнет заканчиваться память. Ведь мы же можем запускать десяток контейнеров с приложениями и если в каждом из них Java будет считать что ей доступна четверть памяти, то в суммме наши 10 контейнеров будут претендовать на 2.71 * 10 = 27 ГБ памяти, что больше чем 2 раза чем у нас доступно.
Как будем ограничивать?
Попытка номер 1
Ну для начала вспомним что где-то со времен последних версий Java 8 у JVM появился параметр -XX:+UseContainerSupport, который позволяет Java автоматически устанавливать размер памяти в зависимости от размера памяти доступной контейнеру, и вроде как он даже включен по умолчанию.
Что же проверим. Для этого добавим нашему контейнеру лимитов по памяти. Для удобства я буду использовать Compose.
services:
app:
build:
context: .
dockerfile: Dockerfile
container_name: my_app
deploy:
resources:
limits:
memory: 512M
reservations:
memory: 256M
Попробуем снова запустить контейнер и посмотрим повляло ли как-то ограничение на размер памяти на которую рассчитывает JVM.
Для этого снова отправляемся в наш контейнер и выполняем команду: java -XshowSettings:vm -version
И видим что наш eclipse-temurin никак не отреагировал на ограничение:
VM settings:
Max. Heap Size (Estimated): 2.71G
Using VM: OpenJDK 64-Bit Server VM
openjdk version "21.0.6" 2025-01-21 LTS
OpenJDK Runtime Environment Temurin-21.0.6+7 (build 21.0.6+7-LTS)
OpenJDK 64-Bit Server VM Temurin-21.0.6+7 (build 21.0.6+7-LTS, mixed mode, sharing)
При этом если мы посмотрим что нам скажет docker (podman) stats:
ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS CPU TIME AVG CPU %
e6924c2f421c app 2.80% 300.9MB / 536.9MB 56.04% 555.4kB / 113.8kB 127kB / 2.497MB 41 30.991232s 2.80%
Что лимит памяти для контейнера установлен корректно.
Такое несоотвестве может нам грозить нестабильной работой контейнера, если потребление памяти JVM будет приближаться к лимиту контейнера. Потому что JVM будет считать что у нее есть еще память, а контейнер уже не сможет предоставить ее.
Попытка номер 2
Теперь предположим что параметр -XX:+UseContainerSupport просто не включен по умолчанию и включим его вручную.
Для этого добавим в наш compose значение для переменной JAVA_TOOL_OPTIONS, которая будет передаваться в JVM при запуске.
services:
app:
build:
context: .
dockerfile: Dockerfile
container_name: my_app
deploy:
resources:
limits:
memory: 512M
reservations:
memory: 256M
environment:
JAVA_TOOL_OPTIONS: "-XX:+UseContainerSupport"
Снова посмотрим результат вывода java -XshowSettings:vm -version
Picked up JAVA_TOOL_OPTIONS: -XX:+UseContainerSupport
VM settings:
Max. Heap Size (Estimated): 2.71G
Using VM: OpenJDK 64-Bit Server VM
openjdk version "21.0.6" 2025-01-21 LTS
OpenJDK Runtime Environment Temurin-21.0.6+7 (build 21.0.6+7-LTS)
OpenJDK 64-Bit Server VM Temurin-21.0.6+7 (build 21.0.6+7-LTS, mixed mode, sharing)
Видим что JVM увидела переданный флаг но по прежнему никак на него не реагирует и рассчитывает на больший объем памяти чем доступно в контейнере.
Попытка номер 3
Теперь попробуем явно ограничить память JVM с помощью флага -Xmx. Передадим его также в переменную JAVA_TOOL_OPTIONS.
services:
app:
build:
context: .
dockerfile: Dockerfile
container_name: my_app
deploy:
resources:
limits:
memory: 512M
reservations:
memory: 256M
environment:
JAVA_TOOL_OPTIONS: "-XX:+UseContainerSupport -Xmx500M"
Запускаем и снова смотрим результат вывода java -XshowSettings:vm -version
Picked up JAVA_TOOL_OPTIONS: -XX:+UseContainerSupport -Xmx500M
VM settings:
Max. Heap Size (Estimated): 500.00M
Using VM: OpenJDK 64-Bit Server VM
openjdk version "21.0.6" 2025-01-21 LTS
OpenJDK Runtime Environment Temurin-21.0.6+7 (build 21.0.6+7-LTS)
OpenJDK 64-Bit Server VM Temurin-21.0.6+7 (build 21.0.6+7-LTS, mixed mode, sharing)
И видим что JVM наконец увидело наше ограничение и теперь рассчитывает на 500Мб памяти.
Всегда ли так?
Рассмотренные пример с образом eclipse-temurin не говорит о том что все контейнеры с JRE ведут себя одинаково.
Так например образы ibm-semeru корректно обрабатывают параметр -XX:+UseContainerSupport и нам не обязательно передавать Xmx флаг чтобы JVM увидела ограничение на контейнере.
Используя следующий Dockerfile:
FROM maven:3.9.9-amazoncorretto-21-al2023 AS builder
ARG BUILD_DIR=/build
WORKDIR $BUILD_DIR
COPY . .
RUN mkdir -p $BUILD_DIR/ && mvn package
FROM ibm-semeru-runtimes:open-21.0.8_9-jre-noble AS app
ARG APP_DIR=/app
WORKDIR $APP_DIR
COPY --from=builder /build/target/*.jar $APP_DIR/
CMD ["java","-jar","app.jar"]
Вместе с параметрами compose:
services:
app:
build:
context: .
dockerfile: Dockerfile
container_name: my_app
deploy:
resources:
limits:
memory: 512M
reservations:
memory: 256M
environment:
JAVA_TOOL_OPTIONS: "-XX:+UseContainerSupport"
Выполнив java -XshowSettings:vm -version мы видим что JVM сразу ограничивает размер кучи с учетом ограничений контейнера.
VM settings:
Max. Heap Size (Estimated): 256.00M
Using VM: Eclipse OpenJ9 VM
openjdk version "21.0.8" 2025-07-15 LTS
IBM Semeru Runtime Open Edition 21.0.8.0 (build 21.0.8+9-LTS)
Eclipse OpenJ9 VM 21.0.8.0 (build openj9-0.53.0, JRE 21 Linux amd64-64-Bit Compressed References 20250715_547 (JIT enabled, AOT enabled)
OpenJ9 - 017819f167
OMR - 266a8c6f5
JCL - d5f1e70d135 based on jdk-21.0.8+9)
В общем будьте внимательны и изучайте как правильно работать с Вашими контейнерами.