0%

자바 힙 덤프 분석 (실습 예제 있음!!)

개요

서버를 운영하다 보면 여러 이유로 힙 덤프를 분석할 일이 생깁니다. 힙 덤프 분석을 통해서 메모리 누수나 메모리 사용량이 높은 부분을 찾아내는데 도움이 됩니다.

처음 힙 덤프를 분석하려고 하면 어떻게 해야할지 막막할 수 있습니다. 그래서 그런 분들에게 도움이 되고자 힙 덤프 예제 문제와 함께 힙 덤프 분석 방법을 설명하겠습니다. 문제는 아래 링크에서 확인 할 수 있습니다. 문제 정답은 글 맨 아래에 있습니다.

https://github.com/marinesnow34/dump-example

문제

고라파덕은 멍때리며 코딩하는 것으로 유명하다. 어느 날, 고라파덕이 운영하는 서버에서 OOM(Out Of Memory) 오류가 발생했다. 다행히 Heap Dump 파일(dump.hprof)을 확보할 수 있었고, 이를 통해 문제를 분석할 수 있는 상황이다.

📌 분석할 내용

  1. 어떤 메서드에서 문제가 발생했는가?
  2. 어떤 상황(변수 값)에서 문제가 발생했는가?

소스 코드와 Heap Dump를 분석하여 고라파덕을 도와주자!!

정적 분석

본격적으로 힙 덤프를 분석하기 전에 간단하게 소스 코드를 확인 해 봅시다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// src/main/java/com/example/dump/domain/UserRepository.java
public interface UserRepository extends JpaRepository<User, Long> {
User findByIdx(Long idx);
User findByName(String name);
}

// src/main/java/com/example/dump/service/UserService.java
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
public String getUserNameById(Long idx) {
return userRepository.findByIdx(idx).getName();
}

public String getUserAddressByName(String name) {
return userRepository.findByName(name).getAddress();
}
}

일단 서비스를 확인해 보니 idx가 주어지면 idx를 가진 유저의 이름을 가져오고, 이름이 주어지면 이름을 가진 유저의 주소를 가져오는 것으로 보입니다.

코드로 봤을 때는 문제가 될만한 부분이 보이지 않습니다. 그럼 이제 힙 덤프를 분석해 봅시다.

힙 덤프 분석

힙 덤프를 분석하기 위해서는 MAT
VisualVM을 사용했습니다. 먼저 MAT로 힙 덤프를 열어봅시다.

MAT 메인 화면

MAT에서 dump.hprof 파일을 열면 위와 같은 화면이 나옵니다. 먼저 Leak Suspects Report를 클릭해 줍니다. 분석되는 동안 Action > Dominator Tree를 합니다.

Domiantor Tree 내용을 보면 Retained Heap이 큰 객체를 찾을 수 있습니다. 참고로 Retained Heap은 특정 객체가 참조하고 있는 모든 객체들 입니다. 즉 GC에 의해 제거될 때 회수될 수 있는 힙 메모리의 양입니다.
ArrayList의 capacity는 360,145이고 안에 들어있는 객체의 갯수는 349,584개 입니다. 이를 통해서 모종의 이유로 ArrayList가 너무 많은 객체를 가지고 있어서 OOM이 발생했을 것으로 추측할 수 있습니다.

아까 분석시킨 supects Report를 확인 합시다.
suspect

http-nio-8080-exec-2 스레드에서 문제가 발생했음을 알 수 있습니다. Detail을 확인하면 더 많은 정보를 얻을 수 있습니다. 가독성이 좋은 VisualVM으로 해당 스레드를 확인해 봅시다.


visualVM

VisualVM에 메인에서 OutOfMemory Thread를 확인할 수 있습니다. 이번 상황에서는 메모리를 많이 들고 있는 쓰레드와 OOM이 발생한 쓰레드가 동일한 것을 알 수 있습니다. view all로 해당 쓰레드를 확인해 봅시다.

visualVM

http-nio-8080-exec-2스레드의 스택트레이스가 보입니다. 길어서 저희가 호출한 부분을 찾아보겠습니다.
visualVM

익숙한 UserService.getUserNameById() 메서드가 보입니다. findByIdx()를 호출하다가 OOM이 발생했습니다. findByIdx()를 확인해 봅시다.
visualVM

local variable을 확인해 보면 long 값이 0인 것을 확인할 수 있습니다. 이는 idx가 0인 유저를 가져오는 것으로 추정됩니다. idx가 0인 유저는 36만명 이상으로 추측 됩니다.

정답 및 해설

  1. 어떤 메서드에서 문제가 발생했는가?
    • UserService.getUserNameById()userRepository.findByIdx(idx)에서 idx가 동일한 유저를 모두 가져와 메모리 부족으로 인한 OOM이 발생했습니다.
  2. 어떤 상황(변수 값)에서 문제가 발생했는가?
    • idx가 0인 유저를 가져올 때 문제가 발생했습니다. idx가 0인 유저는 36만명 이상으로 추측 됩니다.

고라파덕의 서비스는 어떤 이유로 idx가 0인 유저가 36만명 이상이 되었고, 이를 findByIdx(0L)으로 호출하게 됐습니다. NonUniqueResultException가 발생하기 전에 메모리가 부족해 OOM이 발생했습니다.