Out-of-Memory(OOM)는 왜 발생하고, 어떻게 예방할까?

2024. 11. 30. 22:18·💻 Dev/Java & OOP

 

Java 애플리케이션을 개발하다 보면 OutOfMemoryError라는 런타임 에러를 마주칠 때가 있다.

JVM이 더 이상 메모리를 할당할 수 없을 때 발생하는 이 에러는 코드나 환경 문제를 나타내는 강력한 신호다.

그래~서 OOM 에러가 발생하는 이유와 이를 예방하기 위한 방법을 알아보았다.

 

Out-of-Memory 에러는 언제 발생할까?

OOM 에러는 자바의 메모리 구조와 밀접한 관련이 있다.

JVM은 Heap, Stack, Metaspace 등으로 나뉘어 메모리를 관리하는데 이 중 특정 영역이 고갈되면 문제가 생긴다.
각각의 상황을 예제와 함께 살펴보자.

 

1. Heap 메모리가 부족한 경우

Heap 영역은 객체를 저장하는 공간이다. 프로그램이 너무 많은 객체를 생성하거나 메모리를 효율적으로 관리하지 못하면 고갈된다.

예를 들어, 아래와 같은 코드가 있을 때

public class HeapOOM {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        while (true) {
            list.add("가자아아아!");
        }
    }
}

위 코드는 끝없이 문자열을 ArrayList에 추가한다.

 

결국 메모리가 부족해져 java.lang.OutOfMemoryError: Java heap space 에러가 발생한다. 즉, 대규모 데이터를 처리하거나 컬렉션 객체를 잘못 사용했을 때 흔히 나타난다.

 

2. GC Overhead Limit 초과

GC(Garbage Collector)가 메모리를 정리하려고 애써도 공간이 회수되지 않으면 OOM 에러 중 GC overhead limit exceeded가 발생한다.

이는 메모리가 부족해 GC가 대부분의 시간을 소모하고, 프로그램이 정상적으로 실행되지 않을 때 나타난다.
정확히는 GC가 수행되어 새로 확보된 메모리가 전체 메모리의 2% 미만이면 발생한다.

 

예를 들어, 아래처럼 대량의 객체를 반복적으로 생성하면 GC가 이를 처리하지 못할 수 있다.

public class GCOverheadOOM {
    public static void main(String[] args) {
        HashMap<Integer, String> map = new HashMap<>();
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            map.put(i, "Data " + i);
        }
    }
}

이 경우 JVM이 계속 GC만 실행하고도 메모리를 확보하지 못하니 OOM이 발생한다.

 

3. Metaspace 부족

Java 8부터는 PermGen 영역이 Metaspace로 대체되었는데, 이 공간에는 클래스 메타데이터가 저장된다.

동적으로 클래스를 많이 로드하거나 리플렉션을 남발하면 Metaspace가 부족해질 수 있다.

public class MetaspaceOOM {
    public static void main(String[] args) throws Exception {
        while (true) {
            ClassPool pool = ClassPool.getDefault();
            pool.makeClass("Class" + System.nanoTime()).toClass();
        }
    }
}

위 코드는 무한히 새로운 클래스를 생성해 Metaspace를 고갈시킨다.
이는 동적 클래스 로딩을 사용하는 프레임워크(Spring, Hibernate 등)에서 잘못된 설계로 인해 발생할 수 있다.

 

4. Direct Memory 부족

NIO 기반 애플리케이션은 Heap이 아닌 Direct Memory를 사용하는데, 이를 과도하게 할당하면 문제가 생긴다.

JVM 옵션으로 설정한 Direct Memory 크기를 초과하면 에러가 발생한다.

public class DirectMemoryOOM {
    public static void main(String[] args) {
        while (true) {
            ByteBuffer.allocateDirect(1024 * 1024);
        }
    }
}

이 코드는 Direct Memory를 계속 할당하며, java.lang.OutOfMemoryError: Direct buffer memory를 유발한다.

 

OOM 에러를 예방하는 방법

OOM은 코드와 환경의 문제를 개선하면 충분히 예방할 수 있다. 아래는 실제로 적용 가능한 해결책들이다.

1. JVM 메모리 크기 조정

첫 번째 방법으로는 JVM의 메모리 크기를 적절히 설정하는 것이다.
예를 들어 Heap 메모리가 부족한 경우 다음과 같이 JVM 옵션을 추가해 메모리를 늘릴 수 있다.

java -Xms512m -Xmx4g MyApp

여기서 -Xms는 초기 Heap 크기, -Xmx는 최대 Heap 크기를 의미한다.

 

필요하다면 Metaspace나 Direct Memory 크기도 -XX:MaxMetaspaceSize나 -XX:MaxDirectMemorySize 옵션으로 조정할 수 있다.

 

2. 메모리 누수 방지

