미분류

자바 OOM 분석하는 방법

고구마엔사이다·2026년 6월 20일·조회 4

Java OOM 분석 방법: OutOfMemoryError가 발생했을 때 무엇을 봐야 할까

Java 애플리케이션을 운영하다 보면 한 번쯤 마주치는 장애가 있습니다. 바로 java.lang.OutOfMemoryError, 흔히 말하는 OOM입니다.

OOM은 단순히 “메모리가 부족했다”는 뜻으로만 보면 안 됩니다. 실제 원인은 꽤 다양합니다. Java heap이 부족할 수도 있고, Metaspace가 가득 찼을 수도 있습니다. Direct Memory 문제일 수도 있고, Kubernetes 환경에서는 JVM 내부 문제가 아니라 컨테이너 메모리 제한 때문에 프로세스가 강제로 종료되는 경우도 있습니다.

그래서 OOM을 볼 때는 에러 메시지만 보고 바로 -Xmx 값을 늘리는 방식으로 접근하면 위험합니다. 당장은 서비스가 살아날 수 있지만, 원인이 그대로라면 같은 문제는 다시 발생합니다.

OOM을 분석할 때는 먼저 어느 메모리 영역에서 문제가 발생했는지 확인해야 합니다. 그다음 메모리가 정상적으로 증가한 것인지, 아니면 해제되어야 할 객체가 계속 살아남아 있는 것인지 봐야 합니다. 결국 중요한 것은 “메모리가 부족했다”가 아니라 “왜 메모리가 회수되지 못했는가”입니다.


먼저 봐야 할 것

OOM이 발생했을 때는 아래 순서로 접근하는 것이 좋습니다.

1. OOM 메시지 확인

먼저 로그에 남은 OOM 메시지를 확인합니다.

Java heap space, Metaspace, Direct buffer memory, unable to create new native thread처럼 메시지에 따라 분석 방향이 달라집니다.

2. 실행 환경 확인

JVM 내부에서 발생한 OOM인지, Kubernetes나 Docker의 컨테이너 메모리 제한 때문에 발생한 OOMKilled인지 구분해야 합니다.

Java 로그에 OutOfMemoryError가 남아 있는지, Pod 이벤트에 OOMKilled가 남아 있는지 함께 봅니다.

3. 증거 자료 확보

Heap dump, GC 로그, 애플리케이션 로그, JVM 옵션, 컨테이너 이벤트를 확보합니다.

자료가 없으면 원인 분석이 아니라 추측에 가까워집니다. 운영 환경에서는 OOM이 발생했을 때 자동으로 분석 자료가 남도록 미리 설정해두는 것이 좋습니다.

4. Heap 분석

Java heap 문제라면 Eclipse MAT 같은 도구로 Heap dump를 분석합니다.

Dominator Tree, Retained Heap, Path to GC Roots를 중심으로 보면 됩니다.

5. Native Memory 분석

Direct Memory나 JVM 외부 메모리가 의심된다면 NMT를 확인합니다.

jcmd <pid> VM.native_memory summary 명령을 사용할 수 있지만, JVM 시작 시 NMT 옵션이 켜져 있어야 합니다.

6. 재발 방지

원인이 확인되면 단순히 메모리를 늘리는 것으로 끝내지 말아야 합니다.

캐시 크기, ThreadLocal 사용, 대량 조회 방식, 스레드 풀 설정, 컨테이너 limit, 객체 생명주기를 함께 점검해야 합니다.


1. OOM 메시지부터 정확히 확인한다

Java OOM은 메시지에 따라 분석 방향이 달라집니다. 같은 OutOfMemoryError라도 원인이 전혀 다를 수 있습니다.

가장 흔한 메시지는 다음과 같습니다.

java.lang.OutOfMemoryError: Java heap space

Java 객체가 저장되는 heap 영역이 부족할 때 발생합니다. 대량 데이터 처리, 캐시 증가, 컬렉션 누수, 세션 객체 증가, 대용량 조회 결과 적재 등이 원인이 될 수 있습니다.

java.lang.OutOfMemoryError: GC overhead limit exceeded

GC가 계속 돌고 있지만 실제로 회수되는 메모리가 거의 없을 때 발생합니다. heap이 거의 꽉 찬 상태에서 GC만 반복되는 상황으로 보면 됩니다.

java.lang.OutOfMemoryError: Metaspace

