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 示例代码