1. 서론: WAS 운영의 최대 복병, OutOfMemoryError
Tomcat 기반의 Java 백엔드 시스템을 프로덕션 환경에서 운영하다 보면 한 번쯤 맞닥뜨리는 가장 치명적인 재앙이 바로 java.lang.OutOfMemoryError (OOM)입니다.
서버의 물리적 RAM이 32GB, 64GB로 충분하더라도 JVM에 할당된 힙 메모리 옵션이 기본값으로 방치되어 있다면, Tomcat은 불과 수백 MB의 기본 메모리만 사용하다가 대규모 트래픽이나 파일 업로드 요청이 들어오는 순간 프로세스가 급작스럽게 뻗어버릴 수 있습니다.
반대로 물리 메모리를 초과하여 힙 메모리를 과도하게 할당하면 OS 레벨의 메모리 스왑(Paging/Swapping)이 발생하여 시스템 전체 I/O가 마비되거나 긴 시간 동안 서버가 멈추는 STW(Stop-The-World) 현상이 초래됩니다.
이번 포스팅에서는 Tomcat JVM 메모리 구조의 핵심, Xms/Xmx 동일 설정의 원리, Metaspace 통제, G1GC 최적화, 그리고 실무용 setenv.sh 완벽 템플릿까지 낱낱이 파헤칩니다.
2. JVM 메모리 구조 한눈에 보기
Tomcat이 실행되는 JVM 프로세스의 메모리는 크게 힙(Heap) 영역과 넌힙(Non-Heap / Native) 영역으로 나뉩니다.
subgraph JVM_Process ["Tomcat JVM 프로세스"]
subgraph HeapArea ["1. JVM 힙 메모리 (-Xms, -Xmx)"]
Young["Young Gen (Eden + S0/S1 Survivor)"]
Old["Old Gen (장기 생존 객체)"]
end
subgraph NonHeapArea ["2. Non-Heap / Native Memory"]
Meta["Metaspace (-XX:MaxMetaspaceSize)"]
CodeCache["Code Cache (JIT 컴파일러)"]
Stack["Thread Stacks (-Xss: 스레드당 1MB)"]
Direct["Direct Buffer (NIO 네이티브 I/O)"]
end
end
end
- 힙(Heap) 메모리: 자바 애플리케이션에서
new키워드로 동적 생성되는 모든 객체와 배열이 상주하는 공간입니다. 가비지 컬렉터(GC)의 주요 관리 대상입니다. - 메타스페이스(Metaspace): 클래스의 바이트코드 메타데이터, 메서드 정보 등이 저장되는 네이티브 메모리 공간입니다 (Java 8 이전의 PermGen을 대체).
- 스레드 스택(Thread Stack): 각 스레드마다 호출 스택 프레임, 지역 변수를 저장하는 공간으로, Tomcat의
maxThreads수치에 비례하여 네이티브 메모리를 직접 점유합니다.
3. 힙 메모리 핵심 파라미터와 ‘골든 룰’
1) -Xms와 -Xmx를 반드시 동일하게 설정하라
-Xms(Initial Heap Size): JVM 시작 시 최초로 할당받는 힙 크기.-Xmx(Maximum Heap Size): 힙이 도달할 수 있는 최대 크기.
[!IMPORTANT] 왜 실무에서는 Xms와 Xmx를 같은 크기로 고정하는가? 만약
-Xms1g -Xmx8g처럼 차이를 두면, 트래픽이 몰릴 때 JVM은 힙 크기를 동적으로 확장(Resize)하기 위해 OS에 메모리를 추가 요청하고 힙 재배치 작업을 수행합니다. 이 과정에서 극심한 GC STW(Stop-The-World) 지연이 발생합니다. 운영 서버에서는 초기 기동 시점에 최대 메모리를 선점하도록 두 값을 동일하게 설정하는 것이 엔터프라이즈 표준입니다.
2) 전체 물리 메모리의 50% ~ 70% 규칙
16GB RAM 서버라면 힙 메모리는 8GB ~ 10GB 내외로 설정하는 것이 안전합니다. 나머지 메모리는 OS 커널 버퍼, Tomcat 스레드 스택(200개 * 1MB = 200MB+), Netty/NIO Direct 버퍼, 그리고 Metaspace를 위해 반드시 여유 공간으로 남겨두어야 OOM Killer에 의한 프로세스 강제 종료를 막을 수 있습니다.
4. Metaspace 및 스택 크기 제어
1) -XX:MetaspaceSize와 -XX:MaxMetaspaceSize
Java 8부터 도입된 Metaspace는 기본적으로 시스템 네이티브 메모리를 무제한으로 끌어다 씁니다. 동적 프록시(Spring CGLIB, ByteBuddy 등)를 다량 사용하는 프레임워크 특성상 클래스 언로딩이 정상 작동하지 않으면 메모리 누수로 인해 물리 메모리가 잠식될 수 있습니다.
따라서 상한선인 -XX:MaxMetaspaceSize=512m를 명시하여 네이티브 메모리 고갈을 사전에 방어해야 합니다.
2) -Xss (스레드 스택 크기)
Tomcat의 동시 요청 처리 스레드는 기본적으로 1MB의 스택 메모리를 소비합니다. 64비트 환경에서 -Xss1024k가 표준이며, 깊은 재귀 호출이 없는 일반 웹 애플리케이션이라면 필요에 따라 512k로 낮춰 메모리를 절약할 수도 있습니다.
5. 최신 G1GC(Garbage-First) 컬렉터 권장 설정
Java 9 이후 기본 GC이자 4GB 이상의 대용량 힙에서 가장 안정적인 성능을 발휘하는 G1GC를 채택하고 지연 시간을 튜닝합니다.
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:InitiatingHeapOccupancyPercent=45
-XX:G1ReservePercent=15
MaxGCPauseMillis=200: 목표 최대 GC 중단 시간을 200ms로 유도합니다.InitiatingHeapOccupancyPercent=45: 힙 점유율이 45%에 도달하면 즉시 백그라운드에서 Old 영역 동시 마킹(Concurrent Marking)을 시작하여 풀 GC(Full GC) 발생을 예방합니다.
6. 프로덕션 완벽 템플릿: bin/setenv.sh
Tomcat은 기동 스크립트(catalina.sh)가 실행될 때 bin/setenv.sh 파일이 존재하면 이를 자동으로 읽어 환경 변수를 병합합니다. 파일 생성 후 실행 권한(chmod +x)을 부여합니다.
#!/bin/bash
# Tomcat JVM 메모리 및 GC 프로덕션 설정 템플릿 (16GB 서버 기준)
# 1. 힙 메모리 할당 (Xms == Xmx)
CATALINA_OPTS="$CATALINA_OPTS -Xms8g -Xmx8g"
# 2. Metaspace 및 스택 크기
CATALINA_OPTS="$CATALINA_OPTS -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m"
CATALINA_OPTS="$CATALINA_OPTS -Xss1024k"
# 3. 최신 G1GC 알고리즘 활성화
CATALINA_OPTS="$CATALINA_OPTS -XX:+UseG1GC"
CATALINA_OPTS="$CATALINA_OPTS -XX:MaxGCPauseMillis=200"
CATALINA_OPTS="$CATALINA_OPTS -XX:InitiatingHeapOccupancyPercent=45"
# 4. OutOfMemory 발생 시 자동 힙 덤프 생성 (장애 사후 분석용)
CATALINA_OPTS="$CATALINA_OPTS -XX:+HeapDumpOnOutOfMemoryError"
CATALINA_OPTS="$CATALINA_OPTS -XX:HeapDumpPath=/var/log/tomcat/dumps/heapdump_%p.hprof"
# 5. Java 17/21 표준 통합 GC 로깅 (Xlog)
CATALINA_OPTS="$CATALINA_OPTS -Xlog:gc*,gc+phases=debug:file=/var/log/tomcat/gc-%t.log:time,uptime,pid:filecount=10,filesize=20M"
# 6. 기타 헤드리스 및 인코딩 옵션
CATALINA_OPTS="$CATALINA_OPTS -Djava.awt.headless=true"
CATALINA_OPTS="$CATALINA_OPTS -Dfile.encoding=UTF-8"
export CATALINA_OPTS
7. 실시간 모니터링 및 트러블슈팅 도구
설정이 완료된 후 운영 환경에서 메모리 동작 상태를 실시간으로 점검하는 명령어입니다.
1) jstat으로 GC 상태 및 힙 영역 사용량 실시간 확인
# 1초 간격으로 GC 발생 통계 출력 (pid는 Tomcat 프로세스 번호)
jstat -gcutil <PID> 1000
# 출력 예시:
# S0 S1 E O M CCS YGC YGCT FGC FGCT CGC CGCT GCT
# 0.00 100.00 42.50 31.20 94.30 88.50 24 0.312 0 0.000 4 0.045 0.357
O(Old 영역)수치가 GC 후에도 줄어들지 않고 계속 90% 이상을 유지한다면 **메모리 누수(Memory Leak)**를 의심해야 합니다.
2) OOM 사후 분석 (Eclipse MAT)
-XX:+HeapDumpOnOutOfMemoryError 옵션으로 떨어진 .hprof 파일은 **Eclipse Memory Analyzer(MAT)**나 IntelliJ Profiler에 로드하여 “Leak Suspects Report"를 확인하면, 어떤 클래스의 인스턴스가 힙을 점유하고 있었는지 단 몇 분 만에 원인 객체를 찾아낼 수 있습니다.
8. 결론
Tomcat의 JVM 힙 메모리 설정은 단순한 숫자 조정을 넘어, 물리 인프라 자원과 Java 애플리케이션의 라이프사이클을 조율하는 엔지니어링의 기본기입니다.
초기값과 최대값을 일치시키고, Metaspace에 안전 상한을 두며, G1GC와 힙 덤프 옵션을 필수적으로 활성화하여 무중단 서비스의 안정성을 확보하시기 바랍니다.