TT Lab
블로그

한국어

시작하기
블로그

블로그

Java 디버깅: 멈춰 보기, 멈추지 않고 보기, 기록해 두고 보기

상태: 초안 (2026-10-26), 실측 반영. 실측 환경은 둘입니다. ① 노트북의 컨테이너(이미 받아 둔 eclipse-temurin:21 이미지, JDK 21.0.12, 망 없음)에서 예제 프로그램 다섯 개를 직접 만들어 돌렸습니다. 새로 내려받거나 설치한 것은 없습니다. ② TT Lab 클러스터에서 돌고 있는 Keycloak 파드(Red Hat 빌드 OpenJDK 21.0.11)에 읽기 명령만(jcmd 1 VM.version 류) 실행했습니다. 디버그 포트를 열거나, JFR 기록을 시작하거나, 힙 덤프를 뜨거나, 재시작하는 일은 운영 파드에서 하지 않았습니다. async-profiler 는 내려받아야 해서 미실측입니다. 공식 문서는 같은 날 원문을 열어 확인했습니다. 작성 규칙: 실측 데이터가 기본이고, 못 잰 것은 「미실측」, 확인하지 못한 것은 「확인 못 함」, 짐작은 「추정」으로 적습니다.

목차

1. 세 가지 보는 법

자바 서비스가 이상할 때 할 수 있는 일은 크게 셋입니다.

이 글은 먼저 JVM 이 바깥 도구와 말하는 두 길을 그림으로 본 뒤(2절), 디버거(3–4절), 진단 명령(5절), 사례 셋(6–8절), JFR(9절), 운영에서의 순서와 주의(10절) 차례로 갑니다.

2. JVM 안을 들여다보는 두 길

JVM 안을 들여다보는 두 길: 디버거(JPDA)와 진단 명령(Attach) ① 디버거 — 멈추고, 한 줄씩, 값을 바꾼다 IDE · jdb JDI (자바 API) 프런트엔드 JDWP 에이전트 JVM TI 로 JVM 조작 -agentlib:jdwp JDWP (TCP 소켓, 인증 없음) 포트포워드로 내 PC 에서만 ② 진단 명령 — 멈추지 않고(또는 아주 잠깐) 상태를 읽는다 jcmd · jstack jmap · jfr 같은 사용자 · 같은 호스트 Attach Listener 첫 요청에 생기는 스레드 명령을 실행해 돌려줌 유닉스 소켓 (.java_pid<N>) 네트워크 포트 없음 대상 JVM 스레드 · 락 Thread.print 데드락 탐지 힙 · GC GC.heap_info 힙 덤프 · GC 로그 JFR 버퍼 JFR.start → .jfr → jfr view 실측: 디버거 없이 JDWP 만 열어 둔 JVM 은 속도 차이 없음(−0.4%, 잡음 수준). 그러나 포트에 닿는 누구나 「JDWP-Handshake」 14바이트로 붙는다.
그림: 디버거가 JDWP 소켓으로 JDWP 에이전트에 붙는 길과, jcmd 가 유닉스 소켓으로 Attach Listener 에 붙는 길

디버거의 길: JPDA. 자바 플랫폼 디버거 아키텍처(JPDA)는 세 층입니다(공식 문서).

층 자리 하는 일
JVM TI 대상 JVM 안의 네이티브 인터페이스 「이 스레드를 세워라」, 「이 메서드에 중단점」, 「이 지역 변수 값」 같은 디버깅 서비스를 JVM 이 제공
JDWP 선 위의 규약 디버거와 대상 JVM 사이의 바이트 규약. 소켓이나 공유 메모리로
JDI 디버거 쪽 자바 API IDE 와 jdb 가 쓰는 고수준 인터페이스

-agentlib:jdwp=… 는 대상 JVM 에 JDWP 에이전트(네이티브 라이브러리)를 올려, JVM TI 를 부르고 JDWP 로 말하게 합니다. 흔히 쓰는 줄을 풀면 이렇습니다(JDK 21 문서).

