LabHub
学习 学习路径 课程

堆还有空间,服务却停了

在堆转储里找到元凶

在 LabHub 中继续学习

한국어 원문으로 표시합니다.

목표

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.txtjcmd <pid> GC.heap_dump /root/jvm/heap/live.hprof 를 남긴다(그 뒤 프로세스는 kill 해도 된다).
  4. histo.txt 의 1위 줄에서 클래스 이름과 인스턴스 수를 읽어 /root/jvm/heap/top.txttop_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 -versionMaxHeapSize 값과 -XX:MaxRAMPercentage=50 을 더한 값을 읽어 /root/jvm/heap/ram.txtdefault_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 이어야 한다.

참고

OutOfMemoryError 를 만든다

/root/jvm/heap 에 Leak.java 를 컴파일하고 -Xmx64m 으로 돌려 출력을 /root/jvm/heap/oom.log 에 남긴다. java.lang.OutOfMemoryError: Java heap space 가 있어야 한다.

javac -d /root/jvm/heap /opt/lab/fixtures/jvm/Leak.java 뒤 java -Xmx64m -cp /root/jvm/heap Leak > oom.log 2>&1. 몇 초 안에 힙이 찹니다.

사건의 순간에 덤프를

-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 가 있어야 한다.

두 옵션은 클래스 이름 앞에 옵니다. 덤프는 예외가 던져지는 순간 JVM 이 만듭니다 — 로그에 Dumping heap to ... 가 찍힙니다.

살아 있는 프로세스의 히스토그램과 덤프

-Xmx64m -Dleak.sleep.ms=50 으로 Leak 을 백그라운드로 띄우고 2초 뒤 jcmd GC.class_histogram > /root/jvm/heap/histo.txt 와 jcmd GC.heap_dump /root/jvm/heap/live.hprof 를 남긴다.

java ... & PID=$! 뒤 sleep 2 하고 두 jcmd 를 부르세요. heap_dump 는 먼저 Full GC 를 요청하므로 도달 가능한 객체만 남습니다.

히스토그램 첫 줄을 읽는다

histo.txt 의 1위 줄에서 클래스 이름과 인스턴스 수를 /root/jvm/heap/top.txt 에 top_class=<이름> 과 instances= 으로 적는다.

1위 줄은 ' 1:' 로 시작하고 열은 순위·인스턴스·바이트·클래스 이름 순입니다. 이름은 (java.base@…) 앞까지입니다. awk '$1=="1:"{print $2, $4}' 가 두 값을 줍니다.

같은 할당, 참조만 없으면

-Xmx64m -Dleak.retain=false -Dleak.iterations=3000 으로 돌려 출력을 /root/jvm/heap/noleak.log 에 남긴다. done iterations=3000 이 있고 OutOfMemoryError 가 없어야 한다.

3000 × 256KB = 750MB 를 할당하지만 64MB 힙에서 끝까지 돕니다. 놓는 순간 GC 가 회수하기 때문입니다.

좀비를 만들지 않는다

-XX:+ExitOnOutOfMemoryError 를 붙여 돌리고 출력을 /root/jvm/heap/exit-on-oom.log 에, 종료 코드를 /root/jvm/heap/exit.txt 에 남긴다. Terminating due to java.lang.OutOfMemoryError 가 있고 exit.txt 는 3 이어야 한다.

java ... > exit-on-oom.log 2>&1; echo $? > exit.txt. 첫 OutOfMemoryError 에서 JVM 이 즉시 끝나며 종료 코드는 3 입니다(실측).

컨테이너 힙 기본값을 잰다

java -XX:+PrintFlagsFinal -version 의 MaxHeapSize 와 -XX:MaxRAMPercentage=50 을 더했을 때의 MaxHeapSize 를 /root/jvm/heap/ram.txt 에 default_max_heap=<바이트> 와 half_max_heap=<바이트> 로 적는다.

참고 절의 awk 로 값을 뽑고, 두 번째는 -XX:MaxRAMPercentage=50 을 -XX:+PrintFlagsFinal 앞에 붙입니다. 기본 25% 라 두 번째 값이 두 배 안팎입니다.

옵션을 스크립트에 못 박는다

/root/jvm/heap/start.sh 를 만든다: -Xmx64m, HeapDumpOnOutOfMemoryError, HeapDumpPath=/root/jvm/heap/dumps, ExitOnOutOfMemoryError, -Xlog:gc:file=/root/jvm/heap/gc.log 로 Leak 을 돌리고 종료 코드를 /root/jvm/heap/runbook-exit.txt 에 적는다. 실행하면 dumps/ 에 .hprof, gc.log, runbook-exit.txt(3) 가 생긴다.

HeapDumpPath 에 디렉터리를 주면 java_pid.hprof 로 생깁니다 — mkdir -p 를 스크립트 첫 줄에. 마지막 줄은 echo $? > runbook-exit.txt 입니다.