LabHub
배우기 러닝패스 코스

The heap had room, but the service stopped

Reading the GC log

LabHub 에서 이어서 보기

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

목표

작은 자바 프로그램으로 실제 GC 로그를 만들고 읽는다 — 정지 횟수·정지 시간·힙 크기의 영향·컬렉터 비교·jstat·jcmd.

왜 중요한가

"서비스가 느리다" 를 받았을 때 첫 갈림길은 GC 인가 아닌가다. GC 로그에 긴 정지가 없으면 범인은 다른 곳(잠금·풀 고갈)이다. 로그를 켜는 법과 읽는 법, 그리고 힙 크기와 컬렉터가 정지에 어떻게 영향을 주는지를 실제 숫자로 본다. 재료 Churn.javajava Churn <초> <유지MB> [반복당KB] 로 돌며, 유지MB 만큼은 붙들고 나머지는 계속 버린다.

단계

  1. mkdir -p /root/jvm/gc && cd /root/jvm/gc && javac -d . /opt/lab/fixtures/jvm/Churn.java 로 컴파일한다. /root/jvm/gc/Churn.class 가 생기고 java -cp /root/jvm/gc Churn 1 0done 을 찍는다.
  2. java -Xmx256m -Xlog:gc:file=/root/jvm/gc/gc.log -cp /root/jvm/gc Churn 5 24 로 GC 로그를 만든다. 로그에 Using 으로 시작하는 컬렉터 줄과 Pause Young 줄이 있어야 한다.
  3. -Xlog:gc*:file=/root/jvm/gc/gc-detail.log 로 세부 로그를 만든다(같은 부하 5초·24MB). Pause Young 을 포함하고 ms 로 끝나는 줄(정지 완료 줄)의 수를 세어 /root/jvm/gc/answer.txtyoung_pauses=<n> 한 줄로 적는다.
  4. 같은 부하를 -Xms64m -Xmx64m 으로 돌려 /root/jvm/gc/gc-64.log(-Xlog:gc*)를, -Xmx256m 으로 돌려 /root/jvm/gc/gc-256.log 를 만들고, /root/jvm/gc/compare.txtxmx64_pauses=<n>xmx256_pauses=<n> 두 줄(3단계와 같은 세는 법)을 적는다. 64M 쪽이 더 많아야 한다.
  5. gc-256.log 에서 가장 긴 정지(Pause 줄의 마지막 ms 값)를 찾아 /root/jvm/gc/pause.txtmax_pause_ms=<x> 로 적는다(소수 셋째 자리까지, 로그 값 그대로).
  6. java -Xmx128m -cp /root/jvm/gc Churn 12 24 를 백그라운드로 띄우고 jstat -gc <pid> 1000 5 > /root/jvm/gc/jstat.txt 로 1초 간격 5번을 남긴다. 헤더(S0C·EC·YGC·FGC…)와 데이터 5행이 있어야 한다.
  7. -XX:+UseSerialGC -Xlog:gc:file=/root/jvm/gc/gc-serial.log-XX:+UseG1GC -Xlog:gc:file=/root/jvm/gc/gc-g1.log 로 같은 부하(-Xmx128m, 5초·24MB)를 돌리고, 각각의 최대 정지를 /root/jvm/gc/collector.txtserial_max_pause_ms=<x>g1_max_pause_ms=<x> 두 줄로 적는다.
  8. Churn 을 다시 백그라운드로 띄운 채 jcmd <pid> GC.heap_info > /root/jvm/gc/heap_info.txtjcmd <pid> VM.flags > /root/jvm/gc/vm_flags.txt 를 남긴다. heap_info 에 totalused 가, vm_flags 에 -XX:MaxHeapSize= 가 있어야 한다.

참고

재료를 컴파일한다

/root/jvm/gc 에 Churn.java 를 컴파일해 Churn.class 를 만든다. java -cp /root/jvm/gc Churn 1 0 이 done 을 찍는다.

javac -d <출력디렉터리> <소스> 입니다. 클래스는 패키지가 없으므로 -cp 에 디렉터리만 주면 됩니다.

GC 로그를 켠다

-Xmx256m -Xlog:gc:file=/root/jvm/gc/gc.log 로 Churn 5 24 를 돌린다. 로그에 Using 줄과 Pause Young 줄이 있어야 한다.

-Xlog:gc:file=<경로> 가 GC 이벤트를 파일로 보냅니다. 첫 줄이 어떤 컬렉터를 쓰는지 말해 줍니다.

세부 로그에서 정지를 센다

-Xlog:gc*:file=/root/jvm/gc/gc-detail.log 로 같은 부하를 돌리고, Pause Young 을 포함하며 ms 로 끝나는 줄 수를 /root/jvm/gc/answer.txt 에 young_pauses= 으로 적는다.

gc* 는 정지 하나를 [gc,start] 와 [gc] 두 줄로 남깁니다. 완료 줄만 세려면 ms 로 끝나는 줄을 고르세요: grep -E 'Pause Young.*ms$' | wc -l

힙이 작으면 정지가 잦다

-Xms64m -Xmx64m 으로 gc-64.log 를, -Xmx256m 으로 gc-256.log 를(둘 다 -Xlog:gc*) 만들고 compare.txt 에 xmx64_pauses= 과 xmx256_pauses= 을 적는다. 64M 쪽이 많아야 한다.

같은 할당량을 작은 힙에 넣으면 에덴이 빨리 차서 Young GC 가 잦아집니다. 세는 법은 3단계와 같습니다.

평균이 아니라 최대 정지

gc-256.log 에서 Pause 줄의 마지막 ms 값 중 최댓값을 /root/jvm/gc/pause.txt 에 max_pause_ms= 로 적는다.

참고 절의 awk 한 줄이 최댓값을 뽑습니다. 값은 로그의 소수 셋째 자리 그대로입니다.

살아 있는 프로세스를 jstat 으로

Churn 12 24 (-Xmx128m) 를 백그라운드로 띄우고 jstat -gc 1000 5 > /root/jvm/gc/jstat.txt 를 남긴다. 헤더와 데이터 5행이 있어야 한다.

java ... & PID=$! 로 pid 를 잡고 jstat -gc $PID 1000 5 입니다. 프로그램이 12초 뒤 스스로 끝나므로 그 안에 5번을 찍습니다.

Serial 과 G1 을 같은 부하로

-XX:+UseSerialGC 로 gc-serial.log 를, -XX:+UseG1GC 로 gc-g1.log 를(-Xmx128m, -Xlog:gc, Churn 5 24) 만들고 collector.txt 에 serial_max_pause_ms= 와 g1_max_pause_ms= 를 적는다.

각 로그의 첫 줄이 Using Serial / Using G1 이어야 합니다. 최대 정지는 5단계의 awk 를 파일만 바꿔 씁니다.

jcmd 로 힙 정보와 실제 플래그

Churn 을 백그라운드로 띄운 채 jcmd GC.heap_info > /root/jvm/gc/heap_info.txt 와 jcmd VM.flags > /root/jvm/gc/vm_flags.txt 를 남긴다.

jcmd -l 로 pid 를 확인할 수 있습니다. VM.flags 는 JVM 이 실제로 적용한 값(-XX:MaxHeapSize=…)을 보여 줍니다 — 설정 파일이 아니라 이것이 진실입니다.