-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005
               │                   │        │         └ 어디서 기다리나. 호스트를 비우면 루프백만, * 면 모든 인터페이스
               │                   │        └ 시작하자마자 멈추고 디버거를 기다릴까(기본 y!)
               │                   └ JVM 이 듣는 쪽(서버)이 될까(기본 n: JVM 이 디버거로 걸어 나간다)
               └ TCP 소켓

suspend 의 기본값이 y 라는 점을 잊으면, 디버그 옵션만 넣고 띄운 서비스가 디버거가 붙을 때까지 아예 시작하지 않습니다. 쿠버네티스라면 준비 프로브가 계속 실패합니다. 주소는 JDK 9 에서 바뀌었습니다. 예전에는 포트만 주면 모든 인터페이스에서 들었지만, JDK 9 부터는 루프백에서만 듣고, 모든 인터페이스를 원하면 *: 를 붙여야 합니다. JDK 9 릴리스 노트는 * 로 바인딩하는 것이 「안전하지 않고 권하지 않는다」고 적습니다.

진단 명령의 길: Attach. jcmd·jstack·jmap 은 네트워크를 쓰지 않습니다. 대상 JVM 의 임시 디렉터리에 있는 유닉스 소켓(.java_pid<PID>)으로 붙고, JVM 은 첫 요청을 받으면 Attach Listener 라는 스레드를 띄워 명령을 실행합니다. 같은 사용자, 같은 호스트(컨테이너라면 같은 컨테이너)에서만 됩니다. 그래서 쿠버네티스에서는 kubectl exec 로 그 컨테이너 안에 들어가 실행합니다.

3. 디버거로 멈춰 보기: jdb 한 바퀴

1초마다 한 점씩 점수를 매기는 작은 프로그램(Rally.point(boolean leftWins, int rallyNo))을 -g 로 컴파일해 JDWP 를 켜고 띄운 뒤, 같은 컨테이너에서 jdb 로 붙었습니다(2026-10-26).

$ jdb -attach 5005 -sourcepath /w
> stop in Rally.point
Breakpoint hit: "thread=main", Rally.point(), line=6 bci=0
6            if (leftWins) left++; else right++;
main[1] where
  [1] Rally.point (Rally.java:6)
  [2] Rally.main (Rally.java:13)
main[1] locals
Method arguments:
leftWins = false
rallyNo = 6
main[1] print Rally.left           ← 정적 필드는 클래스 이름을 붙여야 한다(그냥 left 는 「Name unknown」)
 Rally.left = 4
main[1] set leftWins = !leftWins
 leftWins = !leftWins = true
main[1] clear Rally.point
main[1] cont

프로그램 쪽 출력을 시각과 함께 보면 디버거가 무엇을 했는지 보입니다.

시각(초) 출력 설명
:34.03 랠리 5 → 4:1 평소처럼 1초 간격
:39.54 랠리 6 → 5:1 중단점에서 4.5초 멈춰 있었고, 원래 오른쪽 득점(4:2)이어야 할 랠리를 왼쪽 득점으로 바꿨다
:42.05 Listening for transport dt_socket at address: 5005 jdb 를 끝내자 다시 듣기 시작

디버거는 실행을 세울 뿐 아니라 값을 바꿀 수 있습니다. 그리고 디버거가 떠나도 포트는 다음 손님을 기다립니다. 이 두 사실이 다음 절의 보안 문제로 이어집니다.

IDE 원격 디버깅도 원리는 같습니다. IntelliJ 의 「Remote JVM Debug」, VS Code 의 "request": "attach" 설정이 위 jdb 자리에서 JDI 로 같은 JDWP 포트에 붙습니다.

디버그 포트에는 인증이 없다

JDWP 연결은 양쪽이 JDWP-Handshake 14바이트를 주고받는 것으로 시작합니다. 그게 전부입니다. 직접 확인해 봤습니다. 포트에 붙어 14바이트를 보내고, 「VM 판을 알려 달라」는 명령 패킷(11바이트) 하나를 보냈습니다.

