
💡 이 글을 읽으면 알 수 있는 것
- 메모리 장애가 발생했을 때 힙부터 의심하는 습관에서 벗어날 수 있어요.
- OutOfMemoryError와 StackOverflowError의 차이와 발생 원인을 명확하게 이해할 수 있어요.
- 힙, 스택, 메서드 영역, 다이렉트 메모리처럼 JVM 메모리 영역별 문제의 특징과 대응 방향을 정리할 수 있어요.
JVM에서 발생하는 메모리 문제가 모두 같은 종류는 아니에요.
힙에 객체가 과도하게 쌓이면 OutOfMemoryError가 발생하고, 호출 스택이 지나치게 깊어지면 StackOverflowError가 발생해요.
때로는 다이렉트 메모리처럼 힙 바깥 영역이 한계에 도달하면서 문제가 드러나기도 해요.
실무에서는 이런 장애를 만나면 흔히 “메모리가 부족하다”라고 해석하기 쉬워요.
하지만 메모리 문제를 이렇게 뭉뚱그려 보면 원인 파악이 늦어져요.
같은 메모리 장애처럼 보여도, 실제로는 어느 영역에 문제가 생겼는지에 따라 원인과 대응 방향이 달라지기 때문이에요.
이 글에서는 JVM 메모리 구조를 기준으로 각 영역에서 왜 문제가 발생하는지 살펴볼게요.
단순히 에러 이름을 구분하는 데서 그치지 않고, 메모리 영역과 예외를 함께 연결해 해석하는 관점을 정리해 볼게요.
OutOfMemoryError와 StackOverflowError의 근본적인 차이

