1. 概述

当我们把 Java 应用跑在容器中时,通常会希望对 JVM 的内存参数做精细调整,以充分利用容器分配的资源。

本文将介绍如何在运行 Java 程序的容器中设置 JVM 参数。虽然以下内容适用于任何 JVM 配置项,但我们主要聚焦于常见的 \-Xmx\-Xms 参数。

此外,我们还会探讨在某些 Java 版本下容器化程序时常见的问题,以及如何在一些流行的容器化 Java 应用中设置这些参数。

2. Java 容器中的默认堆设置

JVM 在自动计算内存方面表现不错,能根据系统资源给出合理的默认值。

在过去,JVM 并不能识别容器分配的内存和 CPU 资源。为了解决这个问题,Java 10 引入了 +UseContainerSupport(默认启用),并将其向后移植到了 Java 8 的 8u191 版本。现在,JVM 可以基于容器分配的内存来计算堆大小。

不过,在某些场景下,我们可能还是需要手动调整这些默认值。

2.1. 自动内存计算机制

当我们不显式设置 \-Xmx\-Xms 参数时,JVM 会根据系统资源自动调整堆大小。

我们可以通过以下命令查看默认的堆大小:

$ java -XX:+PrintFlagsFinal -version | grep -Ei "maxheapsize|maxram"

输出示例:

openjdk version "15" 2020-09-15
OpenJDK Runtime Environment AdoptOpenJDK (build 15+36)
OpenJDK 64-Bit Server VM AdoptOpenJDK (build 15+36, mixed mode, sharing)
   size_t MaxHeapSize      = 4253024256      {product} {ergonomic}
 uint64_t MaxRAM           = 137438953472 {pd product} {default}
    uintx MaxRAMFraction   = 4               {product} {default}
   double MaxRAMPercentage = 25.000000       {product} {default}
   size_t SoftMaxHeapSize  = 4253024256   {manageable} {ergonomic}

从输出可以看到,JVM 默认使用系统总内存的约 25% 作为最大堆大小。例如,在 16GB 内存的机器上,默认分配了约 4GB。

为了验证这一点,我们可以编写一个简单的 Java 程序打印堆大小:

public static void main(String[] args) {
  int mb = 1024 * 1024;
  MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
  long xmx = memoryBean.getHeapMemoryUsage().getMax() / mb;
  long xms = memoryBean.getHeapMemoryUsage().getInit() / mb;
  LOGGER.log(Level.INFO, "Initial Memory (xms) : {0}mb", xms);
  LOGGER.log(Level.INFO, "Max Memory (xmx) : {0}mb", xmx);
}

将上述代码保存为 PrintXmxXms.java,放在一个空目录中。

在本地测试(假设已安装 JDK):

$ javac ./PrintXmxXms.java
$ java -cp . PrintXmxXms

输出示例(16GB 内存机器):

INFO: Initial Memory (xms) : 254mb
INFO: Max Memory (xmx) : 4,056mb

接下来,我们尝试在容器中运行这个程序。

2.2. JDK 8u191 之前的版本

创建一个 Dockerfile

FROM openjdk:8u92-jdk-alpine
COPY *.java /src/
RUN mkdir /app \
    && ls /src \
    && javac /src/PrintXmxXms.java -d /app
CMD ["sh", "-c", \
     "java -version \
      && java -cp /app PrintXmxXms"]

构建镜像:

$ docker build -t oldjava .

运行容器:

$ docker run --rm -ti oldjava
openjdk version "1.8.0_92-internal"
OpenJDK Runtime Environment (build 1.8.0_92-...)
OpenJDK 64-Bit Server VM (build 25.92-b14, mixed mode)
Initial Memory (xms) : 198mb
Max Memory (xmx) : 2814mb

再限制容器内存为 1GB:

$ docker run --rm -ti --memory=1g oldjava
openjdk version "1.8.0_92-internal"
OpenJDK Runtime Environment (build 1.8.0_92-...)
OpenJDK 64-Bit Server VM (build 25.92-b14, mixed mode)
Initial Memory (xms) : 198mb
Max Memory (xmx) : 2814mb

可以看到,旧版本 JVM 无法识别容器内存限制,依旧使用宿主机的内存进行计算。

2.3. JDK 8u191 及之后版本

使用更新的镜像:

FROM openjdk:8-jdk-alpine

构建并运行:

$ docker build -t newjava .
$ docker run --rm -ti newjava
openjdk version "1.8.0_212"
OpenJDK Runtime Environment (IcedTea 3.12.0) (Alpine 8.212.04-r0)
OpenJDK 64-Bit Server VM (build 25.212-b04, mixed mode)
Initial Memory (xms) : 198mb
Max Memory (xmx) : 2814mb

