В большинстве случаев все серверные 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)

В общем будьте внимательны и изучайте как правильно работать с Вашими контейнерами.