메모리 누수는 OOM의 주요 원인 중 하나다.
예를 들어 사용하지 않는 객체를 계속 참조하고 있다면 GC가 이를 회수하지 못한다. 이를 해결하려면 다음을 염두에 두자

  • 컬렉션에서 사용 후 객체를 제거한다.
  • 전역 변수로 객체를 참조하지 않도록 한다.

또한, 도구를 사용해 메모리 누수를 점검할 수 있다. Eclipse Memory Analyzer (MAT)나 VisualVM을 활용하면 메모리 누수가 발생한 객체를 쉽게 파악할 수 있다.

 

3. 데이터 처리 최적화

대규모 데이터를 처리할 때는 한 번에 메모리에 모두 로드하지 않고, 스트리밍 방식으로 처리하자.
예를 들어, 아래 코드는 파일을 한 줄씩 읽어 메모리 사용을 최소화한다.

public class FileReaderExample {
    public static void main(String[] args) throws Exception {
        try (BufferedReader reader = new BufferedReader(new FileReader("largeFile.txt"))) {
            String line;
            while ((line = reader.readLine()) != null) {
                // 한 줄씩 처리
                System.out.println(line);
            }
        }
    }
}

 

4. GC 튜닝과 모니터링

GC를 효과적으로 활용하기 위해 GC 옵션을 설정하고, 로그를 활성화해 문제를 분석할 수 있다.

 

단적인 예시로 각 애플리케이션에 최적화된 GC로 세팅해볼 수 있다.

java -Xlog:gc -XX:+UseG1GC MyApp

G1GC는 대규모 Heap을 효과적으로 관리하며, 현대 애플리케이션에 적합하다.
또한, Prometheus나 Grafana 같은 도구로 JVM 메모리 사용량을 모니터링하면 OOM 발생 가능성을 사전에 파악할 수 있다.

 

 

 

 

 

 

참고
Out Of Memory 오류

저작자표시 비영리

'💻 Dev > Java & OOP' 카테고리의 다른 글

Stack은 왜 상속의 실패 사례일까?  (0) 2024.12.12
System.out.println을 실무에서 사용하면 안되는 이유  (0) 2024.12.05
가비지 컬렉터(GC)의 Roots  (0) 2024.12.04
instanceof 사용을 지양해야하는 3가지 이유  (0) 2024.11.29
JVM Warm-up, feat. 스프링 첫 요청이 오래 걸리는 이유  (0) 2024.11.28
JDK 8에서 Perm 영역이 삭제된 이유  (1) 2024.11.25
'💻 Dev/Java & OOP' 카테고리의 다른 글
  • System.out.println을 실무에서 사용하면 안되는 이유
  • 가비지 컬렉터(GC)의 Roots
  • instanceof 사용을 지양해야하는 3가지 이유
  • JVM Warm-up, feat. 스프링 첫 요청이 오래 걸리는 이유
현주먹
현주먹
끄적끄적 개발.log
  • 현주먹
    현주먹의 개발로그
    현주먹
  • 전체
    오늘
    어제
    • 전체글 (162)
      • 👶🏻 CS (15)
        • Operating System (8)
        • Database (4)
        • Data Structure (2)
        • Software Engineering (1)
      • 💻 Dev (54)
        • Java & OOP (24)
        • Spring (4)
        • JPA (5)
        • Test Code (1)
        • Database (1)
        • JSP & Servlet (13)
        • Etc (6)
      • 💡 Algorithm (25)
        • 인프런 (9)
        • 백준 (16)
      • 🛠 DevOps & Tool (11)
        • Linux (4)
        • AWS (1)
        • Git (2)
        • Etc (4)
      • 📝 끄적끄적 (57)
        • 후기 및 회고 (5)
        • TDD, 클린 코드 with Java 17기 (3)
        • F-Lab (23)
        • 🖥️ 자바의 정석 (11)
        • 📖 Clean Code (3)
        • 항해99 코테 스터디 (11)
  • 블로그 메뉴

    • 🐈‍⬛ GitHub
    • TIL
  • 인기 글

  • 태그

    ==와 equals()
    인프런 단어뒤집기
    코딩테스트준비
    PostGreSQL함수
    NextSTEP
    백준
    C
    에프랩
    jsp
    객체지향
    99클럽
    티스토리챌린지
    자바의정석
    오블완
    개발자취업
    인프런 특정문자뒤집기
    f-lab 후기
    til
    데브클럽
    자바의신절판
    개발자멘토링
    TDD 클린 코드 with Java
    JPA
    오라클
    F-Lab
    에프랩 후기
    로또 미션
    코테스터디
    백준10250
    항해99
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.2
현주먹
Out-of-Memory(OOM)는 왜 발생하고, 어떻게 예방할까?
상단으로

티스토리툴바