힙은 남았는데 서비스가 멈췄다 · 힙 덤프에서 범인을 찾는다 · 실습
힙 덤프에서 범인을 찾는다
목표
OutOfMemoryError 를 직접 만들고, 힙 덤프와 클래스 히스토그램으로 범인을 찾고, 재발을 막는 기동 옵션을 스크립트로 남긴다.
왜 중요한가
덤프는 사건의 순간에만 만들어진다. 옵션을 켜 두지 않으면 사건 뒤에는 아무것도 없다. 그리고 OutOfMemoryError 는 프로세스를 죽이지 않는다 — 반쯤 죽은 채 헬스체크를 통과하는 상태를 ExitOnOutOfMemoryError 가 끝낸다. 재료 Leak.java 는 -Dleak.kb=256 크기 배열을 정적 맵에 계속 넣고(-Dleak.retain=false 면 넣지 않음), -Dleak.sleep.ms 로 속도를, -Dleak.iterations 로 횟수를 정한다. 시작할 때 max_heap_mb= 를 찍는다.
단계
1. /root/jvm/heap 에 Leak.java 를 컴파일하고 java -Xmx64m -cp /root/jvm/heap Leak > /root/jvm/heap/oom.log 2>&1 로 돌린다. 로그에 java.lang.OutOfMemoryError: Java heap space 가 있어야 한다.
2. -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/root/jvm/heap/oom.hprof 를 붙여 다시 돌리고 출력을 /root/jvm/heap/oom-dump.log 에 남긴다. oom.hprof 가 생기고(1MB 이상, JAVA PROFILE 1.0.2 로 시작) 로그에 Heap dump file created 가 있어야 한다.
3. java -Xmx64m -Dleak.sleep.ms=50 -cp /root/jvm/heap Leak 을 백그라운드로 띄우고 2초 뒤 jcmd <pid> GC.class_histogram > /root/jvm/heap/histo.txt 와 jcmd <pid> GC.heap_dump /root/jvm/heap/live.hprof 를 남긴다(그 뒤 프로세스는 kill 해도 된다).
4. histo.txt 의 1위 줄에서 클래스 이름과 인스턴스 수를 읽어 /root/jvm/heap/top.txt 에 top_class=<이름> 과 instances=<n> 두 줄로 적는다.
5. java -Xmx64m -Dleak.retain=false -Dleak.iterations=3000 -cp /root/jvm/heap Leak > /root/jvm/heap/noleak.log 2>&1 로 같은 할당을 참조 없이 돌린다. 로그에 done iterations=3000 이 있고 OutOfMemoryError 가 없어야 한다.
6. -XX:+ExitOnOutOfMemoryError 를 붙여 돌리고 출력을 /root/jvm/heap/exit-on-oom.log 에, 종료 코드(echo $?)를 /root/jvm/heap/exit.txt 에 남긴다. 로그에 Terminating due to java.lang.OutOfMemoryError 가 있고 exit.txt 는 3 이어야 한다.
7. java -XX:+PrintFlagsFinal -version 의 MaxHeapSize 값과 -XX:MaxRAMPercentage=50 을 더한 값을 읽어 /root/jvm/heap/ram.txt 에 default_max_heap=<바이트> 와 half_max_heap=<바이트> 두 줄로 적는다.
8. /root/jvm/heap/start.sh 를 만든다: -Xmx64m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/root/jvm/heap/dumps -XX:+ExitOnOutOfMemoryError -Xlog:gc:file=/root/jvm/heap/gc.log 로 Leak 을 돌리고 종료 코드를 /root/jvm/heap/runbook-exit.txt 에 적는 스크립트다. 실행하면 /root/jvm/heap/dumps/ 에 .hprof 가 생기고 gc.log 가 생기며 runbook-exit.txt 가 3 이어야 한다.
참고
- HPROF 머리 확인:
dd if=/root/jvm/heap/oom.hprof bs=1 count=18 2>/dev/null→JAVA PROFILE 1.0.2 - MaxHeapSize 읽기:
java -XX:+PrintFlagsFinal -version 2>/dev/null | awk '/ MaxHeapSize /{print $4}' - HeapDumpPath 에 디렉터리를 주면 그 안에 java_pid<pid>.hprof 로 생깁니다. 디렉터리는 미리 만드세요.
- 흔한 실수: 덤프 경로를 사라지는 곳에 두는 것, ExitOnOutOfMemoryError 없이 좀비를 두는 것, 히스토그램 1위([B)만 보고 끝내는 것.
단계 8개
- OutOfMemoryError 를 만든다
- 사건의 순간에 덤프를
- 살아 있는 프로세스의 히스토그램과 덤프
- 히스토그램 첫 줄을 읽는다
- 같은 할당, 참조만 없으면
- 좀비를 만들지 않는다
- 컨테이너 힙 기본값을 잰다
- 옵션을 스크립트에 못 박는다