이름만 보면 둘 다 메모리가 넘친 것처럼 보이지만, 실제 발생 방식은 달라요.
OutOfMemoryError(OOM) 는 JVM이 필요한 메모리 공간을 더 이상 확보하지 못할 때 발생해요.
여기서 메모리는 힙만 뜻하지 않아요.
메서드 영역이나 다이렉트 메모리처럼 JVM이 사용하는 다른 영역에서도 발생할 수 있어요.
StackOverflowError 는 전체 메모리가 부족해서 생기는 문제와는 달라요.
한 스레드의 호출 스택이 허용된 깊이를 넘을 때 발생해요.
보통 재귀 호출이 과도하게 깊어지거나, 메서드 호출이 계속 누적되면서 스택 프레임이 한계를 넘을 때 나타나요.
이 구분이 중요한 이유는 대응 방향이 다르기 때문이에요.
OOM이 발생했다면 객체가 왜 회수되지 않는지, 또는 한 번에 너무 많은 데이터를 메모리에 올리고 있지는 않은지 살펴봐야 해요.
반면 StackOverflowError가 발생했다면 재귀 구조나 비정상적인 호출 흐름부터 먼저 의심해야 해요.
자바 힙(Heap)에서 OOM이 발생하는 이유
자바 힙은 애플리케이션이 생성한 대부분의 객체가 저장되는 영역이에요.
그래서 OutOfMemoryError가 가장 흔하게 발생하는 영역이기도 해요.
힙 문제는 대체로 객체는 계속 만들어지는데, GC가 회수해야 할 객체를 충분히 정리하지 못할 때 드러나요.
여기서 중요한 것은 단순히 객체를 많이 만들었는지가 아니에요.
실제로 필요한 데이터가 많아서 힙이 부족한 것인지, 아니면 더 이상 필요하지 않은 객체가 참조 때문에 남아 있는 것인지 구분해야 해요.
전자는 데이터 과다이고, 후자는 메모리 누수에 가까운 문제예요.
대표적인 원인은 세 가지예요.
- 컬렉션의 무한 적재
List, Map, 캐시 같은 구조에 데이터를 계속 추가하면서 제거나 만료 정책을 두지 않으면 힙 사용량이 계속 증가해요. - 끊기지 않은 참조
논리적으로는 더 이상 필요 없는 객체라도 정적 필드나 수명이 긴 객체가 계속 참조하고 있으면 GC가 회수하지 못해요. 리스너를 등록한 뒤 해제하지 않는 경우가 대표적이에요. - 대량 데이터 일괄 처리
파일 전체를 한 번에 읽거나, 데이터베이스 조회 결과를 페이징 없이 모두 메모리에 올리면 힙 사용량이 급격히 커질 수 있어요.
힙 OOM을 분석할 때는 “객체를 얼마나 많이 만들었는가”보다 “GC가 왜 이 객체를 회수하지 못했는가”를 먼저 봐야 해요.
필요한 데이터 자체가 많다면 청크 처리나 페이징으로 적재량을 줄여야 하고, 불필요한 객체가 남아 있다면 참조 관계를 찾아 끊어야 해요.
스택(Stack) 문제: 깊이의 문제인가, 개수의 문제인가
스택을 이해할 때 가장 중요한 점은, 스택이 스레드마다 독립적으로 존재한다는 사실이에요.
그래서 스택 관련 문제는 크게 두 가지로 나눌 수 있어요.
한 스레드의 호출이 너무 깊어지는 경우와, 스레드 수가 지나치게 많아지는 경우예요.
StackOverflowError: 호출이 너무 깊게 쌓인 경우
StackOverflowError는 한 스레드 안에서 메서드 호출이 끝나지 않고 계속 누적될 때 발생해요.
종료 조건이 없는 재귀 호출이 대표적인 예예요.
이런 경우 스택 프레임이 계속 쌓이면서, 결국 해당 스레드에 할당된 스택 공간의 한계를 넘게 돼요.
OutOfMemoryError: 스레드가 너무 많은 경우
애플리케이션이 스레드를 지나치게 많이 생성하면, 각 스레드마다 별도의 스택 공간이 필요해요.
이때 JVM이나 운영체제가 더 이상 필요한 메모리를 확보하지 못하면 OutOfMemoryError가 발생할 수 있어요.
이 경우의 원인은 한 스레드의 스택이 깊은 것이 아니라, 스택을 가진 스레드가 너무 많다는 점이에요.
JNI를 통해 C/C++ 외부 라이브러리를 호출할 때 사용하는 네이티브 메서드 스택도 함께 주의해야 해요.
이 영역에서도 과도한 호출이나 비정상적인 반복이 이어지면 스택 관련 문제가 발생할 수 있어요.
외부 라이브러리와 연동할 때도 이 가능성을 함께 확인해야 해요.
메서드 영역과 런타임 상수 풀 문제
메서드 영역은 일반적인 객체를 저장하는 공간이 아니에요.
클래스 이름, 필드와 메서드 정보 같은 클래스 메타데이터와 런타임 상수 풀이 저장되는 영역이에요.
이 영역에서도 OutOfMemoryError가 발생할 수 있어요.
대표적인 원인은 런타임에 클래스를 과도하게 생성하는 구조예요.
프록시, 리플렉션, 바이트코드 조작 라이브러리(CGLib 등)를 사용해 클래스를 계속 만들어 내면, 클래스 메타데이터가 누적되면서 메서드 영역이 부족해질 수 있어요.
런타임 상수 풀도 비슷한 문제가 생길 수 있어요.
예를 들어 String.intern() 을 과도하게 호출하면 상수 풀에 문자열이 계속 쌓이면서 메모리 사용량이 비정상적으로 증가할 수 있어요.
여기서 주의할 점은 JVM 버전에 따라 구현 방식과 에러 메시지가 다르게 보일 수 있다는 점이에요.
JVM 명세에서는 이 영역을 메서드 영역이라고 부르지만, 과거 HotSpot JVM은 이를 PermGen으로 구현했어요.
Java 8 이후에는 네이티브 메모리를 사용하는 Metaspace로 바뀌었어요.
그래서 최근 환경에서는 이 영역 부족 문제가 java.lang.OutOfMemoryError: Metaspace 로 나타나는 경우가 많아요.
다이렉트 메모리(Direct Memory) OOM
다이렉트 메모리 문제는 실무에서 원인을 찾기 어려운 OOM 중 하나예요.
다이렉트 메모리는 자바 힙 바깥에서 네이티브 메모리를 직접 사용하는 영역이에요.
예를 들어 NIO의 ByteBuffer.allocateDirect() 를 사용해 다이렉트 버퍼를 과도하게 할당하고 제때 해제하지 않으면, 힙은 여유로워 보여도 애플리케이션에서는 OOM이 발생할 수 있어요.
그래서 힙 사용량만 보고 있으면 원인을 놓치기 쉬워요.
이런 문제를 분석할 때는 힙이 비어 있는지보다, 힙 바깥 메모리를 얼마나 사용하고 있는지 함께 봐야 해요.
힙에는 여유가 있는데도 OOM이 발생한다면 다이렉트 메모리 이슈를 먼저 의심해 볼 필요가 있어요.
원인별 에러 유형 한눈에 보기
JVM 메모리 문제는 모두 같은 방식으로 발생하지 않아요.
아래 표처럼 어느 영역에서, 어떤 이유로, 어떤 예외로 나타나는지를 함께 봐야 원인을 빠르게 좁힐 수 있어요.
| 문제 영역 | 발생 위치 | 대표 예외 메시지 | 흔한 원인 및 특징 | 주의할 점 |
| 힙 문제 | 자바 힙(Heap) | OutOfMemoryError: Java heap space | 객체 누적, 참조 유지로 인한 메모리 누수, 대량 데이터 일괄 적재 | GC가 있다고 해서 자동으로 해결되지는 않아요 |
| 스택 깊이 문제 | JVM 스택 | StackOverflowError | 무한 재귀 호출, 과도한 호출 중첩 | 전체 메모리 부족과는 성격이 달라요 |
| 스레드 과다 문제 | 전체 메모리 | OutOfMemoryError: unable to create new native thread | 스레드 풀 오설정, 무한 스레드 생성 | 스택 깊이가 아니라 스레드 수가 핵심이에요 |
| 메서드 영역 문제 | Metaspace / PermGen | OutOfMemoryError: Metaspace | 과도한 동적 클래스 생성, 프록시·리플렉션·바이트코드 조작 사용 | 객체가 아니라 클래스 메타데이터가 누적되는 문제예요 |
| 다이렉트 메모리 문제 | 힙 외부(Off-heap) | OutOfMemoryError: Direct buffer memory | 다이렉트 버퍼 과도 할당, 해제 지연 또는 누락 | 힙 모니터링 지표가 정상이더라도 발생할 수 있어요 |
실무에서 메모리 문제를 만났을 때 가장 먼저 해야 할 일은 “메모리가 부족하다”라고 막연히 판단하는 것이 아니에요.
먼저 “어느 영역이 한계에 도달했는가?”를 떠올려야 해요.
분석은 에러 로그에 찍힌 메시지를 해석하는 것에서 시작해요.
힙이 찬 것인지, 호출 스택이 깊어진 것인지, 클래스 메타데이터가 누적된 것인지, 다이렉트 메모리 사용량이 커진 것인지부터 구분해야 해요.
같은 OOM이라는 이름만 보고 무작정 힙 덤프부터 확인하면 실제 원인과는 멀어질 수 있어요.
JVM 메모리 구조를 이해해야 하는 이유도 여기에 있어요.
이 지식은 단순히 면접을 위해 외우는 개념이 아니에요.
실제 장애 상황에서 예외를 빠르게 해석하고, 원인을 구조적으로 좁혀 가기 위한 가장 실용적인 기준이에요.
'Language > Java ☕️' 카테고리의 다른 글
| 자바 객체는 메모리에 어떻게 배치될까요? 객체 생성 과정과 메모리 레이아웃 (0) | 2026.03.09 |
|---|---|
| 자바 메모리 구조: 힙만으로는 부족한 JVM 런타임 데이터 영역 이해하기 (0) | 2026.03.09 |
| Java static 키워드 완전 가이드 (0) | 2024.05.22 |
| 자바 메모리 구조 이해하기 (0) | 2024.05.22 |
| 자바의 가비지 컬렉션(GC) 한 번에 이해하기 (0) | 2024.02.02 |
안녕하세요, 저는 주니어 개발자 박석희 입니다. 언제든 하단 연락처로 연락주세요 😆