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

여기서도 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 및 AdoptOpenJDK

컨테이너의 명령에 직접 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. 톰캣 9

Tomcat 9 컨테이너에는 자체 시작 스크립트가 있으므로 JVM 매개 변수를 설정하려면 해당 스크립트로 작업해야합니다.

빈 / catalina.sh의 스크립트는 우리를 필요로 환경 변수에 메모리 매개 변수를 설정 CATALINA_OPTS .

먼저 Tomcat에 배포 할 war 파일만들어 보겠습니다 .

그런 다음 CATALINA_OPTS 환경 변수를 선언 하는 간단한 Dockerfile을 사용하여 컨테이너화 합니다.

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 Buildpack 을 사용합니다.

플러그인은 JVM의 메모리 설정을 하드 코딩합니다. 그러나 환경 변수 JAVA_OPTS  또는  JAVA_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 JIBDockerfile 없이 효율적인 Docker 이미지를 생성합니다 . Maven 및 Gradle 플러그인은 유사한 방식으로 구성됩니다. Google JIB는 또한 환경 변수 JAVA_TOOL_OPTIONS 를 JVM 매개 변수의 재정의 메커니즘으로 사용합니다.

실행 가능한 jar 파일을 생성 할 수있는 모든 Java 프레임 워크에서 Google JIB Maven 플러그인을 사용할 수 있습니다. 예를 들어 Spring -boot-maven 플러그인 대신 Spring Boot 애플리케이션에서이를 사용 하여 컨테이너 이미지를 생성 할 수 있습니다.

<?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 애플리케이션의 컨테이너화를 관리하기 위해 빌드 도구를 활용하는 방법을 살펴 보았습니다.

항상 그렇듯이 예제의 소스 코드는 GitHub에서 사용할 수  있습니다 .