오늘은 운영 환경에서 자주 쓰이는 메모리 진단 흐름을 직접 따라 해보면서, 일부러 TroubleMaker
Controller 클래스로 장애를 만들고 -> Spring Boot의 heapdump 엔드포인트를 열고 -> Docker 이미지 재빌드 -> heapdump(hprof) 파일 다운로드 -> IntelliJ로 분석하는 과정을 전부 경험했다.
중간중간 생소했던 부분도 있었지만, 직접 해보니 이런 식으로 서비스 메모리를 들여다볼수있음을 깨닫게 되었다.
JVM이 실행 중일 때, 힙 메모리의 전체 스냅샷을 그대로 떠서 파일로 저장한 것.
다시 말해, 지금 이 순간, 내 자바 프로그램이 메모리를 어떻게 쓰고 있는지 그대로 찍은 X-ray 사진과 같다.
- 쉬운 비유!
JVM = 큰 방
Heap = 방 안의 책상들
객체 = 책상 위에 놓인 물건들
Garbage Colletor = 정리하는 로봇
Heapdump = 방 안을 사진으로 그대로 찍어놓은 것
@RestController
public class TroubleMakerController {
//메모리 누수 용도 (static -> GC가 없애지 못함)
private static final List<byte[]> LEAK_LIST = new ArrayList<>();
@GetMapping("/leak")
public String leak() {
LEAK_LIST.add(new byte[10*1024*1024]); //한번 호출 될때 10MB씩 공간 차지
return "Memory leakㅠㅠ current size: " + LEAK_LIST.size();
}
@GetMapping("/slow")
public String slow() throws InterruptedException {
Thread.sleep(5000);
return "I am slow";
}
}
메모리 누수 용도의 leak()와 일부러 느린 API를 만들기 위한 slow()를 작성하였다.
application.yml에 heapdump가 노출되도록 설정해야 한다.
management:
endpoints:
web:
exposure:
include:
- health
- info
- metrics
- loggers
- threaddump
- prometheus
- heapdump
이렇게 저장하면 /actuator/heapdump로 접속했을 때 heapdump 파일을 받을 수 있게 된다.
프로젝트 설정이 바뀌었기 때문에 이미지도 다시 빌드했다.
docker build -t my-spring-app:1.0 . #재빌드
docker stop api1 #현재 api1 컨테이너 중지
docker rm api1 #삭제
docker run -d --name api1 -p 8081:8080 my-spring-app:1.0 #실행
이 과정은 외울 수 있을 정도로 많이 숙련되면 좋을 것 같다.
컨테이너가 정상적으로 가동되면 브라우저에서
http://localhost:8081/actuator/heapdump
로 접근한다.

이렇게 파일을 다운받을 수 있게 되는데, 파일 확장자가 기본적으로 없으므로 저장 후 .hprof 확장자로 직접 변경해주어야 한다.

꽤나 놀랐던 점 :
생각보다 엄청난 양의 객체들이 byte[]로 이루어져 있다!

웹 어플리케이션 내부에서 어떤것이 메모리를 먹는지, 어떤 객체들이 얼마나 잡혀 있는지 실제로 눈으로 확인할 수 있었다.
메모리 누수 분석, 성능 분석할 때 왜 heapdump가 중요한지 체감했다.
역시 막상 해보니 운영 관점에서 꼭 알아야 할 감각이라는걸 몸소 깨닫게 되었다.
코드를 아는 것 -> 프로그램이 실제로 어떻게 돌아가는지까지 보는 것
이 두 단계의 과정이 중요하구나!