java.lang.OutOfMemoryError: Java heap space 완전 해결 가이드
새벽에 알람이 울리고 운영 로그에 java.lang.OutOfMemoryError: Java heap space가 찍혀 있을 때, 머릿속이 하얘지는 경험 다들 한 번쯤 있으실 겁니다. 이 글은 그 순간 바로 복붙해서 쓸 수 있는 명령어 중심으로, 원인 분기 진단부터 힙덤프 수집·분석, JVM 튜닝, 재발 방지까지 30분 안에 끝내는 실전 절차를 정리했습니다.
먼저 메시지부터 읽자: OOM 원인 분기표
OutOfMemoryError는 한 종류가 아닙니다. 뒤에 붙는 문구가 곧 진단의 시작점입니다. 콜론 뒤 메시지를 정확히 보고 분기하세요.
| 메시지 | 의미 | 주요 원인 | 1차 대응 |
|---|---|---|---|
Java heap space | 힙에 객체를 올릴 공간 부족 | 객체 누적·누수(컬렉션 무한 증가), 단순 힙 크기 부족, 대용량 조회 | 힙덤프 수집 → MAT 분석, -Xmx 점검 |
GC overhead limit exceeded | GC가 전체 시간의 98% 이상을 쓰고도 힙의 2% 미만만 회수 | 힙이 거의 가득 차 GC가 헛돌고 있음 (누수의 전조) | 힙덤프 수집 필수, 누수 우선 의심 |
Metaspace | 클래스 메타데이터 영역 고갈 | 동적 클래스 로딩 과다, 클래스로더 누수, 핫 리로드 반복 | -XX:MaxMetaspaceSize 점검, 로드된 클래스 수 확인 |
핵심: Java heap space와 GC overhead limit exceeded는 **같은 뿌리(힙 고갈)**에서 나오는 형제입니다. 후자가 보이면 누수일 확률이 더 높다고 보고 접근하세요.
힙덤프 수집: 자동 + 수동 명령어
자동 수집 (운영 필수 설정)
OOM 발생 순간의 힙을 자동으로 떨궈주는 옵션입니다. 운영 JVM에는 무조건 미리 넣어두세요. 사후에 후회하지 않으려면 필수입니다.
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/var/log/app/ \
-XX:+ExitOnOutOfMemoryError # OOM 후 좀비 상태 방지, 재기동 유도수동 수집 (살아있는 프로세스에서)
이미 떠 있는 프로세스가 위태로울 때 직접 덤프를 뜹니다. <PID>는 jps -l로 확인하세요.
# jmap (live 옵션은 GC 후 살아있는 객체만 → 덤프 크기 축소)
jmap -dump:live,format=b,file=/tmp/heap.hprof <PID>
# jcmd (JDK 권장 방식, 더 안정적)
jcmd <PID> GC.heap_dump /tmp/heap.hprof덤프 없이 빠른 점유 확인
전체 덤프(수 GB)를 뜨기 전에, 어떤 클래스가 메모리를 먹는지 가볍게 확인할 수 있습니다.
jmap -histo:live <PID> | head -30
jcmd <PID> GC.class_histogram | head -30상위에 byte[], char[], java.util.HashMap$Node, 그리고 여러분의 도메인 객체가 비정상적으로 많다면 거의 확실한 누수 신호입니다.
Eclipse MAT로 누수 범인 찾기
수집한 .hprof를 Eclipse MAT로 엽니다. 분석 순서는 늘 동일합니다.
- hprof 로드 → 열 때 자동으로 인덱싱이 돌고 "Leak Suspects Report" 생성 여부를 묻습니다. 예를 누르세요.
- Leak Suspects 리포트 → MAT가 "Problem Suspect 1"로 의심 객체를 파이차트와 함께 제시합니다. 보통 여기서 80%는 답이 나옵니다.
- Dominator Tree →
Retained Heap(해당 객체가 사라지면 회수될 총 메모리) 기준 내림차순 정렬. 최상단 객체를 펼쳐 누가 붙들고 있는지 추적합니다. - 누수 패턴 식별 → 단골 범인은 다음과 같습니다.
- 계속 커지는
static캐시용HashMap/List ThreadLocal정리 누락 (스레드풀 환경에서 치명적)- 리스너/콜백 등록 후 해제 안 함
- 세션/커넥션 객체 무한 적재
- 계속 커지는
의심 객체 우클릭 → Path to GC Roots → exclude weak/soft references를 보면 "왜 이 객체가 GC되지 않는가"의 참조 경로가 드러납니다.
JVM·컨테이너 메모리 튜닝
베어메탈/VM 환경
-Xms4g -Xmx4g # Xms = Xmx 권장
-XX:MaxMetaspaceSize=512m
-Xlog:gc*:file=/var/log/app/gc.log:time,uptime:filecount=5,filesize=10m-Xmx는 물리 메모리의 50~70% 선에서 시작합니다. 나머지는 OS·메타스페이스·스레드 스택·다이렉트 버퍼 몫입니다.-Xms를-Xmx와 같게 두는 이유: 런타임 힙 확장 시 발생하는 OS 메모리 재할당·full GC 오버헤드를 없애고, 시작부터 메모리를 확보해 예측 가능성을 높입니다.
Docker / Kubernetes 환경
JDK 17·21 LTS에서는 UseContainerSupport가 기본 활성화되어 JVM이 cgroup 메모리 한계를 인식합니다. 컨테이너에서는 고정 -Xmx보다 비율 지정이 안전합니다.
-XX:MaxRAMPercentage=75.0 # 컨테이너 메모리 한계의 75%를 힙으로
-XX:InitialRAMPercentage=75.0⚠️ 함정: JDK 8u191 이전 등 구버전은 cgroup을 인식하지 못해 호스트 전체 메모리 기준으로 힙을 잡습니다. 그 결과 컨테이너 한계를 넘겨 곧바로 OOMKilled로 죽습니다. 구버전이라면 -XX:+UseContainerSupport를 명시하거나, 그래도 안 되면 -Xmx를 직접 박으세요.
누수 vs 단순 부족 — 구분 체크리스트
이 구분이 사실상 전부입니다. GC 로그(-Xlog:gc*)에서 Full GC 직후 Old Gen 사용량을 보세요.
- ✅ 단순 부족: GC 직후 Old Gen이 뚝 떨어졌다가 트래픽 피크 때만 일시 상승 →
-Xmx상향으로 해결. - 🚨 메모리 누수: GC를 돌려도 Old Gen 사용량이 계단식으로 우상향, 회수가 안 됨 → 힙을 키워도 시간만 벌 뿐 결국 또 죽습니다. MAT 분석으로 코드 수정이 답.
실무 한마디
제 경험상 운영 OOM의 절반 이상은 "힙이 부족해서"가 아니라 "안 줄어들어서"였습니다. 그래서 저는 장애 시 -Xmx부터 올리는 대신 항상 힙덤프를 먼저 떠둡니다. 재기동하면 증거가 날아가니까요. 일단 덤프 확보 → 재기동 → 차분히 MAT 분석, 이 순서를 몸에 익히면 같은 장애를 두 번 겪지 않습니다.
재발 방지는 세 가지로 정리됩니다.
- GC 로그 상시 수집(
-Xlog:gc*)과 주기적 Old Gen 추세 확인 - APM(Pinpoint, Scouter, Datadog 등)으로 힙 사용률 시각화
- 힙 사용률 임계치(예: 85%) 알람 설정 → OOM 전에 손쓰기
이건 힙 OOM, OOMKilled(137)와는 다르다
마지막으로 가장 헷갈리는 지점입니다. 이 글이 다룬 건 JVM 내부 힙(애플리케이션 레벨) OOM입니다. 진단의 출발점은 애플리케이션 로그의 java.lang.OutOfMemoryError 입니다.
반면 프로세스가 exit code 137로 죽고 힙 OOM 로그가 전혀 없다면, 그건 컨테이너 cgroup 메모리 한계를 넘겨 커널(OOM Killer)이 프로세스를 강제 종료한 OOMKilled입니다. 원인·진단·해결이 완전히 다릅니다(컨테이너 메모리 limit, 힙 외 네이티브 메모리 등). 이 경우는 별도 OOMKilled 진단 글을 참고하세요.
| 구분 | Java heap space (이 글) | OOMKilled (exit 137) |
|---|---|---|
| 주체 | JVM | 리눅스 커널 |
| 신호 | 앱 로그 OOM 스택트레이스 | 컨테이너 종료, 137 코드 |
| 해결 | 힙덤프·코드 수정·-Xmx | 컨테이너 limit·네이티브 메모리 |
자주 묻는 질문 (FAQ)
Q. 힙덤프를 뜨면 서비스가 멈추나요?
A. jmap -dump:live나 jcmd GC.heap_dump는 STW(Stop-The-World)를 유발하므로 덤프 크기에 비례해 수초~수십초 애플리케이션이 멈출 수 있습니다. 트래픽이 적은 시점에 뜨거나, 운영에서는 -XX:+HeapDumpOnOutOfMemoryError로 OOM 순간에만 자동 수집하는 것을 권장합니다.
Q. -Xmx만 올리면 OOM이 해결되나요?
A. 단순 부족이면 해결되지만 누수라면 시간만 버는 임시방편입니다. GC 후에도 Old Gen이 계속 우상향한다면 코드 레벨 누수이므로 MAT 분석이 필수입니다.
Q. 컨테이너에서 메모리는 충분한데 OOM이 나요.
A. 두 가지를 확인하세요. ① 구버전 JVM이 cgroup을 인식 못 해 호스트 메모리 기준으로 힙을 잡았는지, ② -Xmx는 충분한데 메타스페이스/다이렉트 버퍼 등 힙 외 영역이 컨테이너 limit를 넘겨 137로 죽은 건 아닌지. 후자라면 힙 OOM이 아니라 OOMKilled 케이스입니다.
이 글은 AI 에이전트가 1차 초안을 작성한 뒤, 사람 편집자가 사실관계·출처·톤과 맥락을 검토하여 발행했습니다. 오류나 부정확한 내용이 확인되면 24시간 이내에 정정합니다.
댓글
불러오는 중...