JDWP-Handshake
…Java Debug Wire Protocol (Reference Implementation) version 21.0
JVM Debug Interface version 21.0
JVM version 21.0.12 (OpenJDK 64-Bit Server VM, mixed mode, sharing)…

비밀번호도, 인증서도 묻지 않았습니다. 디버거가 할 수 있는 일(임의의 메서드 호출, 값 바꾸기)을 포트에 닿는 누구나 할 수 있다는 뜻입니다. JDWP 에 인증이 없다고 못박은 공식 문서 문장은 찾지 못했지만(확인 못 함), 위 실측과 JDK 9 릴리스 노트의 경고, OpenJDK 메일링 리스트의 「원격 코드 실행을 허용한다」는 설명이 같은 방향입니다. 실측 컨테이너에서 address=*:5005 의 소켓은 /proc/net/tcp6 에 모든 주소(::)의 5005 로 떠 있었습니다.

비용은 어떨까요? 디버거가 붙어 있지 않으면 JDWP 에이전트만 올린 비용은 재지 못할 만큼 작았습니다(9절 표: 기준 대비 −0.4%, 잡음 범위). 그래서 「꺼 두기 귀찮아서」 운영 이미지에 남기 쉽고, 그래서 위험합니다.

4. 쿠버네티스 파드를 원격 디버깅하려면

운영 파드에서는 하지 않는 것이 원칙입니다. 중단점이 서비스 스레드를 세우면 그동안 요청이 쌓이고, 활성 프로브(liveness)가 실패해 kubelet 이 컨테이너를 죽입니다. 개발·스테이징에서 할 때의 안전한 모양은 이렇습니다.

  1. 디버그용 사본을 따로 띄운다. 운영 Deployment 를 고치지 말고, 같은 이미지로 디버그 전용 파드를 만듭니다(kubectl debug <파드> --copy-to=<사본> 이나 별도 매니페스트). 환경 변수 JAVA_TOOL_OPTIONS 에 JDWP 옵션을 넣으면 이미지의 시작 스크립트를 고치지 않아도 JVM 이 읽습니다.
  2. 루프백에만 묶는다. address=127.0.0.1:5005 처럼 파드 안 루프백에서만 듣게 하고, 파드 바깥으로 Service 를 만들지 않습니다.
  3. port-forward 로 내 PC 에만 연다. kubectl port-forward pod/<사본> 5005:5005 는 기본으로 내 PC 의 localhost 에만 열리고(공식 문서: --address 기본값 localhost), TCP 만 지원합니다. API 서버의 인증·권한(RBAC 의 pods/portforward)을 거치므로, JDWP 자체에 없는 인증을 쿠버네티스가 대신 해 주는 셈입니다.
  4. 프로브를 끄거나 늘린다. 사본에서는 liveness 를 빼고, IDE 의 중단점은 「모든 스레드」가 아니라 「그 스레드만」 세우게 설정합니다.
  5. 끝나면 사본을 지운다.

이 절의 절차는 운영 파드에서 디버그 포트를 열지 않는다는 이 글의 원칙에 따라 미실측입니다. port-forward 가 파드 안의 루프백 주소로 닿는다는 점은 쿠버네티스 문서의 예제(파드의 포트로 전달)로만 확인했습니다.

5. 멈추지 않고 보기: jcmd

jcmd <PID> help 가 그 JVM 이 받는 명령을 모두 보여 줍니다. 자주 쓰는 것과 JVM 에 주는 부담을 함께 적습니다(부담은 실측과 문서로 판단).

