Docker JVM 메모리 누수, 힙 말고 네이티브를 봐야 하는 이유
Docker 환경에서 JVM 힙 메모리(Heap Memory)는 정상이나, 전체 서버 메모리 사용량이 지속적으로 증가하는 현상 발생
cgroup 기반 메모리 측정 방식으로 인해 JVM이 관리하지 않는 네이티브 메모리 증가 시 OOM Killer에 의해 프로세스 강제 종료될 위험 존재
Native Memory Tracking(NMT)으로 JVM 관리 네이티브 메모리 추적 시 약 5GB의 미확인 메모리 사용량 발생, 원인 규명 필요
Docker cgroup과 JVM 메모리 측정 기준의 차이
Docker 환경에서는 cgroup의 RSS(Resident Set Size)를 기준으로 컨테이너 메모리 사용량을 측정하지만, JVM의 `-Xmx` 옵션은 Java 힙 영역(Heap Area)만 제한한다.
JVM은 힙 외에도 네이티브 메모리(Native Memory)를 사용하며, 이는 OS의 `malloc` 함수 등을 통해 할당됨
cgroup은 JVM이 직접 관리하지 않는 네이티브 라이브러리, 스레드 스택 등 프로세스 전체의 물리 메모리 점유량을 감시
결과적으로 힙 메모리가 충분해도 네이티브 메모리 누수가 발생하면 Linux OOM Killer에 의해 프로세스가 강제 종료될 수 있음
JVM 네이티브 메모리 구성 요소와 NMT의 한계
JVM은 힙 외에도 Metaspace, Thread Stack, Code Cache, GC 데이터 구조, Direct ByteBuffer 등 다양한 네이티브 메모리를 사용한다.
Thread Stack: 스레드 수 증가 시 네이티브 메모리 사용량이 비례하여 증가하는 주요 원인 중 하나임
NMT(Native Memory Tracking)는 JVM이 직접 할당하고 관리하는 메모리만 추적 가능
NMT의 한계: JVM 위에서 동작하는 네이티브 라이브러리가 `malloc`으로 직접 할당한 메모리는 추적하지 못함
따라서 NMT 결과와 실제 `top` 명령의 RSS 간에 메모리 사용량 불일치가 발생할 수 있음
메모리 누수 원인 규명을 위한 삽질 과정
본문에서는 힙 덤프 분석 등 기존 방식으로는 해결되지 않는 네이티브 메모리 누수 문제 해결을 위해 다양한 시도를 했다고 밝힌다.
로그 비활성화: 로그 파일 기록으로 인한 메모리 누수 가능성 확인 시도 (실패)
Docker 메모리 제한 명시: 컨테이너 메모리 제한 설정을 명시적으로 지정 (실패)
JVM 버전 변경: Amazon Corretto, Temurin, GraalVM 등 다양한 JVM 벤더 및 버전 변경 시도 (실패)
이러한 시도들은 문제의 원인이 특정 JVM 버전이나 설정의 문제가 아님을 시사함
NMT 대비 실제 RSS의 5GB 메모리 차이 분석
NMT(Native Memory Tracking) 결과 약 9GB의 네이티브 메모리가 사용된 것으로 나타났으나, `top` 명령어로 확인한 실제 물리 메모리 사용량(RSS)은 14GB에 달했다.
이는 NMT가 추적하지 못하는 영역에서 약 5GB의 메모리가 추가로 사용되고 있음을 의미함
해당 미확인 메모리 영역은 JVM 외부의 네이티브 라이브러리나 glibc 등 시스템 라이브러리에서 발생했을 가능성이 높음
다음 편에서는 jemalloc, jeprof, async-profiler 등의 도구를 활용하여 이 미스터리한 5GB의 출처를 추적할 예정임