클래스 메타데이터를 저장하는 Metaspace 영역이 부족할 때 발생합니다. 동적 클래스 생성, 프록시 객체 증가, 반복 배포 과정에서 클래스 로더가 해제되지 않는 문제 등이 원인이 될 수 있습니다.

java.lang.OutOfMemoryError: Direct buffer memory

NIO, Netty, 파일 처리, 네트워크 처리 등에서 사용하는 Direct Memory가 부족할 때 발생합니다. 이 경우에는 heap 사용량이 정상처럼 보여도 OOM이 날 수 있습니다.

java.lang.OutOfMemoryError: unable to create new native thread

새로운 OS 스레드를 만들 수 없을 때 발생합니다. heap 문제가 아니라 스레드 수, OS limit, 컨테이너 리소스 제한, 스레드 풀 설정 등을 확인해야 합니다.

OOM 로그를 보면 일단 여기서부터 분기해야 합니다. heap 문제인지, Metaspace 문제인지, Direct Memory 문제인지에 따라 봐야 할 자료가 달라집니다.


2. OOM은 “메모리가 죽지 못하는 상황”일 수 있다

OOM이 나면 가장 먼저 “메모리가 부족했구나”라고 생각하기 쉽습니다. 틀린 말은 아니지만, 여기서 멈추면 원인을 놓치기 쉽습니다.

Java에서는 GC가 더 이상 필요 없는 객체를 회수합니다. 그런데 어떤 객체가 아직 참조되고 있다면 GC는 그 객체를 살아있는 객체로 판단합니다. 실제로는 비즈니스 로직상 더 이상 필요 없는데, 어딘가에서 계속 참조하고 있으면 메모리는 해제되지 않습니다.

운영에서 자주 보는 패턴은 다음과 같습니다.

  • static Map에 데이터가 계속 쌓이는 경우

  • 캐시에 TTL이나 최대 크기가 없는 경우

  • ThreadLocal에 저장한 객체를 remove하지 않는 경우

  • 세션에 대용량 객체를 계속 보관하는 경우

  • 메시지 소비가 지연되어 내부 Queue가 계속 커지는 경우

  • 배치 처리 중 중간 결과를 List에 계속 누적하는 경우

  • 대량 조회 결과를 한 번에 메모리에 올리는 경우

이런 경우에는 단순히 heap을 늘려도 문제가 사라지지 않습니다. 장애 발생 시점만 늦춰질 뿐입니다.

OOM 분석은 객체의 생명주기를 보는 작업에 가깝습니다. 객체가 언제 생성됐고, 언제 사라져야 했고, 그런데 왜 아직도 살아있는지를 확인해야 합니다.


3. OOM 발생 시점의 자료를 남겨야 한다

OOM은 발생한 뒤에 분석하려고 하면 이미 중요한 정보가 사라진 경우가 많습니다. 그래서 운영 환경에서는 미리 덤프와 로그가 남도록 설정해두는 편이 좋습니다.

OOM 발생 시 heap dump를 남기려면 다음 옵션을 사용할 수 있습니다.

-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/app/heapdump.hprof

heap dump는 OOM이 발생한 시점에 heap 안에 어떤 객체들이 있었는지 보여주는 스냅샷입니다. 나중에 Eclipse MAT 같은 도구로 열어볼 수 있습니다.

GC 로그도 반드시 남겨두는 것이 좋습니다.

Java 9 이상에서는 보통 다음과 같이 설정합니다.

-Xlog:gc*:file=/var/log/app/gc.log:time,uptime,level,tags

Java 8 환경에서는 다음 옵션을 많이 사용합니다.

-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:/var/log/app/gc.log

OOM이 발생했을 때는 최소한 아래 자료가 있어야 분석이 가능합니다.

1. 애플리케이션 로그

OOM 직전에 어떤 요청, 배치, 스케줄러, 예외가 있었는지 확인합니다.

특정 API 호출이나 특정 배치 이후에만 문제가 반복된다면 원인 범위를 좁히는 데 도움이 됩니다.

2. OOM 메시지와 스택 트레이스

어느 메모리 영역에서 문제가 발생했는지 확인합니다.

Java heap space인지, Metaspace인지, Direct buffer memory인지에 따라 이후 분석 방법이 달라집니다.

3. Heap dump

OOM 시점에 어떤 객체가 heap을 차지하고 있었는지 확인합니다.

