| 순서 | 질문 | 명령 |
|---|---|---|
| 1 | 프로세스가 있나 | systemctl status, jps -l |
| 2 | 죽었다면 왜 | journalctl -u, dmesg, 종료 코드 |
| 3 | 살아 있다면 CPU·메모리 | top -p PID, free -m |
| 4 | 스레드가 뭘 하나 | jstack 3장 |
| 5 | 힙이 찼나 | jstat -gcutil, jmap -histo |
| 6 | FD·연결 수 | ls /proc/PID/fd, ss -s |
1·2 는 03 레슨, 3 은 자원 확인, 4~6 이 이 레슨의 본문입니다. 덤프를 남기기 전에는 재시작하지 않는 것이 원칙입니다.
JDK 도구는 대상 JVM 의 PID 가 필요합니다. jps -l 이 자바 프로세스만 클래스·jar 이름과 함께 보여 줍니다.
jps -l # 12345 /opt/myapp/current/app.jar
jcmd 12345 help # 그 JVM 이 받는 진단 명령 목록
jcmd 12345 VM.flags # 실제 적용된 JVM 옵션도구는 JVM 과 같은 사용자로 실행해야 붙습니다. myapp 계정으로 뜬 앱은 sudo -u myapp jstack PID 로 봅니다.
jstack PID 가 모든 스레드의 상태와 스택을 출력합니다. 한 장으로는 순간이고, 5초 간격으로 세 장을 찍어 같은 스택이 계속 보이는 스레드를 찾습니다.
for i in 1 2 3; do jstack 12345 > td$i.txt; sleep 5; done
grep -oE 'java.lang.Thread.State: [A-Z_]+' td1.txt | sort | uniq -c | sort -rn| 상태 | 뜻 | 의심 |
|---|---|---|
| RUNNABLE | 실행 중·실행 대기 | CPU 소모, 소켓 대기 |
| BLOCKED | 모니터 잠금 대기 | synchronized 경합 |
| WAITING | 무기한 대기 | 풀에서 놀고 있음 |
| TIMED_WAITING | 시간 제한 대기 | sleep, 타임아웃 대기 |
BLOCKED 가 수십 개면 잠금 경합, RUNNABLE 이 많고 스택이 같은 코드면 그 코드가 CPU 를 먹는 것입니다. 덤프 뒤쪽의 Found one Java-level deadlock 은 데드락 자동 감지입니다.
덤프의 - locked <주소> 와 - waiting to lock <주소> 로 누가 잡고 누가 기다리는지 이어집니다.
"lock-holder" ... TIMED_WAITING
- locked <0x00000000ffc57c30> (a java.lang.Object)
"lock-waiter" ... BLOCKED (on object monitor)
- waiting to lock <0x00000000ffc57c30> (a java.lang.Object)같은 주소를 grep 하면 잡은 스레드 하나와 기다리는 스레드 여럿이 나옵니다. 잡은 스레드의 스택 맨 위가 원인 코드입니다. DB 커넥션 풀 고갈은 HikariPool 스택에서 WAITING 인 스레드가 많은 형태로 보입니다.
top -H -p PID 가 스레드별 CPU 를 보여 줍니다. 그 TID 를 16진수로 바꾸면 덤프의 nid 와 일치합니다.
top -H -p 12345 -b -n 1 | head -n 12 # TID 열, %CPU 순
printf '%x\n' 12389 # 12389 -> 3065
grep -A10 'nid=0x3065' td1.txt # 그 스레드의 스택JDK 21 리눅스는 nid=0x3065 형식이고 Windows 는 10진수로 찍힙니다. 이 세 줄이 "CPU 100% 인데 어느 코드인가" 를 1분 안에 답합니다.
jstat -gcutil PID 1초 횟수 가 영역별 사용률과 GC 횟수·시간을 주기적으로 찍습니다.
jstat -gcutil 12345 1000 5| 열 | 뜻 |
|---|---|
| E | Eden 사용률 |
| O | Old 사용률 |
| YGC / YGCT | Young GC 횟수·시간 |
| FGC / FGCT | Full GC 횟수·시간 |
O 가 90% 이상에서 안 내려오고 FGC 가 초마다 늘면 힙이 찬 것입니다. 앱은 살아 있지만 GC 만 돌아 응답이 없는 상태로, 곧 OutOfMemoryError 가 납니다.
jmap -histo:live PID 가 클래스별 인스턴스 수와 바이트를 큰 순서로 보여 줍니다. :live 는 Full GC 를 먼저 돌려 살아 있는 객체만 셉니다.
jmap -histo:live 12345 | head -n 15 num #instances #bytes class name
1: 2829 52588720 [B
2: 138 160824 [C
3: 780 97344 java.lang.Class[B 는 byte[], [C 는 char[] 입니다. 상위에 앱 클래스나 HashMap$Node 가 수백만 개면 캐시나 컬렉션 누수입니다. 원인 클래스가 보이면 그 객체를 누가 잡고 있는지는 힙 덤프로 봅니다.
힙 덤프는 힙 전체를 파일로 씁니다. 크기가 힙과 같고 쓰는 동안 앱이 멈추므로 운영에서는 신중히 찍습니다.
jcmd 12345 GC.heap_dump /var/log/myapp/heap.hprof
jmap -dump:live,format=b,file=heap.hprof 12345 # 같은 결과분석은 서버가 아니라 PC 의 Eclipse MAT 이나 VisualVM 에서 합니다. scp 로 내려받아 "Leak Suspects" 보고서를 보면 가장 큰 객체 트리가 나옵니다. 04 레슨의 HeapDumpOnOutOfMemoryError 옵션이 OOM 순간에 이 파일을 자동으로 남깁니다.
같은 "메모리 부족" 이지만 둘은 전혀 다릅니다. 증거가 다르고 대책도 다릅니다.
| 구분 | JVM OOM | 커널 OOM killer |
|---|---|---|
| 증거 | 로그 OutOfMemoryError |
dmesg Killed process |
| 종료 코드 | 앱이 정한 값 | 137 |
| 덤프 | .hprof 자동 |
없음 |
| 원인 | 힙 부족·누수 | 서버 RAM 부족 |
| 대책 | 누수 수정·-Xmx |
-Xmx 축소·앱 분리 |
-Xmx 를 RAM 에 가깝게 주면 힙 밖 메모리까지 합쳐 RAM 을 넘어 커널이 JVM 을 죽입니다. 04 레슨에서 RAM 의 50~70% 를 권한 이유입니다.
dmesg -T | grep -iE 'killed process|out of memory'
journalctl -k --since '2 hours ago' | grep -i oom
systemctl status myapp | grep -E 'code=|status='Killed process 12345 (java) total-vm:... anon-rss:... 줄이 증거입니다. anon-rss 가 그때 JVM 이 쓰던 실제 메모리입니다. journalctl -u myapp 에 Main process exited, code=killed, status=9/KILL 이 같이 보입니다.
소켓, 파일, 파이프가 모두 FD 입니다. 한도를 넘으면 Too many open files 로 새 연결과 파일 열기가 실패합니다.
ls /proc/12345/fd | wc -l # 현재 열린 수
cat /proc/12345/limits | grep 'open files' # 상한
lsof -p 12345 | awk '{print $NF}' | sort | uniq -c | sort -rn | head -n 5
ss -tnp | grep -c 'pid=12345' # 소켓 수상한은 systemd 유닛의 LimitNOFILE 이 정합니다. 03 레슨에서 65536 을 권한 이유입니다. 현재 수가 계속 늘면 닫지 않는 스트림이나 커넥션이 있는 것이고, lsof 의 파일 이름이 어느 코드인지 알려 줍니다.
덤프를 남겼으면 재시작하고, 그다음 기록합니다. 기록이 없으면 다음 장애에서 처음부터 다시 합니다.
| 항목 | 내용 |
|---|---|
| 시각 | 발생·인지·복구 |
| 증상 | 사용자가 본 것 |
| 증거 | 덤프 파일 경로, 로그 발췌 |
| 원인 | 확인된 것과 추정 |
| 조치 | 임시·영구 |
덤프 파일은 날짜를 붙여 /var/log/myapp/incident/ 에 모으고, 06 레슨의 cron 으로 30일 뒤 지웁니다.