如何在Docker容器中配置Java堆大小

    1.概述

    當我們在容器中運行Java時,我們可能希望對其進行調整以充分利用可用資源。

    在本教程中,我們將看到如何在運行Java進程的容器中設置JVM參數。儘管以下內容適用於任何JVM設置,但我們將重點介紹常見的-Xmx-Xms標誌。

    我們還將研究將某些版本的Java運行的程序容器化的常見問題,以及如何在一些流行的容器化Java應用程序中設置標誌。

    2. Java容器中的默認堆設置

    JVM非常擅長確定適當的默認內存設置。

    過去, JVM並不知道分配給容器的內存和CPU 。因此,Java 10引入了一個新設置: +UseContainerSupport (默認情況下啟用)以解決根本原因,並且開發人員將修復程序反向移植到8u191中的Java 8。 JVM現在基於分配給容器的內存來計算其內存。

    但是,在某些應用程序中,我們仍可能希望更改其默認設置。

    2.1。自動內存計算

    當我們不設置-Xmx-Xmx參數時,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將其堆大小設置為大約可用RAM的25%。在此示例中,它在具有16GB的系統上分配了4GB。

    為了進行測試,我們創建一個程序來打印堆大小(以兆字節為單位):

    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,則可以在主機上對其進行測試。在Linux系統中,我們可以編譯程序並從該目錄上打開的終端上運行該程序:

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

    在具有16Gb RAM的系統上,輸出為:

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

    現在,讓我們在一些容器中嘗試一下。

    2.2。在JDK 8u191之前

    讓我們在包含我們的Java程序的文件夾中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"]

    在這裡,我們使用的容器使用Java 8的較早版本,該容器早於最新版本中可用的容器支持。讓我們來建立它的形象:

    $ docker build -t oldjava .

    DockerfileCMD行是我們運行容器時默認執行的過程。由於我們沒有提供-Xmx-Xms JVM標誌,因此將默認使用內存設置。

    讓我們運行該容器:

    $ 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 8u130之後

    使用相同的測試程序,讓我們通過更改Dockerfile的第一行來使用最新的JVM 8:

    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

    同樣,它使用整個docker主機內存來計算JVM堆大小。但是,如果我們為容器分配1GB的RAM:

    $ 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根據容器可用的1GB RAM計算了堆大小。

    現在,我們了解了JVM如何計算其默認值以及為什麼需要最新的JVM才能獲取正確的默認值,讓我們來看一下自定義設置。

    3.常用基本映像中的內存設置

    3.1。 OpenJDK和採用OpenJDK

    與其直接在容器的命令中直接對JVM標誌進行硬編碼,不如使用JAVA_OPTS這樣的環境變量。 Dockerfile使用該變量,但是在啟動容器時可以對其進行修改:

    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 .

    JAVA_OPTS環境變量在運行時選擇內存設置:

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

    我們應該注意, -Xmx參數與JVM報告的最大內存之間存在細微差別。這是因為Xmx設置了內存分配池的最大大小,該池包括堆,垃圾回收器的倖存者空間和其他池。

    3.2。tomcat9

    Tomcat 9容器具有自己的啟動腳本,因此要設置JVM參數,我們需要使用這些腳本。

    bin/catalina.sh腳本要求我們**在環境變量CATALINA_OPTS**設置內存參數。

    首先讓我們創建一個war文件,以將其部署到Tomcat。

    然後,我們將使用簡單的Dockerfile對其進行容器化,在其中聲明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

    我們應該注意,在運行此代碼時,我們正在將新值傳遞給CATALINA_OPTS.但是,如果不提供此值, Dockerfile第3行中提供一些默認值。

    我們可以檢查應用的運行時參數,並驗證我們的選項-Xmx-Xms是否存在:

    $ 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開始,Spring Boot MavenGradle插件可以Dockerfile的高效容器

    使用Maven,我們將它們添加到spring-boot-maven-plugin configuration>

    <?xml version="1.0" encoding="UTF-8"?>
    
     <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    
     xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    
     <groupId>com.baeldung.docker</groupId>
    
     <artifactId>heapsizing-demo</artifactId>
    
     <version>0.0.1-SNAPSHOT</version>
    
     <!-- dependencies... -->
    
     <build>
    
     <plugins>
    
     <plugin>
    
     <groupId>org.springframework.boot</groupId>
    
     <artifactId>spring-boot-maven-plugin</artifactId>
    
     <configuration>
    
     <image>
    
     <name>heapsizing-demo</name>
    
     </image>
    
     <!--
    
     for more options, check:
    
     https://docs.spring.io/spring-boot/docs/2.4.2/maven-plugin/reference/htmlsingle/#build-image
    
     -->
    
     </configuration>
    
     </plugin>
    
     </plugins>
    
     </build>
    
     </project>

    要構建項目,請運行:

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

    這將產生一個名為<artifact-id>:<version>.在此示例中, demo-app:0.0.1-SNAPSHOT 。在後台,Spring Boot使用Cloud Native Buildpacks作為底層容器化技術。

    該插件對JVM的內存設置進行硬編碼。但是,我們仍然可以通過設置環境變量JAVA_OPTSJAVA_TOOL_OPTIONS:

    $ 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 8
    
     Calculated JVM Memory Configuration: [...]
    
     [...]
    
     Picked up JAVA_TOOL_OPTIONS: -Xms20M -Xmx20M
    
     [...]

    4.2。使用Google JIB

    就像Spring Boot maven插件一樣,Google JIB無需Dockerfile即可創建高效的Docker映像。 Maven和Gradle插件以類似的方式配置。 Google JIB還使用環境變量JAVA_TOOL_OPTIONS作為JVM參數的覆蓋機制。

    我們可以在任何能夠生成可執行jar文件的Java框架中使用Google JIB Maven插件。例如,可以在Spring Boot應用程序中使用它代替spring-boot-maven插件來生成容器映像:

    <?xml version="1.0" encoding="UTF-8"?>
    
     <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    
     xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    
    
    
     <!-- dependencies, ... -->
    
    
    
     <build>
    
     <plugins>
    
     <!-- [ other plugins ] -->
    
     <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>
    
     </plugins>
    
     </build>
    
     </project>
    

    該映像是使用maven的jib:DockerBuild目標構建的:

    $ 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
    
     [...]
    
     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) : 50mb
    
     2021-01-25 17:46:44.075 INFO 1 --- [ main] c.baeldung.docker.XmxXmsDemoApplication : Max Memory (xmx) : 50mb

    5.結論

    在本文中,我們介紹了使用最新的JVM獲取在容器中正常工作的默認內存設置的需求。

    然後,我們研究了-Xms-Xmx最佳實踐,以及如何與現有Java應用程序容器一起在其中設置JVM選項。

    最後,我們看到瞭如何利用構建工具來管理Java應用程序的容器化。