一、概述
當(dāng)我們在容器中運(yùn)行 Java 應(yīng)用程序時(shí),可能希望對其進(jìn)行調(diào)整參數(shù)以充分利用資源。
在本教程中,我們將了解如何在運(yùn)行 Java 進(jìn)程的容器中設(shè)置 JVM 參數(shù)。本文將重點(diǎn)關(guān)注常見的 -Xmx 和-Xms 標(biāo)志。
另外,我們還將研究使用某些 Java 版本運(yùn)行的程序容器化的常見問題,以及如何在常見的容器化 Java 應(yīng)用程序時(shí)設(shè)置自定義標(biāo)志。
二、Java 容器中的默認(rèn)堆設(shè)置
過去,JVM 不知道分配給容器的內(nèi)存和 CPU。
Java 10 引入了一個新設(shè)置:+UseContainerSupport(默認(rèn)啟用)來修復(fù) 這個問題[3],并在 8u191[4] 中將修復(fù)反向移植到 Java 8 。
現(xiàn)在 JVM 可以根據(jù)分配給容器的內(nèi)存計(jì)算其內(nèi)存。
1. 自動內(nèi)存計(jì)算
當(dāng)不設(shè)置-Xmx和-Xmx參數(shù)時(shí),JVM 會根據(jù)系統(tǒng)規(guī)格來調(diào)整堆大小。
看看堆大?。?/p>
$ java -XX:+PrintFlagsFinal -version | grep -Ei “maxheapsize|maxram”
輸出結(jié)果如下:
openjdk version “15” 2020-09-15OpenJDK 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 將其堆大小設(shè)置為可用 RAM 的大約 25%。在這個例子中,在一個 16GB 的系統(tǒng)上分配了 4GB。
出于測試目的,創(chuàng)建一個文件,名為PrintXmxXms.java,內(nèi)容是以 MB 為單位打印堆大小,代碼內(nèi)容如下:
import java.lang.management.ManagementFactory;import java.lang.management.MemoryMXBean;public class PrintXmxXms { 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; System.out.println(“Initial Memory (xms) : ” + xms + “mb”); System.out.println(“Max Memory (xmx) : ” + xmx + “mb”); }}
假設(shè)已經(jīng)安裝了 JDK,可以編譯程序并運(yùn)行:
$ javac ./PrintXmxXms.java$ java -cp . PrintXmxXms
在 16Gb RAM 的主機(jī)上,輸出結(jié)果為:
INFO: Initial Memory (xms) : 254mbINFO: Max Memory (xmx) : 4056mb
下面,在容器中嘗試一下。
FROM openjdk:8u92-jdk-alpineCOPY *.java /src/RUN mkdir /app && ls /src && javac /src/PrintXmxXms.java -d /appCMD [“sh”, “-c”, “java -version && java -cp /app PrintXmxXms”]
這里使用的容器使用舊版本的 Java 8,它早于更新版本中可用的容器支持。構(gòu)建鏡像:
$ sudo docker build -t oldjava .
Dockerfile 中的 CMD 行是運(yùn)行容器時(shí)默認(rèn)執(zhí)行的進(jìn)程。由于沒有提供-Xmx或-XmsJVM 標(biāo)志,內(nèi)存設(shè)置將是默認(rèn)設(shè)置。
運(yùn)行容器:
$ sudo docker run –rm -ti oldjavaopenjdk 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) : 198mbMax Memory (xmx) : 2814mb
現(xiàn)在使用–memory=1g命令行標(biāo)志將容器內(nèi)存限制為 1GB:
$ sudo docker run –rm -ti –memory=1g oldjavaopenjdk 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) : 198mbMax Memory (xmx) : 2814mb
輸出完全相同。這證明舊的 JVM 沒有遵守容器內(nèi)存限制。
FROM openjdk:8-jdk-alpine
然后再次做測試:
$ sudo docker build -t newjava .$ sudo docker run –rm -ti newjavaopenjdk 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) : 198mbMax Memory (xmx) : 2814mb
如上輸出,使用整個 docker 主機(jī)內(nèi)存來計(jì)算 JVM 堆大小。但是,如果為容器分配 1GB 的 RAM:
$ sudo docker run –rm -ti –memory=1g newjavaopenjdk 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) : 16mbMax Memory (xmx) : 247mb
這一次,JVM 根據(jù)容器可用的 1GB RAM 計(jì)算堆大小。
現(xiàn)在我們了解了 JVM 如何計(jì)算其默認(rèn)值以及為什么需要一個最新的 JVM 來獲得正確的默認(rèn)值。
三、常用的基礎(chǔ)鏡像中內(nèi)存設(shè)置
FROM openjdk:8u92-jdk-alpineCOPY *.java /src/RUN mkdir /app && ls /src && javac /src/PrintXmxXms.java -d /appENV JAVA_OPTS=””CMD [“sh”, “-c”, “java -version && java $JAVA_OPTS -cp /app PrintXmxXms”]
構(gòu)建鏡像:
$ sudo docker build -t openjdk-java .
通過指定JAVA_OPTS環(huán)境變量在運(yùn)行時(shí)選擇內(nèi)存設(shè)置:
$ sudo docker run –rm -ti -e JAVA_OPTS=”-Xms50M -Xmx50M” openjdk-javaopenjdk version “1.8.0_92-internal”OpenJDK Runtime Environment (build 1.8.0_92-internal-alpine-r1-b14)OpenJDK 64-Bit Server VM (build 25.92-b14, mixed mode)Initial Memory (xms) : 50mbMax Memory (xmx) : 48mb
注意:-Xmx 參數(shù)和 JVM 報(bào)告的 Max memory 之間存在細(xì)微差別。這是因?yàn)?Xmx 設(shè)置了內(nèi)存分配池的最大大小,其中包括堆、垃圾收集器的幸存者空間和其他池。
2. Tomcat 9
Tomcat 9 容器有自己的啟動腳本,因此要設(shè)置 JVM 參數(shù),需要使用這些腳本。
bin/catalina.sh 腳本要求在環(huán)境變量 CATALINA_OPTS 中設(shè)置內(nèi)存參數(shù)。
首先需要 創(chuàng)建一個 war 包 部署到 Tomcat。
然后,我們使用下面的Dockerfile 對其進(jìn)行容器化,并在其中聲明CATALINA_OPTS環(huán)境變量:
FROM tomcat:9.0COPY ./target/*.war /usr/local/tomcat/webapps/ROOT.warENV CATALINA_OPTS=”-Xms1G -Xmx1G”
然后構(gòu)建容器鏡像并運(yùn)行它:
$ sudo docker build -t tomcat .$ sudo docker run –name tomcat -d -p 8080:8080 -e CATALINA_OPTS=”-Xms512M -Xmx512M” tomcat
注意:運(yùn)行時(shí),將新值傳遞給 CATALINA_OPTS 。如果不提供這個值,會使用 Dockerfile 的第 3 行給出的默認(rèn)值。
可以檢查應(yīng)用的運(yùn)行時(shí)參數(shù)并驗(yàn)證選項(xiàng)-Xmx和-Xms是否存在:
$ sudo docker exec -ti tomcat jps -lv1 org.apache.catalina.startup.Bootstrap -Xms512M -Xmx512M
四、使用構(gòu)建插件
Maven 和 Gradle 提供的插件允許我們在沒有Dockerfile的情況下創(chuàng)建容器鏡像。生成的鏡像通??梢栽谶\(yùn)行時(shí)通過環(huán)境變量進(jìn)行參數(shù)化。
下面看幾個例子。
使用 Maven 時(shí),將它們添加到 spring-boot-maven-plugin 中的 塊中:
com.baeldung.docker heapsizing-demo 0.0.1-SNAPSHOT org.springframework.boot spring-boot-maven-plugin heapsizing-demo
要構(gòu)建項(xiàng)目,運(yùn)行下面的命令:
$ ./mvnw clean spring-boot:build-image
這將產(chǎn)生一個名為 : 的鏡像。在這個例子中產(chǎn)生的鏡像名為:demo-app:0.0.1-SNAPSHOT。Spring Boot 底層使用 Cloud Native Buildpacks 作為容器化技術(shù)。
該插件對 JVM 的內(nèi)存設(shè)置進(jìn)行硬編碼。但是,我們?nèi)匀豢梢酝ㄟ^設(shè)置環(huán)境變量JAVA_OPTS 或 JAVA_TOOL_OPTIONS 來覆蓋:
$ sudo docker run –rm -ti -p 8080:8080 -e JAVA_TOOL_OPTIONS=”-Xms20M -Xmx20M” –memory=1024M heapsizing-demo:0.0.1-SNAPSHOT
輸出將與此類似:
Setting Active Processor Count to 8Calculated JVM Memory Configuration: […][…]Picked up JAVA_TOOL_OPTIONS: -Xms20M -Xmx20M[…]
我們可以在任何能夠生成可執(zhí)行 jar 文件的 Java 框架中使用 Google JIB Maven 插件。例如,可以在 Spring Boot 應(yīng)用程序中使用它來代替spring-boot-maven插件來生成容器鏡像:
com.google.cloud.tools jib-maven-plugin 2.7.1 heapsizing-demo-jib
鏡像是使用 mvn jib:dockerBuild 命令構(gòu)建的:
$ mvn clean install && mvn jib:dockerBuild
嘗試運(yùn)行:
$ sudo docker run –rm -ti -p 8080:8080 -e JAVA_TOOL_OPTIONS=”-Xms50M -Xmx50M” heapsizing-demo-jibPicked up JAVA_TOOL_OPTIONS: -Xms50M -Xmx50M[…]2021-01-25 17:46:44.070 INFO 1 — [ main] c.baeldung.docker.XmxXmsDemoApplication : Started XmxXmsDemoApplication in 1.666 seconds (JVM running for 2.104)2021-01-25 17:46:44.075 INFO 1 — [ main] c.baeldung.docker.XmxXmsDemoApplication : Initial Memory (xms) : 50mb2021-01-25 17:46:44.075 INFO 1 — [ main] c.baeldung.docker.XmxXmsDemoApplication : Max Memory (xmx) : 50mb
五、結(jié)論
在本文中,我們介紹了需要使用最新的 JVM 來獲取在容器中默認(rèn)內(nèi)存設(shè)置。
然后,研究了在自定義容器映像中設(shè)置 -Xms 和 -Xmx 的最佳實(shí)踐, 以及如何使用現(xiàn)有 Java 應(yīng)用程序容器在其中設(shè)置 JVM 選項(xiàng)。
最后,我們看到了如何利用構(gòu)建工具來管理 Java 應(yīng)用程序的容器