특정 Map, List, Queue, Cache, Session 객체가 비정상적으로 커져 있는지 봐야 합니다.

4. GC 로그

GC 이후에도 메모리가 회수되지 않았는지 확인합니다.

Full GC가 반복되는데도 Old 영역 사용량이 내려가지 않는다면 메모리 누수를 의심할 수 있습니다.

5. JVM 옵션

-Xmx, -Xms, MaxMetaspaceSize, MaxDirectMemorySize, GC 설정 등을 확인합니다.

컨테이너 환경에서는 JVM 옵션과 container memory limit의 관계도 함께 봐야 합니다.

6. 컨테이너 메모리 사용량

JVM 내부 OOM인지, 컨테이너 OOMKilled인지 구분합니다.

Kubernetes에서는 Pod 이벤트와 restart count를 함께 확인합니다.

7. 배포·트래픽·배치 이력

OOM이 특정 배포 이후 발생했는지, 트래픽 증가 시점과 맞물리는지, 특정 배치 실행 후 반복되는지 확인합니다.

자료가 없으면 결국 추측만 하게 됩니다. 운영 환경에서는 OOM이 한 번 발생했을 때 바로 분석할 수 있도록 미리 준비해두는 것이 좋습니다.


4. Heap dump에서는 Retained Heap을 봐야 한다

Java heap spaceGC overhead limit exceeded라면 heap dump 분석이 중요합니다.

heap dump는 Eclipse MAT 같은 도구로 분석할 수 있습니다. MAT를 사용하면 어떤 객체가 메모리를 많이 차지하고 있는지, 어떤 참조 때문에 GC가 객체를 회수하지 못하는지 확인할 수 있습니다.

여기서 단순히 객체 자체의 크기만 보면 안 됩니다. 중요한 것은 Retained Heap입니다.

Shallow Heap

객체 자체가 차지하는 메모리입니다.

예를 들어 어떤 HashMap 객체 하나가 직접 차지하는 메모리만 보면 크지 않아 보일 수 있습니다.

Retained Heap

해당 객체가 참조하고 있어서 함께 해제되지 못하는 전체 메모리입니다.

HashMap 자체는 작아도 그 안에 수백만 개의 엔트리와 비즈니스 객체가 들어 있다면 Retained Heap은 매우 커집니다.

MAT에서 주로 보는 화면은 다음과 같습니다.

Dominator Tree

가장 많은 메모리를 붙잡고 있는 객체를 확인합니다.

특정 Map, List, Queue, Cache, Session, ClassLoader 등이 상위에 있다면 먼저 의심해볼 수 있습니다.

Leak Suspects Report

MAT가 자동으로 누수 의심 지점을 보여줍니다.

다만 이 결과를 그대로 믿기보다는 코드 구조와 운영 상황을 함께 봐야 합니다. 특정 시점의 heap dump만 보고 판단한 결과이기 때문입니다.

Path to GC Roots

객체가 왜 GC 대상이 되지 않았는지 확인할 때 사용합니다.

static 필드, ThreadLocal, 캐시, 세션, 클래스 로더 등 어떤 참조가 객체를 붙잡고 있는지 추적할 수 있습니다.

Heap dump 분석의 목적은 “큰 객체를 찾는 것”이 아닙니다. 더 정확히는 “사라졌어야 할 객체가 왜 아직 살아있는지”를 찾는 것입니다.


5. GC 로그에서는 Full GC 이후의 사용량을 본다

heap dump가 특정 시점의 상태를 보여준다면, GC 로그는 시간에 따른 흐름을 보여줍니다.

OOM 분석에서 GC 로그를 볼 때는 다음을 확인합니다.

  • Full GC가 자주 발생했는지

  • Full GC 이후에도 heap 사용량이 줄어들지 않는지

  • Old 영역 사용량이 계속 증가하는지

  • GC 시간이 점점 길어지는지

  • OOM 직전에 allocation failure가 반복되는지

정상적인 애플리케이션이라면 GC 이후 사용량이 어느 정도 내려갑니다.

반대로 메모리 누수가 있으면 Full GC 이후에도 Old 영역 사용량이 잘 내려가지 않습니다. 시간이 지날수록 GC 이후의 기준선이 계속 올라가는 모습이 보일 수 있습니다.

그래서 단순히 “heap 사용률이 높다”만 보면 부족합니다. GC가 돌고 난 뒤에도 내려가지 않는 메모리가 계속 늘어나는지를 봐야 합니다.


