Java Heap Dump 분석하기

Spring으로 개발한 웹어플리케이션을 운영하다보면, 많은 트래픽으로 인해 또는 구현상의 버그로 인해(보통은 이 경우겠지만) Heap의 사용량이 순간적으로 증가할 수 있다.
이 경우 GC(Garbage Collection)가 과도하게 일어나면서 어플리케이션의 성능이 저해되거나, 심한 경우에는 OOM(Out Of Memory)이 발생하여 결국 어플리케이션이 죽게 된다.
이 때 Heap의 높은 사용량을 만든 주범을 알아내기 위해 Heap Dump를 해야할 일이 생긴다. 즉 장애가 났을 때의 Heap 상태를 기록으로 남겨 그 당시에 어떤 Java 객체들이 많이 만들어졌는지 분석해보아야 한다.

Spring Boot Application 작성

우선 아래와 같이 /hello라는 요청이 들어왔을때, 엄청난 수의 Java Object들을 생성하는 간단한 Spring Boot 어플리케이션을 만들어보았다.

// SpringDemoApplication.java

@SpringBootApplication
public class SpringDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringDemoApplication.class, args);
    }
}
// DemoController.java

@RestController
@RequestMapping("/")
public class DemoController {

    private class DemoData {
        private int num;
        private String name;

        private DemoData(int num, String name) {
            this.num = num;
            this.name = name;
        }
    }

    @RequestMapping("/hello")
    public String hello() {
        List l = new ArrayList<DemoData>();
        for (int i = 0; i < 100000000; i++) {
            l.add(new DemoData(i, "test" + i));
        }
        return "hello";
    }
}

서버를 띄운 후, /hello라는 경로로 요청을 보내면,

$ curl -X GET localhost:8080/hello

GC의 발생빈도와 Heap size가 급격하게 늘어남을 볼 수 있다. (모니터링 도구로는 VisualVM 을 사용하였다.)

Heap Dump하기

먼저 Heap Dump를 뜨기 전에 운영되고 있는 어플리케이션 프로세스의 PID를 알아낸다.

$ jps
84544 SpringDemoApplication  

그리고 jmap 툴을 이용하여 Heap Dump를 뜨면 된다.
명령어 형식은 아래와 같다.

$ jmap -dump:format=b,file=[FILE_NAME] [PID]

$ jmap -dump:format=b,file=heapdump.hprof 84544

명령어가 성공적으로 실행되면 heapdump.hprof 이름의 Heap Dump 파일이 생성될 것이다.

Heap Dump 분석하기

생성된 Heap Dump 파일을 분석하는 여러 방법이 있겠지만, 그중에서 Eclipse Memory Analyzer(MAT) 툴을 이용하여 분석해보자.

MAT를 다운받은 후 실행하면, 아래와 같은 화면이 나타난다.

"Open a Heap Dump" 를 클릭한 후, 앞서 생성한 Heap Dump 파일을 Open한다.
이 과정에서 몇 분정도 소요되고, 파일의 크기가 크면 클수록 더 많은 시간이 소요된다. 그리고 Heap Dump 파일이 있는 경로에 다량의 *.index 이름의 인덱스 파일이 생성이 되는데 이때 당황하지 않고, Heap Dump 분석이 끝난 후 모두 삭제해주면 된다.

참고로, 기본 heap 크기가 1G로 되어 있어, Heap Dump이 이보다 큰 경우 Parsing할 때 "Out of Memory" 에러를 만나게 된다.

An internal error occurred during: "Parsing heap dump from '/Users/jupiny/heapdump.hprof'".  

이 경우(Mac 기준), /Applications/mat.app/Contents/Eclipse/MemoryAnalyzer.ini 설정 파일에서, 아래와 같이 원하는 크기로 변경해주면 된다.

...
-Xms6g
-Xmx6g
...

성공한 후에 "Overview" 탭에서 "Dominator Tree" 를 클릭한다.

그럼 아래와 같이 Heap Dump를 뜰 당시에 만들어진 Java 객체들을 한 눈에 확인할 수 있다.

비정상적으로 높은 퍼센트를 차지하는 객체가 어떤 것인지 확인할 수 있을 것이고, 이 객체가 결국 Heap의 높은 사용량을 만든 주범이라는 것을 쉽게 알아낼 수 있다.

이제 코드에서 이 객체가 사용된 부분을 하나씩 찾아보며 구현상의 잘못된 부분이 없었는지 디버깅할 일만 남았다. :D