명령 보여 주는 것 부담
VM.version · VM.uptime · VM.flags 판, 가동 시간, 실제 적용된 JVM 옵션 거의 없음
Thread.print (= jstack) 모든 스레드의 스택과 락, 데드락 탐지 짧은 정지(safepoint). 실측 89ms(명령 전체 왕복)
GC.heap_info 힙 영역별 사용량 거의 없음
GC.class_histogram 클래스별 객체 수·바이트 Full GC 를 일으킨다(8절 실측)
GC.heap_dump <파일> 힙 전체를 HPROF 파일로 JVM 정지, 힙 크기만큼 디스크
JFR.start · JFR.dump · JFR.stop 비행 기록 시작·저장·끝 기본 설정 1% 안팎(9절)
VM.native_memory summary 힙 밖 메모리(스레드 스택·메타스페이스 등) 시작 때 -XX:NativeMemoryTracking 을 켰을 때만
Thread.dump_to_file 가상 스레드까지 포함한 덤프(JSON 가능) JEP 444 예시에 나오며, JDK 21 jcmd 매뉴얼에는 없고 JDK 25 매뉴얼에 있다

실측: 컨테이너 안의 JVM 은 메모리를 얼마로 아나

JVM 은 컨테이너의 메모리 한도(cgroup)를 읽어 기본 힙 크기를 정합니다. 기본은 그 한도의 25%(-XX:MaxRAMPercentage 기본값, 공식 문서)입니다. 노트북 컨테이너에 메모리 한도를 주고 -XX:+PrintFlagsFinal 로 최대 힙을 읽었습니다.

컨테이너 메모리 옵션 없음 -XX:MaxRAMPercentage=70
512MiB 128MiB 360MiB
2GiB 512MiB 1,434MiB

옵션 없이 2GiB 파드에 띄우면 힙은 512MiB 뿐이고 나머지 1.5GiB 는 놀게 됩니다. 반대로 비율을 너무 높이면 힙 밖 메모리(스레드 스택, 메타스페이스, 다이렉트 버퍼)가 모자라 컨테이너가 OOMKilled 됩니다. 이때는 자바의 OutOfMemoryError 도 힙 덤프도 없이 파드만 사라집니다.

실측: 클러스터의 Keycloak 파드를 읽기만

TT Lab 클러스터의 Keycloak 파드에 kubectl exec 로 들어가 jcmd 를 실행했습니다(2026-10-26). 이 이미지는 Red Hat 의 headless JRE 를 쓰는데, 그 안에도 jcmd 가 들어 있었습니다(jstack·jmap 은 없음). jcmd 하나로 둘의 일을 다 하므로 문제는 없습니다. JVM 은 PID 1 이었습니다.

명령 결과
VM.version OpenJDK 64-Bit Server VM 21.0.11+10-LTS
VM.uptime 711,384초(약 8.2일)
VM.flags(일부) MaxRAM=2147483648(파드 한도 2GiB), MaxRAMPercentage=70 → MaxHeapSize=1503657984(1,434MiB), UseG1GC, ExitOnOutOfMemoryError, FlightRecorderOptions=stackdepth=512
GC.heap_info G1 힙 177MiB 확보 중 157MiB 사용, 메타스페이스 141MiB
Thread.print 요약 스레드 95개(자바 상태가 있는 것: RUNNABLE 40·TIMED_WAITING 15·WAITING 14). 데드락 없음. 가장 많은 것은 vert.x-eventloop-thread-N 22개
JFR.check 진행 중인 기록 없음
VM.native_memory summary 「Native memory tracking is not enabled」

몇 가지가 읽힙니다. 이미지가 ExitOnOutOfMemoryError 를 켜 두었으니 힙이 넘치면 JVM 이 바로 끝나고 쿠버네티스가 다시 띄웁니다. 대신 원인을 남길 힙 덤프 옵션은 없습니다. 최대 힙 1,434MiB 에 비해 실제 사용은 157MiB 라 여유가 큽니다. 위 표의 2GiB·70% 계산(1,434MiB)과 운영 파드의 값이 정확히 같은 것도 확인할 수 있습니다.

명령 하나를 실행할 때마다 Attach Listener 스레드가 생긴다는 점은 기억해 둡니다. 「읽기 명령」이라도 대상 JVM 안에 스레드를 하나 만드는 일입니다.

