시스템 관리_22 톰캣 WAS java.lang.OutOfMemoryError Java heap space 오류 원인 분석 및 JVM 튜닝 가이드

 
  • 출처 / 참고: Apache Tomcat Official Documentation, Oracle JVM Tuning Guide (HotSpot Virtual Machine Garbage Collection Tuning Guide)

명령어: jcmd, jstat, jmap, ps, free, top
키워드: OutOfMemoryError, Java heap space, JVM Options, setenv.sh, HeapDumpOnOutOfMemoryError, Garbage Collection, MAT
사용처: 톰캣(Tomcat) 기반 웹 애플리케이션 서비스 먹통(Stall) 장애 복구, 대용량 파일 다운로드/엑셀 추출 시 메모리 고갈 해결, 실시간 힙 메모리 누수 추적


실행예제

톰캣 catalina.out에 java.lang.OutOfMemoryError: Java heap space가 찍히며 서비스 응답이 중단될 때, 현재 실행 중인 톰캣의 PID, 실제 적용된 JVM 힙 옵션(-Xms, -Xmx), 그리고 실시간 GC(가비지 컬렉션) 동작 상태를 점검합니다.

# 1. 톰캣 로그에서 OutOfMemoryError 발생 내역 확인
$ tail -n 50 /opt/tomcat/logs/catalina.out
# [출력 예시]
# java.lang.OutOfMemoryError: Java heap space
# Dumping heap to /opt/tomcat/dump/heap_20261004.hprof ...
# Heap dump file created [1824719283 bytes in 3.412 secs]

# 2. 톰캣 프로세스(PID) 및 적용된 JVM 시작 인자(-Xms, -Xmx 등) 확인
$ ps -ef | grep -E 'org.apache.catalina.startup.Bootstrap|tomcat' | grep -v grep
# tomcat  32145  1  99 14:10 ?  00:15:20 /usr/bin/java -Djava.util.logging.config.file=... -Xms512m -Xmx1024m -XX:+HeapDumpOnOutOfMemoryError ...
# -> 힙 크기 설정이 기본값(1GB)으로 너무 작게 잡혀 있는지, 힙 덤프 옵션이 누락되었는지 확인

# 3. 실시간 GC 동작 통계 및 힙 영역별 점유율 모니터링 (1초 간격 5회 갱신)
# S0C/S1C: 서바이버 영역 용량, EC: 에덴 영역 용량, OC: 올드 영역 용량, OU: 올드 영역 사용량
$ jstat -gcutil 32145 1000 5
  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     CGC    CGCT     GCT   
  0.00   0.00 100.00  99.98  95.40  91.20   1245   12.450    85   185.340     -       -   197.790
# -> Old 영역(O)이 99.98%에 도달하고 Full GC(FGC) 횟수 및 시간(FGCT)이 폭증하면 Heap Space 고갈 상태임

# 4. 메모리를 가장 많이 점유하고 있는 Top 20 자바 클래스 인스턴스 실시간 덤프 (히스토그램)
$ sudo -u tomcat jcmd 32145 GC.class_histogram | head -n 25
# [출력 예시] byte[] 배열이나 특정 Entity/VO 객체가 메모리의 대부분을 차지하는지 확인

   

스크립트

구동 중인 톰캣 프로세스를 자동으로 찾아 JVM 인자 구성, 가용 시스템 RAM 대비 힙 비율, Full GC 지연 상태를 진단하고 OOM 자동 덤프 스위치(HeapDumpOnOutOfMemoryError)가 적용되어 있는지 검증하는 쉘 스크립트입니다.

#!/bin/bash
# diagnose_tomcat_heap.sh - Diagnose Tomcat JVM Heap Memory and GC Health

echo "=========================================="
echo " [톰캣 JVM 힙 메모리 및 OOM 상태 점검]"
echo "=========================================="

# 1. 톰캣 PID 추출
TOMCAT_PID=$(pgrep -f "org.apache.catalina.startup.Bootstrap" | head -n 1)

