메모리가 부족해 더 이상 할당할 수 없을 때 발생하는 오류(OutOfMemoryError).
자바에서 — 메시지가 원인을 알려 준다
| 메시지 | 원인 | 대응 |
|---|---|---|
Java heap space | 힙 부족 | 누수 조사 또는 -Xmx 조정 |
GC overhead limit exceeded | GC에 98% 시간을 쓰는데 2% 미만 회수 | 사실상 누수 |
Metaspace | 클래스 과다·클래스로더 누수 | 동적 프록시·잦은 재배포 확인 |
unable to create new native thread | 스레드 과다 | 풀 크기·ulimit 확인 |
Direct buffer memory | NIO 다이렉트 버퍼 | -XX:MaxDirectMemorySize |
Requested array size exceeds VM limit | 비정상적 배열 크기 | 코드 버그 |
첫 줄만 봐도 어디를 봐야 하는지 알 수 있다.
Error이지 Exception이 아니다
try { … } catch (OutOfMemoryError e) { … } // 잡지 말 것
Error는 복구를 기대할 수 없는 상황이다. 잡아서 계속 진행하면
메모리가 없는 상태로 동작해 예측 불가능한 오류가 이어진다.
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/dump.hprof -XX:+ExitOnOutOfMemoryError # 덤프 후 즉시 종료 → 재시작
"덤프를 남기고 죽고, 오케스트레이터가 재시작한다" 가 현대적 대응이다. 살아 있는 척하는 것보다 낫다.
원인 — 누수인가 부족인가
누수 : 안 쓰는 객체가 참조되어 계속 쌓인다
-
힙을 늘려도 시간만 벌 뿐 결국 터진다 부족 : 실제로 그만큼 필요하다
-
힙을 늘리거나 처리 방식을 바꾼다
구분 방법
- Full GC 이후에도 Old 사용률이 계속 우상향 — → 누수
- 톱니 모양으로 오르내리며 안정적 — → 정상
흔한 누수 패턴
| 패턴 | 설명 |
|---|---|
| static 컬렉션 | 애플리케이션 생명주기 내내 살아 계속 쌓인다 |
| 캐시에 상한 없음 | Map을 캐시로 쓰면서 제거 정책이 없다 |
| ThreadLocal 미제거 | 스레드 풀에서 remove() 안 하면 스레드가 사는 한 유지 |
| 리스너·콜백 미해제 | 등록만 하고 해제 안 함 |
| 대량 조회 | findAll() 로 수백만 건을 한 번에 |
| 스트림·커넥션 미close | 네이티브 자원까지 물고 있다 |
진단 절차
# 1. 힙 사용 추이
jstat -gcutil <pid> 1000
# 2. 무엇이 많은가
jmap -histo:live <pid> | head -20
# 3. 힙 덤프 (운영 중이면 부하 주의)
jcmd <pid> GC.heap_dump /tmp/heap.hprof
# 4. Eclipse MAT 로 분석
# - Leak Suspects 리포트
# - Dominator Tree: 무엇이 메모리를 붙잡고 있나
# - Path to GC Roots: 왜 회수 안 되나 ← 핵심
Path to GC Roots 가 결정적이다. "이 객체가 왜 살아 있는지"의
참조 사슬을 보여 준다. 대개 여기서 원인이 드러난다.
컨테이너 환경의 함정
① 컨테이너 메모리 인식
옛 JVM은 컨테이너 제한을 못 보고 호스트 메모리 기준으로 힙을 잡아 컨테이너가 OOM Killer에 죽었다. 자바 10+는 인식하지만 명시가 안전하다.
-XX:MaxRAMPercentage=75.0 # 고정값(-Xmx)보다 유연하다
② 힙 외 메모리를 잊는다
컨테이너 메모리 = 힙 + Metaspace + 스레드 스택 + 코드 캐시
-
- 다이렉트 버퍼 + GC 구조체 + 네이티브
힙만 딱 맞춰 잡으면 → 컨테이너 OOM Killer(exit 137)에 죽는다
JVM의 OutOfMemoryError와 리눅스 OOM Killer는 다른 것이다.
후자는 로그도 안 남기고 프로세스를 죽인다(dmesg에 기록).
③ 스왑을 끈다 쿠버네티스 기본값이다. 느리게 아픈 것보다 빨리 죽고 재시작하는 편이 낫다.
예방
✅ 캐시에는 반드시 최대 크기와 TTL ✅ 대량 조회는 페이징·스트리밍·커서로 ✅ ThreadLocal 은 finally 에서 remove() ✅ 힙 사용률·GC 시간에 알림 ✅ 부하 테스트로 장시간 운영 시 메모리 추이 확인