
회사에서 개발 IDC 이중화 서버와 운영IDC가 이중화 되어있다.
개발 IDC중 하나의 서버만 cgroup만 v2가 아니여서 openjdk8 이미지를 사용해도 JVM 메모리 설정이 자동으로 적용되지만
나머지 서버에서는 해당 컨테이너에 설정한 메모리에 맞게 메모리가 적용되지 않는다고 생각을 했다
또한-Xmx, -Xms 설정이 적용되지 않은걸로 유추했는데 적용이 정말 안되어있는지 검증이다.
1. 어플리케이션 세팅
1.1. 상세
- 베이스이미지: openjdk:8-jre-slim
- 실행된 서버: 개발 IDC 서버
- 1번 서버는 cgroup2, 2번 서버는 cgroup1
간단하게 어플리케이션 실행시 설정한 -Xmx, -Xms 값을 로그에 찍을 수 있도록 세팅
package com.example.java8_test;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
import java.lang.management.MemoryPoolMXBean;
import java.util.logging.Level;
import java.util.logging.Logger;
public class PrintXmxXms {
private static final Logger logger = Logger.getLogger(PrintXmxXms.class.getName());
public static void main(String[] args) {
logMemory(logger);
}
static void logMemory(Logger logger) {
float mb = 1024f * 1024f;
MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
float xms = memoryBean.getHeapMemoryUsage().getInit() / mb;
float xmx = memoryBean.getHeapMemoryUsage().getMax() / mb;
logger.log(Level.INFO, "Initial Memory (xms) : {0}mb", xms);
logger.log(Level.INFO, "Max Memory (xmx) : {0}mb", xmx);
for (MemoryPoolMXBean mp : ManagementFactory.getMemoryPoolMXBeans()) {
logger.log(Level.INFO, "Pool: {0} (type {1}) = {2}", new Object[]{ mp.getName(), mp.getType(), mp.getUsage().getMax() / mb });
}
}
}
1.2. 테스트 결과
- 컨테이너의 ENTRYPOINT 는 아래와 같으며 이번테스트에 중요한 -Xms512, -Xmx2048로 설정했다.
ENTRYPOINT ["/docker-entrypoint.sh", "java", "-javaagent:/usr/app/dd-java-agent.jar", "-Xms512m", "-Xmx2048m", "-jar", "/usr/app/test.jar"]
- ECS 작업정의에는 3GB로 설정

1번 서버 결과
어플리케이션 로그에서 찍힌 값을 확인하면 Initial Memory (xms) : 512mb, Max Memory (xmx) : 1,820.5mb로 확인되는데 java 명령어로 어플리케이션 실행시 -Xm* 설정으로 힙메모리 제한이 적용된걸 확인 할 수 있다.

java -XX:+PrintFlagsFinal -version | grep HeapSize 명령어로 확인하면 호스트의 메모리 기준으로 설정된걸 확인 할 수 있다. 초기 2GB, 최대 30GB

2번 서버 결과
34번 서버에서도 위와 동일한 조건이며 어플리케이션에 찍힌 로그도 동일하다. 하지만
java -XX:+PrintFlagsFinal -version | grep HeapSize로 찍힌 결과는 다르다. 34번 서버는 cgroup1이기때문이다. 정확하게 ECS에서 설정한 3GB로 Min 3/64 = 48MB, Max 3/4 = 768MB로 찍힌걸 확인 할수 있다.

