최신글

JVM 옵션 하나로 OOM 순간의 heap dump 남기기

반응형

heap OOM(OutOfMemoryError)을 분석하려면 문제가 발생한 순간의 heap dump가 필요합니다. 그런데 OOM이 발생하면 JVM 프로세스는 이미 종료된 뒤입니다. 프로세스가 죽는 순간에 어떻게 dump를 뜰 수 있을까요?

JVM은 heap OOM발생시 dump를 하는 옵션이 있습니다. 이 글에서는 JVM의 heap dump 옵션과 관련 배경에 대해 정리했습니다.

java -Xmx2g \
  -XX:+HeapDumpOnOutOfMemoryError \
  -XX:HeapDumpPath=/dumps/heap.hprof \
  -jar app.jar

 

heap OOM은 왜 분석하기 어려운가

JVM에서 실행되는 애플리케이션은 객체를 생성할 때 heap이라는 메모리를 사용합니다. heap은 유한한 자원이어서, heap보다 더 큰 메모리를 요구하는 객체를 생성하거나 GC로도 더 이상 heap을 정리하지 못하면 OOM이 발생하고 해당 프로세스는 종료됩니다.

  • 객체 생성 흐름
    → 자리 없으면 young generation만 정리 (minor GC)
    → 그래도 자리 없으면 heap 전체 정리 (full GC)
    → 그래도 자리 없으면 OOM (java.lang.OutOfMemoryError: Java heap space)

 

OOM을 분석하려면 애플리케이션 로그로 어림짐작하거나 heap dump 파일이 필요합니다. 문제는 heap dump가 문제가 발생한 시점에 떠야 의미가 있다는 점입니다. OOM이 발생하면 프로세스가 이미 종료되어, 정작 필요한 그 시점에는 dump를 뜰 수 없습니다.

 

OOM 순간에 heap dump하는 방법: JVM 옵션

JVM은 이 상황을 위한 옵션을 제공합니다. HeapDumpOnOutOfMemoryError 옵션을 지정하면, heap OOM이 발생하는 순간 프로세스가 종료되기 전에 JVM이 heap dump를 생성합니다. HeapDumpPath 옵션으로 dump 파일 위치를 지정합니다.

java -Xmx2g \
  -XX:+HeapDumpOnOutOfMemoryError \
  -XX:HeapDumpPath=/dumps/heap.hprof \
  -jar app.jar

 

heap OOM과 관련된 JVM 옵션은 아래와 같습니다.

  • -XX:+HeapDumpOnOutOfMemoryError 첫 OOM 발생 시 heap dump
  • -XX:HeapDumpPath=<경로> dump 저장 위치
  • -XX:+ExitOnOutOfMemoryError 첫 OOM에서 JVM을 즉시 종료
  • -XX:OnOutOfMemoryError=<명령> 첫 OOM에서 지정한 명령을 실행

 

heap dump 실습

실습 코드는 저의 GitHub에 공개되어 있습니다.
- https://github.com/choisungwook/portfolio/tree/master/computer_science/java/heapdump

실습 코드는 간단합니다. static 컬렉션은 GC가 회수하지 못하므로, 배열에 1MiB씩 계속 데이터를 넣어 heap이 가득 찰 때까지 쌓습니다.

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

/**
 * 메모리 누수를 재현하는 앱.
 * static 컬렉션은 GC가 회수하지 못하므로 heap이 가득 찰 때까지 계속 쌓인다.
 */
public class LeakApp {
  static final List<byte[]> CACHE = new ArrayList<>();

  public static void main(String[] args) throws InterruptedException {
    System.out.println("pid: " + ProcessHandle.current().pid());
    while (true) {
      CACHE.add(new byte[1024 * 1024]); // 1MiB씩 누적
      if (CACHE.size() % 16 == 0) {
        System.out.println("leaked: " + CACHE.size() + " MiB");
      }
      Thread.sleep(10);
    }
  }
}

 

Java 코드는 Docker container로 실행합니다. heap OOM을 빨리 발생시키기 위해 heap 크기를 64MB로 아주 작게 설정했습니다.

FROM eclipse-temurin:21-jdk

WORKDIR /app
COPY app/LeakApp.java .
RUN javac LeakApp.java

CMD ["java", "-Xmx64m", "-XX:+HeapDumpOnOutOfMemoryError", "-XX:HeapDumpPath=/dumps/heap.hprof", "LeakApp"]

 

실행한 지 1초도 안 되어 heap OOM이 발생합니다.

java.lang.OutOfMemoryError

 

 

heap dump 파일 분석

heap dump 파일은 hprof라는 바이너리 포맷입니다. 그래서 분석하려면 바이너리를 해석하는 도구가 필요합니다.

 

heap dump 파일을 분석하는 오픈소스 도구 중 하나는 Eclipse MAT입니다.
- https://eclipse.dev/mat/

 

MAT에서 Leak Suspects Report를 클릭하면, 어느 구간에서 memory leak이 의심되는지 분석해줍니다.

 

마무리

정리하면, OOM 순간의 heap dump는 프로세스가 죽은 뒤에 뜨는 것이 아니라, 죽기 직전에 JVM이 직접 남기게 하는 것입니다. 그 스위치가 HeapDumpOnOutOfMemoryError이고, 남은 hprof 파일은 Eclipse MAT 같은 도구로 분석하면 됩니다.

 

참고자료

반응형