6. Direct Memory와 Native Memory는 NMT로 확인한다

Direct Memory OOM은 분석이 까다로운 편입니다. heap dump를 열어도 원인이 잘 보이지 않는 경우가 많습니다.

예를 들어 다음 메시지가 발생할 수 있습니다.

java.lang.OutOfMemoryError: Direct buffer memory

이 경우에는 Java heap이 아니라 JVM 외부의 native memory를 같이 봐야 합니다.

이때 사용할 수 있는 방법이 NMT, Native Memory Tracking입니다.

NMT를 사용하려면 JVM 시작 시 다음 옵션을 켜야 합니다.

-XX:NativeMemoryTracking=summary

더 자세히 보고 싶다면 detail 모드를 사용할 수도 있습니다.

-XX:NativeMemoryTracking=detail

다만 detail 모드는 summary보다 오버헤드가 더 있을 수 있으므로 운영 환경에서는 신중하게 적용하는 것이 좋습니다.

NMT가 켜져 있다면 jcmd로 native memory 사용량을 확인할 수 있습니다.

jcmd <pid> VM.native_memory summary

NMT 결과에서는 아래 항목을 주로 봅니다.

Java Heap

Java 객체가 저장되는 heap 영역입니다.

Direct Memory OOM을 보는 중이라도 heap이 컨테이너 메모리 대부분을 차지하고 있지는 않은지 함께 확인해야 합니다.

Class

클래스 메타데이터 관련 메모리입니다.

클래스 로딩이 많거나 동적 프록시가 과도하게 생성되는 경우 증가할 수 있습니다.

Thread

스레드 stack 등 스레드 관련 메모리입니다.

스레드 수가 많으면 native memory 사용량도 함께 증가합니다.

Code

JIT 컴파일 결과가 저장되는 code cache 관련 메모리입니다.

일반적인 OOM 원인으로 자주 보지는 않지만, native memory 전체 사용량을 볼 때 함께 확인합니다.

GC

GC 내부 구조에서 사용하는 메모리입니다.

GC 알고리즘과 heap 크기에 따라 사용량이 달라질 수 있습니다.

Compiler

JIT 컴파일러 관련 메모리입니다.

JVM 내부 동작에 필요한 영역으로, 전체 native memory 분석 시 참고합니다.

Internal

JVM 내부 구조에서 사용하는 메모리입니다.

어느 한 항목만 보기보다는 전체 native memory 증가 추세를 함께 봐야 합니다.

Symbol

심볼 테이블 관련 메모리입니다.

클래스 로딩, 문자열, 메타데이터 관련 문제를 볼 때 참고할 수 있습니다.

주의할 점도 있습니다. NMT가 모든 native memory 문제를 완벽하게 보여주는 것은 아닙니다. JNI, 외부 native library, 일부 third-party native code 문제는 OS 도구와 함께 봐야 할 수도 있습니다.


7. Kubernetes에서는 OOMKilled 여부를 꼭 확인한다

Kubernetes나 Docker 환경에서는 Java OOM과 컨테이너 OOMKilled를 구분해야 합니다.

애플리케이션 로그에 java.lang.OutOfMemoryError가 남아 있다면 JVM 내부에서 OOM이 발생한 것입니다.

반면 애플리케이션 로그 없이 Pod가 재시작되고, 이벤트에 OOMKilled가 보인다면 컨테이너 메모리 제한에 의해 프로세스가 강제로 종료된 것입니다.

이 경우에는 heap만 보면 안 됩니다. 컨테이너 메모리에는 heap 외에도 여러 영역이 포함됩니다.

  • Java heap

  • Metaspace

  • Direct Memory

  • Thread stack

  • Code cache

  • GC 내부 구조

  • Native library 메모리

  • OS page cache 일부

예를 들어 컨테이너 memory limit이 1GiB인데 -Xmx를 1GiB로 잡으면 안전하지 않습니다. heap 외에도 JVM이 사용하는 non-heap과 native memory가 있기 때문입니다.

컨테이너 환경에서는 대략 다음 관계를 생각해야 합니다.

container memory limit > Java heap + non-heap + native memory 여유분

Java 8, 11, 17은 컨테이너 인식 동작이 버전과 업데이트 수준에 따라 다를 수 있습니다. 최신 LTS에서는 cgroup 기반 컨테이너 메모리 제한을 JVM이 더 잘 인식하지만, 운영에서는 여전히 -Xmx, -XX:MaxRAMPercentage, -XX:MaxMetaspaceSize, Direct Memory, 스레드 수를 함께 봐야 합니다.

