Java OutOfMemoryError 자주 보는 원인과 해결 방향
마지막 업데이트

Java OutOfMemoryError 자주 보는 원인과 해결 방향


Java 서비스에서 OutOfMemoryError 가 났을 때 가장 흔한 실수는 모든 경우를 일반적인 heap 문제로 보는 것입니다. Java 메모리 장애는 단순히 “메모리가 꽉 찼다” 가 아니라, 어느 메모리 영역이 압박을 받았는지, 그리고 그 압박이 생겼는지를 구분해야 풀리는 경우가 많습니다.

짧게 말하면 핵심은 이것입니다. 정확한 OutOfMemoryError variant 를 먼저 잡아야 합니다. heap pressure, metaspace growth, direct memory exhaustion, large queued backlog 는 서로 다른 해결 경로를 가리킵니다.

Java 전체 증상 분기부터 다시 보고 싶다면 Java Troubleshooting Guide 로 먼저 돌아가도 좋습니다.


정확한 에러 모양부터 본다

OutOfMemoryError 메시지마다 뜻이 다릅니다.

예를 들면:

  • Java heap space
  • GC overhead limit exceeded
  • Metaspace
  • Direct buffer memory

이것들은 같은 원인을 뜻하지 않으며, 같은 대응을 해도 안 됩니다.

그래서 첫 단계는 “heap 을 늘리자” 가 아니라 “어느 메모리 영역이 실패했는가” 입니다.


OOM 장애는 운영에서 어떻게 보이나

프로세스가 죽거나 재시작되기 전에 보통 이런 조짐이 있습니다.

  • queue backlog 가 계속 커진다
  • GC activity 가 급격히 늘어난다
  • 프로세스가 죽기 전 latency 가 먼저 악화된다
  • heap 설정은 그럴듯한데 container memory limit 에 닿는다
  • 배포나 traffic 변화가 기존 retention 가정을 깨뜨린다

즉 OOM 은 갑자기 생기는 사건이 아니라, retention, backlog, class loading, native pressure 가 먼저 쌓여 오다가 마지막에 터지는 경우가 많습니다.


흔한 원인

1. 애플리케이션 객체가 heap 을 오래 잡고 있다

가장 익숙한 패턴입니다.

아래처럼 큰 객체나 너무 많은 reference 가 heap 을 천천히 채울 수 있습니다.

  • 큰 collection
  • bound 없는 cache
  • in-memory queue
  • request payload retention
  • response aggregation buffer

영원히 leak 되는 것이 아니더라도, 객체가 예상보다 오래 살아남으면 실제 traffic 에서는 JVM 이 버티지 못할 수 있습니다.

2. metaspace 가 커진다

모든 OOM 이 일반 객체 문제는 아닙니다.

과도한 class loading, dynamic proxy, bytecode generation, repeated classloader churn 이 metaspace 를 예상보다 높게 밀어 올릴 수 있습니다.

특히 아래 같은 시스템에서 더 중요합니다.

  • plugin loading
  • dynamic framework
  • 반복적인 redeploy 패턴
  • custom classloader 사용

3. direct 또는 native memory pressure

일부 장애는 일반적인 heap 바깥에서 생깁니다.

예를 들면:

  • direct byte buffer
  • JNI allocation
  • off-heap cache
  • container 안의 process memory overhead

이 경우 heap metric 은 그럴듯해 보여도 프로세스는 여전히 죽을 수 있습니다.

4. queue backlog 와 retained work 가 메모리를 부풀린다

자주 놓치는 패턴입니다.

thread pool, messaging buffer, request backlog 가 계속 커지면 queued work 자체가 많은 객체를 한꺼번에 붙잡게 됩니다.

즉 근본 문제는 object leak 보다 throughput collapse 인데, 마지막 모습만 메모리 장애로 보일 수 있습니다.

5. capacity assumption 이 틀어졌다

traffic, payload size, tenant 수, data shape 가 원래 JVM sizing 과 queue 설계를 넘어섰을 수 있습니다.

코드는 크게 안 바뀌었는데 workload 가 변해서 터지는 경우도 꽤 많습니다.


실전 점검 순서

1. 정확한 OutOfMemoryError variant 를 잡는다

그냥 “OOM 났다” 로 뭉개면 안 됩니다.

정확한 메시지 하나가 탐색 범위를 크게 줄여 줍니다.