2. cgroupv2의 버그
포스트를 확인하면 JVM 컨테이너 경우 아래와 같이 설명한다.
Docker 컨테이너에서 실행되는 JVM에 대한 Hotspot 런타임 지원을 추가했습니다. 당시 Docker는 cgroups v1을 사용했기 때문에 런타임 지원에는 cgroup v1 컨트롤러만 포함되었습니다.
cgroup v2와 Java 8의 문제점
- 메모리 제한 인식 문제:
- Java 8의 초기 버전은 cgroup v2의 새로운 인터페이스를 제대로 인식하지 못한다.
- 결과적으로, JVM은 컨테이너의 메모리 제한(--memory 또는 --memory-limit)을 감지하지 못하고, 물리적 호스트의 전체 메모리를 기준으로 힙 크기를 계산한다.
- 이로 인해 설정된 -Xmx 값을 무시하거나 메모리 초과(OOM)가 발생할 수 있다.
ManagementFactory에서 찍힌 값과 -XX:+PrintFlagsFinal 문제
1. 메모리 초과(OOM) 위험
컨테이너에서 JVM이 -XX:+PrintFlagsFinal에 찍힌 값을 기준으로 메모리 할당을 시도할 경우, 컨테이너의 실제 메모리 제한(Docker의 --memory)을 초과해 OOM(Out Of Memory)이 발생할 수 있다.
- 예시:
- 컨테이너 메모리 제한: 3GB
- -XX:+PrintFlagsFinal의 MaxHeapSize: 32GB
- JVM이 32GB의 메모리를 사용하려다 컨테이너 제한을 초과해 강제 종료됨
2. 컨테이너 메모리 부족으로 시스템 성능 저하
컨테이너 내부에서 JVM이 -XX:+PrintFlagsFinal에 찍힌 값대로 메모리를 예약하려고 시도하면, 시스템이 과도한 메모리 스왑을 사용하거나 다른 프로세스에 영향을 미쳐 성능 저하가 발생할 수 있다.
- 결과:
- 컨테이너가 메모리를 초과하지 않더라도, 다른 프로세스의 메모리를 잠식할 수 있음
- CPU와 메모리 사용량이 급격히 증가해 전체 컨테이너 성능이 저하됨
3. JVM 힙 크기와 시스템 메모리 간 불일치로 인한 효율 저하
JVM이 ManagementFactory의 힙 크기(Xms, Xmx)를 기준으로 동작하면서도 내부적으로 PrintFlagsFinal 값에 따라 GC 설정(예: GC 쓰레드 수, 메모리 풀 크기 등)을 조정할 경우, 메모리 관리가 비효율적으로 이뤄질 수 있다.
- 결과:
- GC가 자주 동작하거나, 과도한 메모리 재분배로 인해 애플리케이션이 느려짐
- 애플리케이션의 메모리 사용 패턴이 일정하지 않게 됨
3. ManagementFactory와 -XX:+PrintFlagsFinal 차이
- ManagementFactory
- 주요 목적: 런타임 시점에서 JVM이 사용하는 메모리 상태를 제공한다.
- 값의 출처:
- JVM이 실제로 초기화한 힙 메모리 크기와 최대 힙 메모리 크기를 기반한다.
- 런타임 중 JVM이 동적으로 설정한 값을 반영한다.
- 값의 출처:
- 특징:
- JVM이 실행 중에 현재 힙 메모리 사용량과 관련 설정을 실시간으로 반영한다.
- Docker의 cgroup 제한(--memory)이나 사용자가 명시적으로 설정한 값(-Xms, -Xmx)이 영향을 미친다.
4. 결과
결과적으로 사용중인 대부분 jdk8이라면 베이스 이미지가 openjdk:8-jre-slim인데
해당 버전은 1.8.0_342-b07 이며 cgroup2 문제가 해결된 버전은 8u372 버전 이상이므로 해당되지 않는다..
그러므로 결국에는 버전을 올리는게 최선으로 보여진다.
예시로 컨테이너에 4GB 메모리를 할당한 경우 아래와 같이 설정을 고려할 수 있을거같다.
4.1. 기본
JVM은 힙 메모리 외에도 다음을 포함한 추가 메모리를 사용한다.
- 힙 외 메모리 (Native Memory): 스레드 스택, Metaspace, JIT Code Cache 등
- JVM 자체 오버헤드: GC 데이터 구조 및 내부 메모리 관리
일반적으로 JVM 힙 메모리는 컨테이너 메모리의 50~75% 수준으로 설정하는 것이 안전할 것으로 보여진다.
4.2. 설정 예시
- Initial Heap Memory (-Xms):
- 힙 메모리의 최소 크기. 성능을 위해 적절히 설정
- 컨테이너 메모리의 50% 이상을 할당 가능
- Maximum Heap Memory (-Xmx):
- 힙 메모리의 최대 크기 컨테이너 메모리를 초과하지 않도록 설정
- 컨테이너 메모리의 75% 이하로 설정
- 예시:
-Xms2g -Xmx3g
4.3. JVM 자동 조정 사용
Java 8u191 이상 또는 cgroup v2를 지원하는 버전에서는 JVM이 컨테이너 메모리를 자동으로 감지하도록 설정할 수 있다.
- JVM 자동 메모리 관리 옵션:
- -XX:+UnlockExperimentalVMOptions: -XX:+UseContainerSupport와 같은 실험적인 옵션을 사용할 수 있도록 허용
- -XX:+UseContainerSupport:
- Docker, Kubernetes 등 컨테이너에서 JVM이 실행될 때, 컨테이너가 지정한 메모리 및 CPU 제한(cgroup)을 감지하여 JVM 동작을 조정
- 기본적으로 OpenJDK 10 이상에서는 활성화되어 있으나, Java 8에서는 활성화하려면 명시적으로 설정해야한다.
-
- 나머지 옵션은 힙사이즈를 비율로 설정하는 옵션이다.
-XX:+UnlockExperimentalVMOptions -XX:+UseContainerSupport
-XX:InitialRAMPercentage=50.0
-XX:MaxRAMPercentage=75.0
4.4. 다른 메모리 영역 설정
Metaspace 크기
- 힙 외 메모리 사용량을 제한하기 위해 Metaspace 크기를 조정한다.
-XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m