6. 사례 1: 멈춤과 데드락

증상에서 도구로: 무엇을 먼저 보나 멈춤 · 응답 없음 jcmd Thread.print 5–10초 간격 세 번 같은 자리에 BLOCKED? 「Found one Java-level deadlock」 확인 실측: 2초 만에 두 스레드의 고리 CPU 가 높다 top -H 로 뜨거운 TID jstack 의 nid 와 맞추기 (JDK 21 은 10진수) JFR 10초 기록 jfr view hot-methods 실측: 460 샘플 100% 한 메서드 메모리 · OOM GC 로그: Full GC 뒤 줄지 않는가 GC.heap_info OOM 때 힙 덤프 (힙 크기만큼 디스크) 실측: 64MB 힙이 5.5초에 OOM 운영 파드에서 조심할 것 · GC.class_histogram 은 Full GC 를 일으킨다(실측으로 확인) · 힙 덤프는 그동안 JVM 을 멈추고, 파일에 개인정보·토큰이 그대로 담긴다 · JFR 파일에는 환경 변수 값이 들어간다(실측) — 밖으로 보내기 전에 scrub · 디버그 포트(JDWP)는 운영 이미지에 켜 두지 않는다
그림: 멈춤·CPU 과열·메모리 증가 세 증상에서 먼저 볼 도구와 실측 결과

두 스레드가 자물쇠 두 개를 반대 순서로 잡는 고전적인 예제(rally-A 는 table → net, rally-B 는 net → table)를 띄우고 2초 뒤 jstack 1 을 찍었습니다.

Found one Java-level deadlock:
=============================
"rally-A":
  waiting to lock monitor 0x0000ffff18001d30 (object 0x000000008bb13c80, a java.lang.Object),
  which is held by "rally-B"
"rally-B":
  waiting to lock monitor 0x0000ffff1c000f00 (object 0x000000008bb13c70, a java.lang.Object),
  which is held by "rally-A"
…
"rally-A":
	at Deadlock.grab(Deadlock.java:10)
	- waiting to lock <0x000000008bb13c80> (a java.lang.Object)
	- locked <0x000000008bb13c70> (a java.lang.Object)
…
Found 1 deadlock.

JVM 이 스스로 고리를 찾아 줍니다(공식 문제 해결 안내서의 예시와 같은 모양). 두 스레드의 CPU 시간은 각각 1.01ms·0.60ms 로, 멈춘 서비스가 CPU 를 쓰지 않는다는 것도 보입니다. 현실의 멈춤은 데드락보다 느린 무언가를 기다리는 것(DB 연결 풀 고갈, 바깥 HTTP 호출의 타임아웃 없음)이 더 흔합니다. 그래서 덤프는 5–10초 간격으로 세 번 찍어 같은 스레드가 같은 줄에 계속 있는지 봅니다. 한 장만 보면 우연히 그 순간 거기 있던 것인지 알 수 없습니다.

7. 사례 2: CPU 가 높다

한 스레드(referee-worker)만 계산을 돌리고 네 스레드는 잠만 자는 예제입니다. 순서는 어느 스레드인가 → 그 스레드가 무엇을 하나입니다.

  1. 컨테이너 안에서 top -H -p 1 로 스레드별 CPU 를 봅니다. 실측: TID 24 가 90.9%, 이름은 referee+(잘림).
  2. jstack 1 에서 그 TID 를 찾습니다. 스레드 줄의 nid 가 OS 스레드 번호입니다.
"referee-worker" #20 [24] … cpu=3922.52ms elapsed=3.96s … nid=24 runnable
   java.lang.Thread.State: RUNNABLE
	at Hot.scoreRally(Hot.java:7)