2. pressure 가 heap 인지, metaspace 인지, native 인지 구분한다

이 구분 하나로 낭비되는 시간이 크게 줄어듭니다.

heap tuning 으로 direct buffer exhaustion 은 안 풀리고, metaspace 조정으로 queue retention 도 안 풀립니다.

3. cache, collection, queue, payload-heavy path 를 본다

애플리케이션이 예상보다 훨씬 많은 데이터를 붙잡을 수 있는 지점을 찾아야 합니다.

질문은 이렇습니다.

  • traffic 과 함께 무엇이 커지는가
  • retry 나 backlog 와 함께 무엇이 커지는가
  • upper bound 가 없는 곳은 어디인가

4. 최근 traffic 과 deployment 변화를 비교한다

incident 는 아래 변화 뒤에 더 잘 설명되는 경우가 많습니다.

  • 새 기능 경로 추가
  • 더 큰 request payload
  • 더 많은 concurrent work
  • 바뀐 cache 동작
  • 달라진 classloading 패턴

OOM 은 workload 변화로 보면 더 잘 이해되는 경우가 많습니다.

5. pressure source 가 분명해진 뒤에만 JVM sizing 을 바꾼다

메모리를 더 주면 시간을 벌 수는 있습니다. 하지만 진단을 대신해서는 안 됩니다.

pressure source 가 retention 이나 runaway backlog 라면, 더 큰 heap 은 같은 실패를 조금 늦출 뿐입니다.


예시: queue backlog 가 heap pressure 로 번지는 경우

ExecutorService pool = Executors.newFixedThreadPool(8);
for (Task t : tasks) {
    pool.submit(() -> process(t));
}

process(t) 가 느려지고 incoming work 는 계속 들어오면, queued task 가 payload object, reference, closure 를 오래 붙잡아 두면서 throughput incident 가 heap incident 로 바뀔 수 있습니다.

그래서 어떤 OOM 은 사실 backlog 문제가 메모리 문제처럼 보이는 경우도 많습니다.


유용한 JVM 옵션 하나

java -XX:+HeapDumpOnOutOfMemoryError -jar app.jar

첫 OOM 에서 heap dump 를 남기면, 프로세스가 죽은 뒤 감으로 추정하는 대신 실제 object graph 를 볼 수 있습니다.

저장 공간과 운영 정책이 허용한다면, 상당히 가치가 큰 안전장치입니다.


pressure source 를 찾은 뒤 무엇을 바꾸면 좋나

heap retention 이 문제라면

retention 을 줄이고 cache 와 queue 를 bound 하며 오래 사는 reference 를 없애야 합니다.

metaspace 가 문제라면

classloader 동작, dynamic code generation, redeploy 패턴을 봐야 합니다.

native 또는 direct memory 가 문제라면

heap 차트만 보지 말고 off-heap usage 와 container memory 가정을 추적해야 합니다.

backlog 가 문제라면

queue growth 와 throughput collapse 를 2차 증상이 아니라 1차 incident 로 다뤄야 합니다.

workload 가 sizing 을 넘어선 것이라면

어떤 경로가 메모리를 먹는지 이해한 뒤 의도적으로 resize 해야 합니다.


장애 중에 던져볼 질문

이 질문이 꽤 유용합니다.

실제로 실패한 메모리 영역은 어디였고, JVM 이 죽기 전에 그 영역에서 무엇이 자라고 있었나?

이 질문이 “heap 을 키울까?” 보다 훨씬 행동 가능한 답을 줍니다.


FAQ

Q. 그냥 heap size 를 늘리면 되나

어느 메모리 영역이 실패했는지 알기 전에는 권하지 않습니다.

Q. thread pool 도 memory pressure 를 만들 수 있나

그렇습니다. 큰 backlog 와 queued work 는 많은 객체를 한꺼번에 붙잡을 수 있습니다.

Q. 가장 빠른 첫 단계는 무엇인가

정확한 error variant 를 잡고 affected memory area 와 연결하는 것입니다.

Q. 모든 OOM 이 memory leak 인가

아닙니다. 고전적인 leak 도 있지만, backlog, workload 증가, sizing mismatch 때문에 나는 경우도 많습니다.


Sources:

먼저 읽어볼 가이드

검색 유입이 많은 핵심 글부터 이어서 보세요.