限制容器内存为 1GB:

$ docker run --rm -ti --memory=1g newjava
openjdk version "1.8.0_212"
OpenJDK Runtime Environment (IcedTea 3.12.0) (Alpine 8.212.04-r0)
OpenJDK 64-Bit Server VM (build 25.212-b04, mixed mode)
Initial Memory (xms) : 16mb
Max Memory (xmx) : 247mb

新版本 JVM 能正确识别容器内存限制,自动调整堆大小。

3. 流行基础镜像中的内存设置

3.1. OpenJDK 和 AdoptOpenJDK

推荐使用环境变量(如 JAVA_OPTS)来设置 JVM 参数,而不是硬编码在命令行中。

FROM openjdk:8u92-jdk-alpine
COPY src/ /src/
RUN mkdir /app \
 && ls /src \
 && javac /src/com/baeldung/docker/printxmxxms/PrintXmxXms.java \
    -d /app
ENV JAVA_OPTS=""
CMD java $JAVA_OPTS -cp /app \ 
    com.baeldung.docker.printxmxxms.PrintXmxXms

构建镜像:

$ docker build -t openjdk-java .

运行时设置内存:

$ docker run --rm -ti -e JAVA_OPTS="-Xms50M -Xmx50M" openjdk-java
INFO: Initial Memory (xms) : 50mb
INFO: Max Memory (xmx) : 48mb

⚠️ 注意:\-Xmx 和 JVM 报告的最大内存略有差异,因为前者包含了堆、GC survivor 区等。

3.2. Tomcat 9

Tomcat 使用自己的启动脚本,JVM 参数需通过 CATALINA_OPTS 环境变量设置。

FROM tomcat:9.0
COPY ./target/*.war /usr/local/tomcat/webapps/ROOT.war
ENV CATALINA_OPTS="-Xms1G -Xmx1G"

构建并运行:

$ docker build -t tomcat .
$ docker run --name tomcat -d -p 8080:8080 \
  -e CATALINA_OPTS="-Xms512M -Xmx512M" tomcat

验证参数是否生效:

$ docker exec -ti tomcat jps -lv
1 org.apache.catalina.startup.Bootstrap <other options...> -Xms512M -Xmx512M

4. 使用构建插件

Maven 和 Gradle 提供插件可直接生成容器镜像,无需 Dockerfile。

4.1. Spring Boot

从 Spring Boot 2.3 开始,支持通过插件构建容器镜像。

<plugin>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-maven-plugin</artifactId>
  <configuration>
    <image>
      <name>heapsizing-demo</name>
    </image>
  </configuration>
</plugin>

构建:

$ ./mvnw clean spring-boot:build-image

运行时覆盖参数:

$ docker run --rm -ti -p 8080:8080 \
  -e JAVA_TOOL_OPTIONS="-Xms20M -Xmx20M" \
  --memory=1024M heapsizing-demo:0.0.1-SNAPSHOT

输出示例:

Picked up JAVA_TOOL_OPTIONS: -Xms20M -Xmx20M

4.2. Google JIB

Google JIB 同样支持无 Dockerfile 构建容器镜像。

<plugin>
  <groupId>com.google.cloud.tools</groupId>
  <artifactId>jib-maven-plugin</artifactId>
  <version>2.7.1</version>
  <configuration>
    <to>
      <image>heapsizing-demo-jib</image>
    </to>
  </configuration>
</plugin>

构建:

$ mvn clean install && mvn jib:dockerBuild

运行:

$ docker run --rm -ti -p 8080:8080 \
-e JAVA_TOOL_OPTIONS="-Xms50M -Xmx50M" heapsizing-demo-jib

输出示例:

Picked up JAVA_TOOL_OPTIONS: -Xms50M -Xmx50M
INFO: Initial Memory (xms) : 50mb
INFO: Max Memory (xmx) : 50mb

5. 总结

本文介绍了如何在 Docker 容器中合理设置 Java 堆大小,尤其是在使用较新版本 JVM 时的自动识别机制。我们还探讨了在自定义镜像和已有容器中设置 \-Xms\-Xmx 的最佳实践,以及如何利用构建工具简化容器化流程。

✅ 最后提醒:务必使用支持容器感知的 JVM 版本(如 Java 8u191+),否则容易踩坑。

源码见:GitHub 示例代码


原始标题:How To Configure Java Heap Size Inside a Docker Container | Baeldung