실측한 JDK 21 은 nid=24 처럼 10진수로 적고 [24] 도 함께 보여 줍니다. JDK 8·11 시절의 안내서는 nid=0x18 같은 16진수라 TID 를 16진수로 바꿔 찾으라고 하니, 판에 맞춰 읽어야 합니다.

  1. 스택 한 장은 한순간입니다. 「대체로 어디에 시간을 쓰나」는 JFR 로 봅니다. 돌고 있는 JVM 에 재시작 없이 기록을 시작했습니다.
$ jcmd 1 JFR.start name=hot duration=10s filename=/w/hot.jfr
$ jfr view hot-methods /w/hot.jfr
Method                  Samples Percent
Hot.scoreRally(long)        460 100.00%

10초 동안 실행 샘플 460개(기본 설정은 약 20ms 간격)가 모두 한 메서드였습니다. 같은 기록의 jfr view cpu-load 는 JVM 사용자 CPU 평균 9.02% 였는데, 이는 기계 전체(코어 10개) 대비 값이라 「코어 하나를 꽉 채웠다」와 같은 뜻입니다. 퍼센트가 무엇에 대한 비율인지 늘 확인합니다.

async-profiler(최신 v4.6)는 JVM 의 내부 API(AsyncGetCallTrace)와 리눅스 perf_events 를 써서 safepoint 편향 없이 자바와 네이티브 스택을 함께 잡는 도구입니다(공식 README). 플레임 그래프를 바로 만들어 주어 CPU 문제에 특히 좋지만, 내려받아 파드에 넣어야 해서 이번에는 미실측입니다.

8. 사례 3: 메모리가 계속 는다

64KB 프레임을 리스트에 넣기만 하고 빼지 않는 「캐시」를 최대 힙 64MB 로 띄웠습니다. 옵션은 운영에서 권하는 조합입니다.

java -Xmx64m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/w/heap.hprof \
     -XX:+ExitOnOutOfMemoryError "-Xlog:gc*:file=/w/gc.log:uptime,level,tags" -cp classes Leak

-Xlog:gc* 는 JDK 9 의 통합 로깅(JEP 158)과 통합 GC 로깅(JEP 271)으로 생긴 형식입니다. 결과는 이랬습니다.

시각 표준 출력 / GC 로그
1.9초 Pause Young (Normal) 23M->22M(64M) 8.5ms — 젊은 세대를 비웠는데 거의 안 줄었다
2.3초 Pause Full (Heap Inspection Initiated GC) 27M->26M — 내가 친 GC.class_histogram 이 일으킨 Full GC
3.2–5.3초 Young 과 Concurrent Mark 가 반복, 사용량 36M → 59M 로 계속 오름
5.45–5.55초 Evacuation Failure, 이어 Pause Full (G1 Compaction Pause) 59M->59M 세 번 — Full GC 를 해도 한 바이트도 안 준다
5.5초 OutOfMemoryError: Java heap space, 힙 덤프 65,304,426바이트를 0.073초에, 종료 코드 3