if [ -z "$TOMCAT_PID" ]; then
    echo "  [오류] 구동 중인 톰캣 프로세스를 찾을 수 없습니다."
    exit 1
fi

TOMCAT_USER=$(ps -o user= -p "$TOMCAT_PID")
echo "  - 톰캣 PID      : ${TOMCAT_PID}"
echo "  - 실행 계정     : ${TOMCAT_USER}"

# 2. JVM 시작 옵션 점검
JVM_CMD=$(ps -p "$TOMCAT_PID" -o args=)
XMS=$(echo "$JVM_CMD" | grep -oE "\-Xms[0-9]+[kmgKMG]")
XMX=$(echo "$JVM_CMD" | grep -oE "\-Xmx[0-9]+[kmgKMG]")
DUMP_OPT=$(echo "$JVM_CMD" | grep -o "HeapDumpOnOutOfMemoryError")

echo -e "\n[1] 설정된 힙 메모리 옵션:"
echo "  - 초기 힙(-Xms) : ${XMS:-"미설정 (기본값 사용)"}"
echo "  - 최대 힙(-Xmx) : ${XMX:-"미설정 (기본값 사용)"}"

if [ -z "$DUMP_OPT" ]; then
    echo "  [경고] -XX:+HeapDumpOnOutOfMemoryError 옵션이 설정되어 있지 않습니다."
    echo "  -> OOM 발생 시 메모리 릭 분석을 위한 .hprof 파일이 생성되지 않습니다."
else
    echo "  [정상] OOM 자동 힙 덤프 옵션이 활성화되어 있습니다."
fi

# 3. 호스트 시스템 물리 메모리 대비 비교
TOTAL_SYS_MEM_MB=$(free -m | awk '/Mem:/ {print $2}')
echo -e "\n[2] 호스트 시스템 메모리 현황:"
echo "  - 전체 RAM 용량 : ${TOTAL_SYS_MEM_MB} MB"

# 4. jstat을 활용한 실시간 Old 영역 점유율 및 Full GC 점검
echo -e "\n[3] 실시간 힙 Old 영역 및 Full GC 상태 (jstat):"
if command -v jstat &>/dev/null; then
    GC_LINE=$(sudo -u "$TOMCAT_USER" jstat -gcutil "$TOMCAT_PID" | tail -n 1)
    OLD_USAGE=$(echo "$GC_LINE" | awk '{print $4}')
    FGC_COUNT=$(echo "$GC_LINE" | awk '{print $9}')
    FGCT_TIME=$(echo "$GC_LINE" | awk '{print $10}')

    echo "  - Old 영역 점유율 : ${OLD_USAGE}%"
    echo "  - Full GC 누적횟수 : ${FGC_COUNT}회"
    echo "  - Full GC 누적시간 : ${FGCT_TIME}초"

    # 점유율 임계치 경고
    IS_HIGH=$(echo "$OLD_USAGE > 90.0" | bc -l 2>/dev/null)
    if [ "$IS_HIGH" -eq 1 ]; then
        echo "  [위험] Old Generation 점유율이 90%를 초과했습니다! 임박한 OOM 위험."
    else
        echo "  [정상] Old Generation 여유 공간이 확보되어 있습니다."
    fi
else
    echo "  [안내] jstat 명령어를 찾을 수 없어 GC 통계를 건너뜁니다."
fi

echo -e "\n=========================================="
echo " [진단 완료]"
echo "=========================================="

   

해설

java.lang.OutOfMemoryError: Java heap space는 JVM의 Garbage Collector가 Full GC를 연속 수행하여 미사용 객체를 회수하려고 시도했음에도 불구하고, 새로 할당할 객체를 위한 힙 메모리(Heap Memory, 특히 Old Generation) 공간이 부족할 때 JVM이 스레드를 중단시키며 던지는 치명적 에러입니다.