Kubernetes에서는 다음 명령어를 자주 사용합니다.

kubectl describe pod <pod-name>
kubectl top pod <pod-name>
kubectl get events --sort-by=.lastTimestamp

Pod 이벤트에서 OOMKilled가 확인되면 JVM 내부 OOM 분석과 별도로 컨테이너 limit, request, heap 비율, native memory 사용량을 다시 봐야 합니다.


8. 실행 중인 JVM은 jcmd로 확인한다

OOM이 아직 발생하지 않았지만 메모리 사용량이 계속 증가하고 있다면 jcmd로 현재 JVM 상태를 확인할 수 있습니다.

먼저 Java 프로세스를 확인합니다.

jcmd

JVM 옵션을 확인합니다.

jcmd <pid> VM.flags

heap 정보를 확인합니다.

jcmd <pid> GC.heap_info

클래스별 객체 수와 메모리 사용량을 확인합니다.

jcmd <pid> GC.class_histogram

운영 중 heap dump를 생성할 수도 있습니다.

jcmd <pid> GC.heap_dump /tmp/heapdump.hprof

NMT가 활성화되어 있다면 native memory도 확인할 수 있습니다.

jcmd <pid> VM.native_memory summary

단, 운영 중 heap dump 생성은 애플리케이션에 부하를 줄 수 있습니다. heap이 큰 서비스라면 파일 생성 시간과 디스크 공간도 확인해야 합니다.


9. OOM 유형별로 보는 지점

OOM 메시지에 따라 우선순위를 다르게 잡는 것이 좋습니다.

Java Heap

먼저 Heap dump를 확인합니다.

Eclipse MAT에서 Dominator Tree와 Retained Heap을 보고, 어떤 객체가 메모리를 많이 붙잡고 있는지 확인합니다.

흔한 원인은 캐시 증가, 컬렉션 누수, 대량 데이터 로딩, 세션 객체 증가입니다.

GC Overhead Limit

GC 로그를 먼저 봅니다.

Full GC가 반복되는지, Full GC 이후에도 heap 사용량이 내려가지 않는지 확인합니다.

흔한 원인은 메모리 누수, heap 부족, 과도한 객체 생성입니다.

Metaspace

클래스 로딩과 클래스 로더 상태를 확인합니다.

동적 프록시가 많이 생성되는지, 반복 배포 이후 클래스 로더가 해제되지 않는지 봐야 합니다.

흔한 원인은 반복 배포, 클래스 로더 누수, 동적 클래스 생성 과다입니다.

Direct Memory

NMT와 DirectByteBuffer 사용량을 확인합니다.

Netty나 NIO 기반 애플리케이션이라면 버퍼 반환이 제대로 되는지, Direct Memory 제한이 적절한지 봐야 합니다.

흔한 원인은 버퍼 반환 누락, Direct Memory 제한 부족, 네트워크 처리 문제입니다.

Native Thread

Thread dump와 스레드 수를 확인합니다.

스레드 풀이 과도하게 커졌는지, OS limit에 도달했는지, 컨테이너 pid 제한에 걸렸는지 봐야 합니다.

흔한 원인은 스레드 풀 과다, 무제한 스레드 생성, ulimit 도달입니다.

Kubernetes OOMKilled

Pod 이벤트와 container memory usage를 확인합니다.

Java OOM 로그가 없고 Pod가 재시작되었다면 OOMKilled를 먼저 의심해야 합니다.

흔한 원인은 컨테이너 limit 부족, heap 설정 과다, native memory 고려 부족입니다.


10. OOM 분석 체크리스트

실제 장애 상황에서는 아래 순서대로 확인하면 됩니다.

1단계. OOM 메시지 확인

먼저 로그에 남은 OOM 메시지를 확인합니다.

Java heap space, Metaspace, Direct buffer memory, unable to create new native thread 중 어떤 유형인지에 따라 분석 방향이 달라집니다.

2단계. JVM OOM과 OOMKilled 구분

애플리케이션 로그에 java.lang.OutOfMemoryError가 남아 있다면 JVM 내부 OOM일 가능성이 높습니다.

반대로 Java OOM 로그 없이 Pod가 재시작되고 Kubernetes 이벤트에 OOMKilled가 보인다면 컨테이너 메모리 제한에 의해 종료된 것입니다.

