IT·리눅스/Java

java.lang.OutOfMemoryError: Java heap space - 원인 진단부터 해결까지

jmineekim 2026. 7. 5. 10:55

java.lang.OutOfMemoryError: Java heap space - 원인 진단부터 해결까지

서비스가 잘 돌아가다가 어느 순간 응답이 뚝 끊기고, 로그를 열어보면 java.lang.OutOfMemoryError: Java heap space 한 줄이 딱 찍혀 있는 상황, 백엔드 개발자라면 한 번씩은 다 겪어보셨을 거예요. 저도 신입 때 이 로그를 처음 봤을 때 "힙이 뭐길래 터졌다는 거지?" 하고 무작정 -Xmx부터 늘려서 잠깐 넘긴 적이 있는데요, 며칠 뒤 똑같이 터지고 나서야 근본 원인을 찾아야 한다는 걸 알았습니다. 오늘은 이 에러가 정확히 뭘 의미하고, 어떻게 원인을 찾아서 진짜로 해결하는지 정리해 볼게요.

개념 / 원리

java.lang.OutOfMemoryError: Java heap space는 JVM이 새 객체를 힙(Heap)에 할당하려고 하는데 공간이 부족하고, GC를 아무리 돌려도 회수되지 않아서 결국 던지는 에러입니다. "메모리가 부족합니다"라는 뜻이라기보다, "GC로도 못 지우는 살아있는 객체가 힙을 다 먹었습니다"에 더 가깝습니다.

힙은 JVM 실행 중 모든 객체(new로 만든 인스턴스, 배열, 대부분의 문자열)가 저장되는 공간입니다. 크기는 시작할 때 -Xms(최소)와 -Xmx(최대) 옵션으로 정해지고, 부족하면 자동으로 늘어나다가 -Xmx 상한에 부딪히면 이 에러가 납니다.

원인은 크게 세 가지입니다.

원인 특징
힙 크기 자체가 작음 데이터·트래픽 규모에 비해 -Xmx가 작을 때. 재시작하면 잠깐 버팀
메모리 누수(Memory Leak) 참조가 안 끊겨서 GC가 못 지움. 시간이 지날수록 힙 사용량이 계단식으로 증가
일시적 폭증 대용량 파일 로딩, 조인 결과 통째로 메모리에 올림 등. 특정 요청에서만 터짐

실전 예제

먼저 재현부터 해봅시다. 아래는 강제로 힙을 채우는 코드입니다.

import java.util.ArrayList;
import java.util.List;

public class HeapOomDemo {
    public static void main(String[] args) {
        List<byte[]> leaks = new ArrayList<>();
        int i = 0;
        while (true) {
            leaks.add(new byte[1024 * 1024]); // 1MB씩 계속 쌓기
            System.out.println("allocated " + (++i) + "MB");
        }
    }
}

이걸 힙 제한과 힙 덤프 옵션을 걸어서 돌립니다.

javac HeapOomDemo.java
java -Xmx128m \
     -XX:+HeapDumpOnOutOfMemoryError \
     -XX:HeapDumpPath=./heap.hprof \
     HeapOomDemo

100~120MB 부근에서 에러가 나고, 현재 디렉터리에 heap.hprof 덤프 파일이 남습니다. 운영에서는 -XX:+HeapDumpOnOutOfMemoryError를 반드시 걸어두세요. 이거 없이 OOM이 나면 원인 분석이 거의 불가능합니다.

실시간 힙 상태는 jstat으로 봅니다.

jstat -gcutil <PID> 1000

E(Eden), O(Old) 열의 사용률을 보고, O가 90%를 넘는데도 안 떨어지면 Old 영역 누수 신호입니다.

힙 덤프 분석은 Eclipse MAT(Memory Analyzer Tool)이 가장 편합니다. 열어서 Leak Suspects 리포트를 돌리면 어떤 객체가 얼마나 물고 있는지 트리로 보여줍니다.

간단히 CLI로 훑어볼 때는:

jmap -histo:live <PID> | head -30

인스턴스 수와 총 크기 순으로 정렬된 클래스 리스트가 나옵니다. byte[]char[]가 상위에 있고 크기가 이상하게 크면 대용량 데이터를 물고 있다는 뜻이에요.

자주 하는 실수

1. -Xmx부터 늘리기. 저도 처음엔 이랬습니다. 4GB로 늘리면 4GB까지 채워지고 터집니다. 누수라면 시간만 벌 뿐이에요. 먼저 덤프로 원인부터 확인하세요.

2. static Map에 캐시 대충 쌓기. private static final Map<String, Object> cache = new HashMap<>();에 만료 없이 계속 넣는 코드, 정말 흔합니다. Caffeine이나 Guava Cache처럼 크기·시간 제한이 있는 라이브러리로 바꾸는 게 안전합니다.

3. ThreadLocal 안 지우기. Tomcat처럼 스레드를 재사용하는 환경에서 ThreadLocal에 값을 넣고 remove()를 안 하면 스레드가 살아있는 한 참조가 남습니다. 요청 처리 끝에 finally { threadLocal.remove(); } 반드시.

4. 에러 메시지 뒷부분을 안 읽음. java.lang.OutOfMemoryError는 뒤에 붙는 문자열이 진짜 원인입니다. Java heap space가 아니라 Metaspace, GC overhead limit exceeded, Direct buffer memory, unable to create new native thread면 원인과 해결이 완전히 다릅니다. 뒷문자열을 정확히 읽으세요.

마무리

정리하면 세 줄입니다.

  • Java heap space OOM은 "GC로도 못 지우는 객체가 힙을 다 먹은 상태"다.
  • -Xmx 튜닝 전에 -XX:+HeapDumpOnOutOfMemoryError로 덤프부터 남기고 MAT으로 원인을 찾는다.
  • 실무 누수 3대장은 무한 캐시 static Map, 미제거 ThreadLocal, 스트리밍 없이 통째로 로딩하는 대용량 처리다.
반응형