소개
Java를 사용하다 보면 OutOfMemoryError (메모리 부족 오류)를 한 번쯤 마주칠 수 있습니다. 이 오류는 JVM이 더 이상 사용할 수 있는 메모리가 없을 때 발생하며, 발생 즉시 애플리케이션이 비정상 종료되므로 서비스 장애로 이어질 수 있습니다. 이번 글에서는 OutOfMemoryError가 발생하는 원인과 Java에서 이 오류가 나타나는 대표적인 상황들을 살펴보고, 주요 해결 방법을 단계별로 정리해보겠습니다. 최신 Java 버전(예: Java 8 이상)에서도 적용 가능한 대응 방법을 중심으로 설명합니다.
- 에러 로그
OutOfMemoryError가 발생하면 아래와 같이 오류 메시지와 스택 트레이스가 출력됩니다 (예시는 힙 메모리 부족 상황):
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.example.MyApp.processData(MyApp.java:15)
at com.example.MyApp.main(MyApp.java:8)
위 로그에서 Java heap space라는 상세 메시지를 통해 어떤 영역의 메모리가 부족한지 확인할 수 있습니다. 이처럼 OutOfMemoryError는 메시지에 따라 여러 종류로 나뉘며, 각각 원인이 되는 메모리 영역이 다릅니다. 대표적으로 "Java heap space", "GC overhead limit exceeded", "Metaspace", "Unable to create new native thread" 등의 형태로 나타날 수 있습니다 (Understand the OutOfMemoryError Exception) (Understand the OutOfMemoryError Exception).
- 원인
OutOfMemoryError는 말 그대로 프로그램에 할당된 메모리를 모두 소진하여 더 이상 객체를 할당할 공간이 없을 때 JVM이 던지는 에러입니다 (Understand the OutOfMemoryError Exception). Java에서는 메모리 영역에 따라 여러 가지 상황에서 이 오류가 발생할 수 있는데, 주요 원인과 대표 사례는 다음과 같습니다:
- Java 힙 공간 부족 (Java heap space): 가장 흔한 경우로, 힙 영역에 객체를 계속 생성하여 **힙 최대 크기(-Xmx)**를 초과하면 발생합니다. 가비지 컬렉터(GC)가 동작해도 남은 객체들을 모두 수용하기에 힙이 부족한 상황입니다 (Understand the OutOfMemoryError Exception). 예를 들어, 한 번에 너무 큰 데이터를 메모리에 로드하거나, 사용이 끝난 객체를 해제하지 않고 계속 참조를 유지(메모리 누수)하면 힙이 고갈될 수 있습니다.
- GC 오버헤드 한계 초과 (GC overhead limit exceeded): GC가 과도하게 동작하여 CPU 시간의 98% 이상을 쓰고도 힙 메모리의 2% 미만만 회수하는 상태가 몇 차례 지속될 때 발생하는 에러입니다 (OutOfMemoryError: Types, Causes, and Solutions). 쉽게 말해 GC가 끊임없이 실행되지만 쓸만한 메모리를 확보하지 못하는 상황으로, 실질적으로는 힙 메모리가 부족한 경우와 마찬가지입니다 (이 오류도 종종 Java heap space 오류와 교대로 나타날 수 있습니다 (OutOfMemoryError: Types, Causes, and Solutions)).
- 메타스페이스 부족 (Metaspace): Java 8부터는 클래스 메타데이터를 저장하는 공간으로 **메타스페이스(Metaspace)**를 사용합니다 (이전 버전에서는 PermGen 영역). 애플리케이션에서 너무 많은 클래스나 메소드 정의를 로드하거나, 클래스 로더를 반복 사용하면서 메타스페이스를 가득 채우면 OutOfMemoryError: Metaspace가 발생합니다 (Understand the OutOfMemoryError Exception). 메타스페이스는 기본적으로 필요 시 자동으로 확장되지만, 시스템 메모리 한계에 다다르거나 -XX:MaxMetaspaceSize 제한에 걸리면 더 이상 늘리지 못하고 오류가 발생합니다 (Understand the OutOfMemoryError Exception). (참고: Java 8 이전에는 PermGen 영역의 크기 제한(-XX:MaxPermSize)으로 인해 비슷한 오류가 발생하였으며, Java 8부터 PermGen이 제거되고 메타스페이스로 대체되었습니다 (Java OutOfMemoryError Exceptions: Causes & Fixes [Tutorial] - Sematext).)
- 너무 큰 배열 할당: 배열 생성 시 JVM 구현 한계를 넘는 초대형 배열을 할당하려 하면 Requested array size exceeds VM limit라는 메시지와 함께 OOM 오류가 발생합니다. 이는 사용할 수 있는 힙 용량과 상관없이, 배열 크기 자체가 32비트 인덱스 한계를 넘어설 때 발생하는 오류입니다 (Understand the OutOfMemoryError Exception). 예를 들어 한 번에 수억 이상의 요소를 가지는 배열을 생성하려 하면 이런 오류가 날 수 있습니다.
- 네이티브 메모리 부족: 힙 이외의 영역에서 메모리가 부족해도 OOM이 발생합니다. 예를 들어, 스레드를 과도하게 생성하여 OS의 스레드 생성 한도나 네이티브 메모리를 소진하면 OutOfMemoryError: Unable to create new native thread 오류가 발생합니다 (OutOfMemoryError: Types, Causes, and Solutions) (OutOfMemoryError: Types, Causes, and Solutions). 또한 NIO의 Direct Buffer를 많이 할당하고 해제하지 않으면 OutOfMemoryError: Direct buffer memory 오류가 날 수 있습니다 (OutOfMemoryError: Types, Causes, and Solutions). 이 밖에도 JNI를 통해 네이티브 코드에서 메모리 할당 실패 시, 또는 swap 공간 부족 등의 이유로 OOM이 발생할 수 있습니다 (Understand the OutOfMemoryError Exception). 이러한 네이티브 영역 문제는 보통 힙이나 메타스페이스 외에 시스템 메모리 자원이 고갈된 경우입니다.
상세 메시지에 따라 원인이 되는 메모리 영역이 다르므로, 에러 메시지를 통해 어떤 종류의 OOM인지 파악하는 것이 첫 단계입니다. 다음으로는 각 상황에 대한 대응 방법을 알아보겠습니다.
- 해결 방법
OOM 상황을 해결하려면 근본 원인에 따른 대응이 필요합니다. 즉, 단순히 JVM 옵션으로 메모리 크기만 늘리는 임시 처방보다는, 왜 메모리가 부족해졌는지를 파악하고 적절한 조치를 취해야 합니다. 아래에 대표적인 해결 방법들을 단계별로 설명합니다.
- 원인 진단 및 메모리 프로파일링: 우선 어떤 객체들이 메모리를 사용하고 있는지를 알아내는 것이 중요합니다. 메모리 누수가 의심되거나 메모리 사용 패턴을 알고 싶다면, 힙 덤프(heap dump)를 뜬 뒤 분석해보는 것이 도움이 됩니다. JVM 실행 시 -XX:+HeapDumpOnOutOfMemoryError 옵션을 주면 OOM 발생 시 자동으로 힙 덤프를 생성할 수 있습니다 (profiling - Tools for OutOfMemoryError java heap space analysis - Stack Overflow). 이렇게 얻은 파일(.hprof)을 Eclipse MAT이나 VisualVM과 같은 메모리 분석 도구로 열어서 어떤 객체가 얼마나 메모리를 차지하는지, GC 루트에서 참조가 끊어지지 않아 해제되지 않는 객체(누수)의 존재 여부 등을 확인합니다. 프로파일러나 모니터링 툴(JVisualVM, JProfiler 등)을 애플리케이션에 연결해 실행 중인 상태에서 메모리 사용량과 객체 수 변화를 관찰하는 것도 방법입니다. 이러한 분석을 통해 메모리 누수가 발견되면 해당 코드를 수정하여 불필요한 객체가 계속 남지 않도록 하는 것이 궁극적인 해결책입니다.
- JVM 힙 메모리 크기 조정: 애플리케이션이 실제로 더 많은 메모리가 필요하다면 JVM 힙 크기를 늘려주는 것을 고려해야 합니다. JVM 옵션 -Xmx로 최대 힙 크기를 지정할 수 있으며, 필요에 따라 -Xms로 초기 힙 크기도 설정합니다. 예를 들어, 기본 힙 크기에서 OOM이 발생한 프로그램을 실행할 때:위와 같이 OOM이 발생했다면, 힙을 충분히 늘려서 다시 실행합니다:이처럼 메모리가 부족해서 생긴 Java heap space 오류의 경우 힙을 늘리면 문제를 완화할 수 있습니다 (Java OutOfMemoryError Exceptions: Causes & Fixes [Tutorial] - Sematext). 다만 무조건 힙을 키우는 것이 만능 해결책은 아니며, 애플리케이션의 용도에 비해 과도하게 적은 힙이 할당되어 있었던 상황이나 일시적으로 데이터량이 증가한 경우 등에 우선 유효한 방법입니다. 힙을 늘려도 계속 문제가 발생한다면 근본적인 메모리 누수 문제를 의심해야 합니다.
- # 힙 최대 크기를 256MB로 늘려서 실행 $ java -Xmx256m ExampleApp (정상 실행됨, OutOfMemoryError 발생 안 함)
- # 기본 힙 크기로 실행 (예: -Xmx16m 정도) -> OOM 발생 $ java ExampleApp Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
- GC 튜닝: GC 오버헤드로 인한 OOM의 경우 또는 메모리 사용량은 적절한데 GC에 너무 시간이 많이 소비되는 경우라면 Garbage Collection 튜닝을 고려해야 합니다. 먼저 -XX:+UseGCOverheadLimit 옵션으로 GC Overhead Limit 기능이 기본 활성화되어 있는데, 필요 시 이 제한을 해제할 수 있습니다 (-XX:-UseGCOverheadLimit 사용) (Understand the OutOfMemoryError Exception). 그러나 제한을 끄는 것은 근본 해결이 아니므로, 근본적으로는 GC 동작을 최적화해야 합니다. 방법으로는 JVM의 GC 로그를 활성화(-Xlog:gc* 옵션 등)하여 GC 빈도와 소요 시간을 분석하고, 적절한 GC 알고리즘이나 파라미터를 선택하는 것이 있습니다. 예를 들어, 대용량 메모리를 사용하는 애플리케이션에는 Java 11 이상의 G1 GC (기본 GC)를 튜닝하거나, 필요에 따라 Shenandoah나 ZGC 같은 최신 GC를 선택함으로써 GC로 인한 성능 저하와 메모리 회수 이슈를 개선할 수 있습니다. 또한 Eden/Survivor 비율이나 NewRatio 등을 조정하여 객체 churn이 많은 환경에 맞게 젊은 세대 크기를 늘리는 등의 튜닝도 고려해볼 수 있습니다. 단, GC 튜닝은 애플리케이션의 특성에 따라 달라지므로, 메모리 프로파일링 단계에서 파악한 객체 생존 기간과 할당 패턴을 기반으로 접근해야 합니다.
- 메타스페이스 크기 조정 및 클래스 로드 관리: 메타스페이스 부족으로 OOM이 발생했다면 우선 메타스페이스 크기 제한을 확인해야 합니다. 기본적으로 MaxMetaspaceSize를 따로 지정하지 않으면 메타스페이스는 제한 없이 필요할 만큼 확장되지만, 컨테이너 환경 등에서는 메모리 제약으로 인해 사실상 한계가 있을 수 있습니다. -XX:MaxMetaspaceSize 옵션을 사용 중이라면 값을 더 크게 늘려볼 수 있습니다 (Understand the OutOfMemoryError Exception) (Understand the OutOfMemoryError Exception). 메타스페이스도 결국 네이티브 메모리 영역을 사용하므로, 시스템 메모리가 충분해야 합니다. 한 가지 방법으로 사용하지 않는 클래스들을 언로드(unload) 해서 메타스페이스를 확보하는 방법이 있지만, 일반적으로 애플리케이션에서 임의로 클래스 언로드를 제어하기는 어렵습니다. 대신, 메타스페이스 OOM이 발생하는 근본 원인을 찾아야 합니다. 예를 들어 어플리케이션을 반복적으로 배포(redeploy)하는 서버 환경에서 OOM이 난다면, 이전 배포된 클래스나 ClassLoader가 해제되지 않고 쌓이는 PermGen/Metaspace 누수일 수 있으므로 이를 해결해야 합니다. 이런 경우 코드 또는 프레임워크 설정을 조정하여 사용하지 않는 클래스 로더를 명시적으로 해제하거나, 문제를 일으키는 라이브러리를 교체해야 합니다. 요약하면, 메타스페이스 OOM에는 (1) 메타스페이스 크기 증설과 (2) 클래스 로딩 로직 점검 두 방향의 대응이 필요합니다.
- 기타: 네이티브 자원 관련 조치: 스레드 생성이나 Direct 메모리 등 힙 외의 영역에서 발생한 OOM의 경우에는 해당 자원을 늘리거나 사용 패턴을 바꾸는 방향으로 해결합니다. Unable to create new native thread 오류의 경우, 과도한 스레드 생성을 줄이도록 설계 변경을 하거나, JVM 프로세스에 생성할 수 있는 스레드 수의 OS 상한에 도달한 것이라면 OS 설정 (ulimit 등)을 조정해야 합니다. 또한 물리적인 메모리가 부족해서 발생한 것일 수도 있으므로 서버 메모리 증설이나 컨테이너 메모리 할당량 조정도 고려합니다. Direct buffer memory 오류의 경우에는 -XX:MaxDirectMemorySize 옵션으로 Direct 메모리 영역을 늘릴 수 있으며, NIO 버퍼를 사용하는 코드를 점검하여 **사용 후 빠르게 해제(또는 Cleaner 통해 GC)**되도록 합니다. 이처럼 네이티브 영역 OOM은 원인에 따라 대응 방법이 다양하므로, 상황에 맞게 OS 레벨 모니터링과 튜닝도 병행해야 합니다 (Understand the OutOfMemoryError Exception) (OutOfMemoryError: Types, Causes, and Solutions).
상황별 권장 대처 요약
- 데이터량 증가로 인한 일시적 힙 부족: JVM 힙 크기를 늘리고 (또는 일시적으로 필요한 데이터는 디스크나 캐시 활용) 추후 모니터링합니다.
- 메모리 누수가 의심되는 경우: 힙 덤프를 분석하여 누수 객체를 찾아 코드 수정으로 근본 원인 제거가 최우선입니다.
- GC Overhead Limit Exceeded 발생: 힙 부족과 유사하므로 힙을 증설하고, GC 로그를 통해 필요시 GC 튜닝을 진행합니다. 그래도 해결되지 않으면 메모리 누수를 재확인합니다.
- 메타스페이스 OOM: MaxMetaspaceSize 확대로 즉각 대응하고, 너무 많은 동적 클래스 로드나 클래스Loader 누수가 없는지 애플리케이션을 점검합니다.
- 스레드/네이티브 메모리 OOM: 스레드 풀 사용 등으로 스레드 생성을 제어하고, Direct 메모리 사용량을 조절하는 등 자원 사용 패턴을 개선합니다. 또한 시스템에 충분한 메모리를 할당합니다.
- 예제 코드로 살펴보기
마지막으로, 간단한 코드 예제를 통해 OutOfMemoryError를 직접 확인하고 해결해보겠습니다. 아래 코드는 큰 배열을 할당하여 의도적으로 OOM을 발생시키는 예제입니다:
// 메모리 부족 예제 (주의: 실행 시 OOM 발생)
public class OOMExample {
public static void main(String[] args) {
// 약 10억 개의 문자열 객체를 담을 수 있는 배열 생성 시도
String[] data = new String[1000 * 1000 * 1000];
}
}
위 코드를 기본 설정으로 실행하면 java.lang.OutOfMemoryError: Java heap space 오류가 발생할 것입니다. 이제 실행 시 JVM 힙 크기를 늘려서 (-Xmx 옵션) 재실행해보면, 오류 없이 실행되거나 더 큰 배열을 할당할 수 있게 됩니다. 이 실험을 통해 힙 메모리 부족 OOM은 메모리 설정을 조정하여 해결 가능함을 확인할 수 있습니다. 반면에, 이런 조치로도 해결되지 않는다면 메모리 누수나 설계 문제를 의심하고 앞서 소개한 프로파일링 기법으로 원인을 찾아 수정해야 합니다.
결론
Java의 OutOfMemoryError는 발생 원인에 따라 다양한 형태로 나타나지만, 근본적으로는 메모리 자원의 한계를 넘어섰을 때 발생하는 오류입니다. 따라서 문제를 해결하려면 증상에 맞는 적절한 대응—메모리 할당량 조정, 코드상의 누수 해결, GC 튜닝, 필요한 경우 인프라 확장—을 수행해야 합니다. 무엇보다 사전에 메모리 사용량을 모니터링하고 애플리케이션 특성에 맞게 메모리를 관리하는 것이 중요합니다. 이번 글에서 다룬 내용을 참고하여, OOM 발생 시 당황하지 말고 단계별로 원인 파악과 해결을 시도해보세요. 정상적인 환경에서는 GC를 통해 자동으로 메모리를 관리해주지만, 결국 개발자의 세심한 메모리 관리와 점검이 안정적인 Java 애플리케이션 운영의 열쇠임을 기억해야겠습니다.
(image) JVM 메모리 구조 개요: 왼쪽 주황색 영역이 힙(Heap Young/Old 세대)이며, 오른쪽 파란색 영역들이 네이티브 메모리(Metaspace, Threads, GC, Direct Buff 등) 영역을 나타냅니다. OutOfMemoryError는 힙 메모리가 부족한 경우뿐 아니라 메타스페이스나 스레드처럼 네이티브 메모리 영역이 부족한 경우에도 발생할 수 있습니다. 따라서 애플리케이션 특성에 맞게 적절한 메모리 영역 크기를 설정하고, 모니터링을 통해 이상 징후를 조기에 발견하는 것이 중요합니다. (Understand the OutOfMemoryError Exception) (OutOfMemoryError: Types, Causes, and Solutions)
'IT > Java' 카테고리의 다른 글
ConcurrentModificationException (반복 중 컬렉션 변경 오류) 발생 원인과 해결 방법 (0) | 2025.03.14 |
---|---|
클래스 로드 오류 해결 방법 (ClassNotFoundException 및 NoClassDefFoundError) (0) | 2025.03.11 |
Java 인증서 추가 방법 ( SunCertPathBuilderException 해결 ) (0) | 2021.10.27 |
Quartz 스케줄러 사용하기 (0) | 2020.06.15 |
댓글