3단계. 발생 시점 로그 확인

OOM 직전에 어떤 요청, 배치, 스케줄러, 배포, 트래픽 증가가 있었는지 확인합니다.

특정 API 호출이나 배치 작업 이후에만 문제가 반복된다면 원인 범위를 좁히기 쉽습니다.

4단계. Heap dump 확인

OOM 시점에 생성된 heap dump를 확인합니다.

어떤 객체가 많이 쌓여 있었는지, 특정 컬렉션이나 캐시가 비정상적으로 커졌는지 확인합니다.

5단계. GC 로그 확인

Full GC가 반복되었는지, Full GC 이후에도 heap 사용량이 줄어들지 않았는지 확인합니다.

특히 Old 영역 사용량이 시간이 지날수록 계속 올라가는지 봐야 합니다.

6단계. Dominator Tree 확인

Eclipse MAT에서 Dominator Tree를 열어 가장 많은 Retained Heap을 잡고 있는 객체를 확인합니다.

Map, List, Queue, Cache, Session, ClassLoader 등이 상위에 있다면 우선적으로 확인합니다.

7단계. Path to GC Roots 확인

문제가 되는 객체가 왜 GC 대상이 되지 않았는지 추적합니다.

static 필드, ThreadLocal, 캐시, 세션, 클래스 로더 등이 객체를 계속 참조하고 있는지 확인합니다.

8단계. NMT 확인

Direct Memory나 Native Memory 문제가 의심된다면 NMT를 확인합니다.

jcmd <pid> VM.native_memory summary 명령으로 Thread, Class, Code, GC, Internal 영역의 native memory 사용량을 볼 수 있습니다.

단, NMT는 JVM 시작 시 -XX:NativeMemoryTracking=summary 또는 detail 옵션이 켜져 있어야 합니다.

9단계. JVM 옵션 확인

-Xmx, -Xms, MaxRAMPercentage, MaxMetaspaceSize, MaxDirectMemorySize 같은 JVM 옵션을 확인합니다.

컨테이너 환경에서는 heap 크기만 볼 것이 아니라 container memory limit과의 비율도 함께 봐야 합니다.

10단계. 재발 방지 조치 정리

원인에 따라 조치를 정리합니다.

무제한 캐시는 최대 크기와 TTL을 설정하고, 대량 조회는 페이징이나 스트리밍 방식으로 바꿉니다. ThreadLocal은 사용 후 remove() 처리하고, 컨테이너 OOMKilled가 원인이라면 heap과 container limit 비율을 다시 조정합니다.


11. 임시 조치와 근본 조치는 다르다

OOM이 발생하면 우선 서비스를 살려야 합니다. 재시작이나 메모리 증설이 필요할 수 있습니다. 하지만 이것은 어디까지나 임시 조치입니다.

원인에 따라 근본 조치는 달라집니다.

무제한 캐시가 원인인 경우

캐시에 최대 크기, TTL, eviction 정책을 설정해야 합니다.

캐시는 성능을 위해 필요하지만, 제한이 없으면 장애 원인이 되기 쉽습니다.

대량 조회가 원인인 경우

한 번에 모든 데이터를 메모리에 올리지 않도록 바꿔야 합니다.

페이징, 스트리밍, 커서 기반 처리 방식을 검토합니다.

배치 중 메모리 누적이 원인인 경우

중간 결과를 계속 List에 담아두는 구조인지 확인합니다.

필요하다면 파일, DB, 외부 저장소로 나누어 처리해야 합니다.

ThreadLocal 미정리가 원인인 경우

사용 후 remove() 처리를 해야 합니다.

특히 WAS나 스레드 풀 환경에서는 ThreadLocal 값이 예상보다 오래 살아남을 수 있습니다.

세션 객체 증가가 원인인 경우

세션에 저장하는 데이터를 최소화해야 합니다.

대용량 객체를 세션에 넣고 있지 않은지, 만료 정책이 적절한지 확인합니다.

Direct Memory 부족이 원인인 경우

버퍼가 정상적으로 반환되는지 확인합니다.

필요하다면 MaxDirectMemorySize 설정도 함께 검토합니다.

스레드 과다가 원인인 경우

스레드 풀 크기와 큐 정책을 확인합니다.

요청마다 새 스레드를 만드는 구조나 제한 없는 스레드 풀은 피해야 합니다.

