JVM process가 응답하지 않거나 CPU를 많이 쓸 때 thread dump는 “그 순간 각 thread가 어디에서 무엇을 기다리는지”를 보여 준다. jstack과 jcmd <pid> Thread.print 모두 thread stack을 출력할 수 있지만, jcmd는 그보다 넓은 diagnostic command 인터페이스다.
결론부터 정리하면 다음과 같다.
- thread dump만 익숙한 형식으로 빠르게 확인한다면
jstack -l <pid>도 유효하다. - 사용 가능한 진단 명령을 확인하고 thread·heap·JFR·VM 정보를 한 도구에서 다룬다면
jcmd가 편하다. - 어느 명령도 “항상 가볍다”고 가정하지 말고, 대상 JVM과 thread 수에 따른 영향과 접근 조건을 먼저 확인한다.
jstack과 jcmd의 역할
| 항목 | jstack | jcmd |
|---|---|---|
| 주 역할 | Java thread stack trace 출력 | 실행 중인 JVM에 diagnostic command 전달 |
| thread dump | jstack -l <pid> |
jcmd <pid> Thread.print -l |
| 추가 진단 | thread 중심 | JVM이 제공하는 GC, JFR, VM, compiler 등 명령 |
| 명령 발견 | 정해진 option 확인 | jcmd <pid> help로 대상 JVM의 목록 확인 |
| 영향 | process·thread 수에 따라 달라짐 | command별 Low·Medium·High impact 표기를 확인 |
jcmd가 jstack, jmap, jinfo, jstat을 문자 그대로 하나로 합친 도구라는 표현은 지나치다. JVM이 노출한 diagnostic command를 실행하는 공통 입구이며, 지원 command와 option은 JDK·JVM에 따라 달라질 수 있다.
수집 전에 확인할 조건
Oracle JDK 문서 기준으로 jcmd는 대상 JVM과 같은 machine에서 실행해야 하고, 실행한 process와 같은 effective user·group identity가 필요하다. attach mechanism이 비활성화되어 있거나 container PID namespace가 다르면 host에서 보이는 PID와 JVM이 보는 PID가 달라 실패할 수 있다.
또 다른 JDK version의 tool로 JVM을 진단하는 조합은 지원되지 않는다. 가능하면 대상 process의 JAVA_HOME에 포함된 tool을 사용한다.
JVM_PID="<pid>"
## 대상 JVM이 제공하는 명령 확인
jcmd "${JVM_PID}" help
## JVM version과 기동 command 확인
jcmd "${JVM_PID}" VM.version
jcmd "${JVM_PID}" VM.command_line
명령 결과에는 class·method·thread 이름, file path, system property 등 운영 정보가 포함될 수 있다. 수집 파일을 ticket이나 public chat에 그대로 올리지 말고 접근 권한과 보관 기간을 정한다.
Thread dump 수집
JVM_PID="<pid>"
STAMP="$(date +%Y%m%d-%H%M%S)"
jcmd "${JVM_PID}" Thread.print -l \
> "thread-${JVM_PID}-${STAMP}.txt"
-l은 java.util.concurrent lock 정보를 함께 출력한다. JDK 25의 jcmd에는 plain text 또는 JSON file로 저장하는 Thread.dump_to_file command도 있으므로, 실제 지원 여부와 문법은 다음처럼 대상 JVM에서 확인한다.
jcmd "${JVM_PID}" help Thread.dump_to_file
jstack으로 같은 목적의 snapshot을 얻으려면 다음처럼 실행한다.
jstack -l "${JVM_PID}" \
> "jstack-${JVM_PID}-$(date +%Y%m%d-%H%M%S).txt"
deadlock 여부만 보는 데 그치지 않고, incident가 지속되는 동안 적절한 간격으로 여러 snapshot을 수집해 같은 stack이 반복되는지 본다. 너무 짧은 간격으로 계속 attach하면 진단 자체가 부하가 될 수 있으므로 서비스 영향과 incident 시간을 기준으로 횟수와 간격을 정한다.
CPU 급증을 thread와 연결하는 흐름
Linux에서 process CPU가 높을 때는 OS thread ID와 Java dump의 nid를 연결한다.
JVM_PID="<pid>"
## thread별 CPU 확인
top -H -p "${JVM_PID}"
## decimal TID를 hexadecimal로 변환
printf '0x%x\n' "<tid>"
변환한 값과 thread dump의 nid=0x...를 맞추고 stack을 본다. 한 snapshot에서 RUNNABLE이라고 해서 CPU를 계속 소비한다고 단정할 수는 없다. 여러 시점의 CPU와 stack이 같은지 확인해야 한다.
기다림과 deadlock을 읽는 순서
BLOCKED: monitor 진입을 기다리는지, lock owner가 누구인지 본다.WAITING·TIMED_WAITING: queue, condition, sleep처럼 의도한 대기인지 본다.RUNNABLE: native I/O 대기일 수도 있어 state 이름만으로 CPU 사용을 판단하지 않는다.- 같은 lock을 두고 많은 thread가 모이면 owner stack과 critical section 시간을 본다.
- JVM이 찾은 Java-level deadlock과 application-level livelock·external dependency wait를 구분한다.
thread dump는 특정 순간의 사진이다. 긴 구간의 CPU, allocation, lock, I/O 사건을 함께 보려면 JFR을 검토한다. jcmd의 JFR.start, JFR.check, JFR.dump, JFR.stop은 각각 impact와 option이 있으므로 운영 표준에 맞춰 사용한다.
heap command는 별도 위험을 본다
jcmd <pid> GC.heap_info는 일반적인 heap 정보를 보여 주지만, GC.class_histogram과 GC.heap_dump는 heap 크기와 option에 따라 영향이 커질 수 있다. thread 문제를 보다가 습관적으로 heap dump까지 뜨지 않는다.
jcmd "${JVM_PID}" help GC.class_histogram
jcmd "${JVM_PID}" help GC.heap_dump
운영에서는 먼저 저비용 정보로 가설을 좁히고, 고비용 command는 disk 여유, pause 가능성, 개인정보 포함 여부와 승인 절차를 확인한 뒤 실행한다.
선택 기준
jstack과 jcmd Thread.print 중 어느 하나가 결과적으로 더 가볍다고 일반화하기는 어렵다. 두 도구 모두 JVM에 attach해 thread 정보를 요청하며 대상 상태에 영향을 줄 수 있다.
새 runbook을 만든다면 jcmd <pid> help로 기능을 발견하고 command별 impact를 확인하는 흐름을 기본으로 두기 좋다. 기존 자동화가 jstack output parser에 의존한다면 곧바로 바꾸기보다 대상 JDK version에서 출력 차이와 실패 동작을 검증한다.
공식 문서
'배움과 성장 > 시스템·성능' 카테고리의 다른 글
| MLFQ 스케줄링 원리: 우선순위·Aging·기아 방지 (0) | 2025.09.23 |
|---|---|
| CPU 스케줄링 기초: Turnaround·Response Time과 SJF·RR 비교 (0) | 2025.09.22 |
| RED와 USE 방법론: 서비스 증상에서 시스템 병목까지 연결하는 법 (0) | 2025.03.08 |
| 시스템 성능 분석 입문 독서노트: 도구보다 질문과 정량화 (0) | 2025.03.05 |
| Linux System Call 이름 읽는 법: execve·openat·fsync·epoll 의미와 경계 (0) | 2025.03.05 |
댓글