읽는 법은 하나입니다. Full GC 뒤에도 사용량이 내려오지 않으면, 살아 있는 무언가가 객체를 붙잡고 있다. 단순히 할당이 많은 것과 누수를 가르는 기준입니다. 2초 시점의 클래스 히스토그램은 범인의 모양까지 보여 줬습니다. [B(바이트 배열) 9,728개가 26,419,600바이트로 1위였습니다. 다만 그 명령이 Full GC 를 일으켰다는 것이 실측으로 남았습니다(2.3초 줄). 큰 힙의 운영 서비스에서 「가볍게 한번 보자」고 칠 명령이 아닙니다.

힙 덤프에서 「누가 그 배열들을 붙잡고 있나」(지배자 트리)를 찾는 일은 Eclipse MAT 이나 VisualVM 같은 분석 도구가 필요해 미실측입니다. 실측에서 확인한 운영상의 사실은 둘입니다. 덤프 파일은 힙 크기만큼(64MB 힙에 65MB) 커서, 1.4GB 힙의 파드라면 그만큼의 쓰기 가능한 볼륨이 필요합니다. 그리고 덤프에는 메모리에 있던 모든 것, 즉 사용자 정보와 토큰과 비밀번호가 그대로 들어 있으므로 비밀값처럼 다뤄야 합니다.

9. JFR: 늘 켜 두는 비행 기록

JFR(JDK Flight Recorder)은 JDK 11 에서 오픈소스가 됐고(JEP 328), 기본 설정의 목표 비용은 1% 이하입니다. JDK 21 의 default.jfc 는 「운영에서 계속 써도 안전, 보통 1% 미만」, profile.jfc 는 「보통 2% 안팎」이라고 스스로 적습니다. 정말 그런지, 해시 계산과 짧게 사는 객체 할당을 섞은 작업을 정해진 횟수(400만 번)만큼 돌려 쟀습니다. 설정 넷을 번갈아 7번씩, 컨테이너 CPU 2개로 묶었습니다.

설정 본 루프 시간 중앙값(7회) 기준 대비
기준(옵션 없음) 1,734ms —
JFR default 1,750ms +0.9%
JFR profile 1,753ms +1.1%
JDWP 에이전트만(디버거 안 붙음) 1,727ms −0.4%

기준 7회의 범위가 1,713–1,784ms 로 이미 ±2% 쯤 흔들리므로, 위 차이는 「문서의 1%·2% 안쪽」이라는 것까지만 말할 수 있습니다. 작업의 성격(할당이 아주 많거나, 락 경합이 심하거나)에 따라 달라지므로 자기 서비스에서 한 번 재 보기를 권합니다.

JFR 파일에는 환경 변수가 들어 있다

위 실측 기록의 jfr summary 에는 jdk.InitialEnvironmentVariable 11건과 jdk.InitialSystemProperty 15건이 있었고, jfr print 로 열어 보면 키와 값이 그대로 나왔습니다. 쿠버네티스 파드는 DB 비밀번호나 API 키를 환경 변수로 받는 일이 많으니, JFR 파일을 티켓에 붙이거나 밖으로 보내기 전에 지워야 합니다. JDK 21 의 jfr scrub 이 그 일을 합니다.

$ jfr scrub --exclude-events jdk.InitialEnvironmentVariable,jdk.InitialSystemProperty hot.jfr hot-clean.jfr

실측: 정리한 파일에서 두 이벤트는 0건이 됐고 실행 샘플 460건은 그대로였습니다(211,772 → 201,397바이트).

운영에서 JFR 을 쓰는 모양

10. 운영 파드에서의 순서와 주의

증상별로 처음 볼 것을 정리하면 그림 2 와 같습니다. 순서의 원칙은 부담이 적은 것부터입니다.

  1. 이미 남아 있는 것: 애플리케이션 로그, GC 로그, 메트릭, 켜 둔 JFR. 아무것도 건드리지 않습니다.
  2. 가벼운 읽기: VM.flags, GC.heap_info, VM.uptime. 여기서 힙 크기 설정이 잘못된 것(5절)을 먼저 거릅니다.
  3. 짧은 정지: Thread.print 를 간격을 두고 세 번.
  4. 기록: JFR.start duration=….
  5. 무거운 것: 힙 덤프, 클래스 히스토그램. 트래픽을 다른 파드로 돌리고(준비 상태에서 빼고) 하거나, -XX:+HeapDumpOnOutOfMemoryError 로 OOM 순간에만 자동으로 뜨게 둡니다.

그리고 피할 것입니다.

11. 정리

티티 한마디. 디버거로 랠리를 멈추면 공도, 상대도, 관중도 다 멈춰요. 경기 중에는 jcmd 로 사진만 찍고, 비디오(JFR)는 늘 켜 두는 게 좋아요. 아, 그리고 비디오에 탈의실 비밀번호(환경 변수)가 찍혀 있으니 공유하기 전에 꼭 지워요!

참고

로그인하면 좋아요를 누를 수 있습니다

댓글

아직 댓글이 없습니다.

로그인하면 댓글을 쓸 수 있습니다