컨테이너 OOMKilled가 원인인 경우

heap과 container limit 비율을 다시 조정해야 합니다.

heap 외에도 Metaspace, Direct Memory, Thread stack, Code cache가 메모리를 사용한다는 점을 고려해야 합니다.

-Xmx를 늘리는 것은 문제를 늦출 수는 있지만, 누수를 해결하지는 못합니다.

원인을 보지 않고 메모리만 늘리는 대응은 위험합니다. 당장은 장애 간격이 길어질 수 있지만, 객체 생명주기나 참조 구조가 잘못되어 있다면 같은 문제는 다시 돌아옵니다.


12. 운영 환경에서 미리 준비할 것

OOM은 발생 후에 허둥지둥 분석하는 것보다, 발생했을 때 바로 분석할 수 있도록 준비해두는 것이 중요합니다.

운영 환경에서는 최소한 다음 옵션을 준비하는 것이 좋습니다.

-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/app
-Xlog:gc*:file=/var/log/app/gc.log:time,uptime,level,tags

Direct Memory나 컨테이너 OOM까지 같이 봐야 하는 서비스라면 NMT 옵션도 검토해볼 수 있습니다.

-XX:NativeMemoryTracking=summary

모니터링 지표도 함께 봐야 합니다.

Heap used / committed / max

heap 사용률과 증가 추세를 확인합니다.

특히 사용량이 계속 올라가기만 하고 내려오지 않는지 봐야 합니다.

Old 영역 사용률

장기 생존 객체가 계속 증가하는지 확인합니다.

Full GC 이후에도 Old 영역이 내려가지 않는다면 메모리 누수를 의심할 수 있습니다.

Full GC 횟수와 시간

GC 압박이 심해지고 있는지 확인합니다.

Full GC가 잦아지고 시간이 길어진다면 OOM 전조일 수 있습니다.

Metaspace 사용량

클래스 메타데이터가 계속 증가하는지 확인합니다.

동적 클래스 생성이나 클래스 로더 누수와 관련될 수 있습니다.

Direct Memory 사용량

NIO, Netty, Buffer 사용량을 확인합니다.

heap 사용량은 정상인데 컨테이너 메모리가 계속 증가한다면 Direct Memory를 의심해볼 수 있습니다.

Thread count

스레드 수가 계속 증가하는지 확인합니다.

스레드가 많아지면 Thread stack으로 인해 native memory 사용량도 증가합니다.

Container memory usage

컨테이너 전체 메모리 사용량을 확인합니다.

JVM heap만이 아니라 non-heap, native memory까지 포함해서 봐야 합니다.

Pod restart count

Pod가 반복적으로 재시작되고 있는지 확인합니다.

애플리케이션 로그와 Kubernetes 이벤트를 함께 봐야 원인을 구분할 수 있습니다.

OOMKilled 이벤트

컨테이너 메모리 제한에 의해 종료되었는지 확인합니다.

Java OOM 로그 없이 OOMKilled만 남아 있다면 JVM 내부가 아니라 컨테이너 limit 관점에서 분석해야 합니다.

OOM 분석은 dump 파일 하나로 끝나지 않습니다. 로그, 메트릭, 배포 이력, 코드 구조를 함께 봐야 원인에 가까워집니다.


마무리

Java OOM은 단순한 메모리 부족 문제가 아닙니다. Java heap, Metaspace, Direct Memory, Native Thread, 컨테이너 메모리 제한 등 여러 원인으로 발생할 수 있습니다.

OOM이 발생하면 먼저 에러 메시지로 문제 영역을 좁혀야 합니다. 그다음 Heap dump, GC 로그, NMT, 컨테이너 이벤트를 통해 실제 메모리 흐름을 확인해야 합니다.

서비스 복구를 위해 메모리를 늘리는 조치는 필요할 수 있습니다. 하지만 거기서 멈추면 안 됩니다. 사라져야 할 객체가 왜 살아 있었는지, GC가 왜 회수하지 못했는지, 컨테이너 limit과 JVM 설정이 적절했는지까지 봐야 합니다.

Java OOM 분석은 메모리가 얼마나 부족했는지를 보는 일이 아닙니다.

왜 그 메모리가 계속 살아 있었는지를 찾는 일입니다.

댓글 0

로그인 후 댓글을 남길 수 있습니다.

아직 댓글이 없습니다.