1. 주요 발생 원인

  1. 절대적인 힙 할당량 부족:
    • 서버 물리 RAM은 16GB인데 톰캣 구동 인자(-Xmx)를 명시하지 않아 JVM 기본값(물리 메모리의 1/4 수준)으로 동작하여 트래픽 증가 시 메모리가 고갈되는 경우.
  2. 단발성 대용량 객체 생성:
    • 수십만 건의 DB 데이터를 페이징 처리 없이 한 번에 조회(List<Entity>)하거나, 수백 MB 크기의 엑셀 파일을 POI 라이브러리로 처리할 때 순간적으로 수 GB의 힙이 소모되는 경우.
  3. 메모리 누수 (Memory Leak):
    • static 컬렉션(Map, List)에 객체를 지속 적재하고 제거(remove)하지 않는 경우.
    • ThreadLocal 변수를 요청 종료 후 remove()하지 않아 톰캣 작업 스레드 풀(Thread Pool)에 이전 요청 객체가 영구 잔존하는 경우.

2. 단계별 해결 및 튜닝 방안

1) bin/setenv.sh 스크립트를 통한 힙 옵션 명시 (권장)

톰캣 시작 시 자동으로 로드되는 bin/setenv.sh 파일에 안전한 힙 크기와 OOM 자동 진단 스위치를 등록합니다.

# /opt/tomcat/bin/setenv.sh
export CATALINA_OPTS="$CATALINA_OPTS \
-Xms4096m -Xmx4096m \
-XX:+UseG1GC \
-XX:InitiatingHeapOccupancyPercent=45 \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/opt/tomcat/dumps/oom_dump.hprof \
-XX:+ExitOnOutOfMemoryError"
  • -Xms와 -Xmx를 동일하게 설정: 힙 영역 동적 확장/축소로 인한 불필요한 Full GC 부하 및 메모리 파편화를 방지합니다.
  • -XX:+UseG1GC: 대용량 힙(4GB 이상) 환경에서 Stop-The-World(STW) 지연 시간을 최소화하기 위해 G1 Garbage Collector를 적용합니다.
  • -XX:+HeapDumpOnOutOfMemoryError: OOM 크래시가 터지는 즉시 메모리 상태를 .hprof 파일로 박제합니다.
  • -XX:+ExitOnOutOfMemoryError: OOM 이후 비정상 상태로 톰캣이 좀비처럼 떠서 다른 요청을 거부하는 것을 방지하고, 프로세스를 즉시 종료시켜 systemd에 의한 안전한 자동 재기동(Restart=always)을 유도합니다.

2) Eclipse MAT(Memory Analyzer)를 통한 원인 규명

생성된 .hprof 파일을 다운로드하여 Eclipse MAT 또는 VisualVM에 로드하고 “Leak Suspects Report”를 실행하면, 힙 메모리의 80~90%를 틀어쥐고 있는 특정 클래스 인스턴스와 해당 객체를 참조하고 있는 쓰레드를 1분 내로 특정할 수 있습니다.

   

주의사항

  1. 물리 메모리(Host RAM) 100%를 힙으로 할당 금지:
    • 서버 물리 RAM이 8GB라고 해서 -Xmx8g로 설정하면 안 됩니다. JVM은 힙 외에도 메타스페이스(Metaspace), 스레드 스택(Thread Stack per 1MB), Native 메모리, 그리고 OS 자체 버퍼를 소비하므로 물리 RAM의 60~70% 수준까지만 최대 힙(-Xmx)으로 할당해야 커널 OOM Killer에 의한 강제 kill -9 참사를 피할 수 있습니다.
  2. OOM 덤프 저장소의 디스크 여유 공간 사전 확보:
    • -XX:HeapDumpPath가 위치한 디스크 파티션의 여유 공간이 -Xmx로 설정한 용량보다 작으면 덤프를 쓰다가 디스크 풀(Disk Full)이 발생하여 덤프 파일이 깨지고 시스템 전체 장애로 번질 수 있습니다.
  3. catalina.sh 직접 수정 지양:
    • 톰캣 배포본의 기본 구동 스크립트인 catalina.sh 본문을 직접 고치면 향후 톰캣 마이너 버전 업그레이드 시 설정이 유실될 수 있습니다. JVM 옵션은 항상 독립적인 setenv.sh 파일로 분리 관리해야 합니다.