JVM tools — jstack, jmap, jstat, JFR và async-profiler
Toolkit chẩn đoán production: jps liệt kê process, jstack thread dump deadlock, jmap heap dump leak, jstat metric realtime, jcmd Swiss-army knife, JFR profiling overhead <1%, async-profiler flame graph CPU/alloc. Workflow debug từ symptom đến root cause.
3h sáng. Pager: "API p99 latency 5s (SLA 200ms), CPU pegged 100%". Bạn SSH vào server prod. App vẫn chạy, không log error. Heap stable. Vậy gì đang xảy ra?
Câu trả lời nằm trong JVM tools — toolkit đi kèm JDK cho phép chẩn đoán JVM đang chạy mà không restart, không deploy code mới. Trong vài phút bạn có thể:
- Liệt kê thread đang block / chạy → tìm thread loop infinite hoặc deadlock.
- Snapshot heap → tìm object leak.
- Profile CPU → tìm method ăn CPU bất thường.
- Capture event GC, JIT, lock contention → reconstruct timeline.
Đây là kỹ năng phân biệt junior với senior Java engineer. Junior đoán, restart, hy vọng. Senior dùng tool, tìm root cause trong 30 phút.
Bài này đi qua: jps (list process), jstack (thread dump), jmap (heap dump), jstat (GC metric), jcmd (Swiss-army knife), JFR (Java Flight Recorder — production profiler), async-profiler (flame graph), và MAT (heap dump analysis). Kết thúc với workflow debug điển hình: từ symptom đến root cause.
1. Analogy — Bộ đồ nghề thợ điện
Thợ điện không "nghe" tủ điện đoán hỏng — họ có dụng cụ đo:
- Multimeter (đo điện áp/dòng) —
jps+jstat: đọc số liệu hiện tại. - Camera nhiệt —
jstack: chụp snapshot mọi "dây" (thread) đang nóng/lạnh, ai đang chờ ai. - Đo dòng dài hạn (data logger) —
JFR: ghi liên tục mọi event để phân tích sau. - Oscilloscope —
async-profiler: đo wave form chi tiết, thấy chính xác chỗ "spike". - Tháo tủ kiểm linh kiện —
jmapheap dump + MAT: xem từng "linh kiện" (object) trong tủ.
Mỗi tool có context phù hợp. Dùng nhầm tool = lãng phí thời gian. Vd jstack cho memory leak vô nghĩa — phải jmap.
| Đời thường | JVM tool |
|---|---|
| Multimeter | jps, jstat |
| Camera nhiệt | jstack |
| Oscilloscope | async-profiler |
| Tháo tủ kiểm | jmap + MAT |
| Data logger | JFR |
| Multimeter universal | jcmd |
Symptom → tool: CPU 100% → async-profiler / JFR; memory leak → jmap + MAT; app hang → jstack thread dump; GC liên tục → jstat / JFR; liệt kê → jps. jcmd thay được hầu hết tool cũ, ưu tiên dùng.
2. jps — list JVM process
Đầu tiên cần biết PID của JVM đang chạy.
jps -l
# 12345 com.myapp.MainApplication
# 23456 org.gradle.launcher.daemon.bootstrap.GradleDaemon
# 34567 sun.tools.jps.Jps
jps -v
# 12345 MainApplication -Xms2g -Xmx4g -XX:+UseG1GC ...
Flag:
-l: full class name hoặc path JAR.-v: JVM args.-m: main args.
jps chỉ thấy JVM cùng user. Sudo nếu cần thấy JVM khác user.
Container Docker: jps chạy ngoài container không thấy JVM trong container. Cần docker exec <container> jps.
3. jstack — thread dump
Dump trạng thái mọi thread. Câu trả lời cho "app hang", "high CPU 1 thread", "deadlock?".
jstack 12345 > thread-dump.txt
Output mẫu:
"http-nio-8080-exec-3" #45 daemon prio=5 os_prio=0 cpu=12345.67ms elapsed=3456.78s tid=0x00007f0c... nid=0x4567 waiting on condition [0x00007f0c...]
java.lang.Thread.State: WAITING (parking)
at jdk.internal.misc.Unsafe.park(Native Method)
- parking to wait for <0x000000076ab12345> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:341)
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:1623)
at com.myapp.Service.process(Service.java:42)
at com.myapp.Controller.handle(Controller.java:25)
...
Đọc:
"http-nio-8080-exec-3": tên thread.#45 daemon prio=5: ID, daemon flag, priority.cpu=12345.67ms: tổng CPU time thread đã dùng.Thread.State: WAITING (parking): trạng thái hiện tại (RUNNABLE, BLOCKED, WAITING, TIMED_WAITING).- Stack trace: từ top frame đi xuống.
parking to wait for <0x...>: thread đợi lock này.
State quan trọng
- RUNNABLE: đang chạy hoặc ready chạy. Nhiều RUNNABLE → CPU bound.
- BLOCKED: đợi lock (synchronized). Nhiều BLOCKED cùng địa chỉ lock → contention.
- WAITING / TIMED_WAITING: đợi notify hoặc condition. Thường idle (pool worker đợi task).
Detect deadlock
jstack tự detect deadlock. Output:
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x00007f0c... (object 0x000000076ab1, a java.lang.Object),
which is held by "Thread-2"
"Thread-2":
waiting to lock monitor 0x00007f0c... (object 0x000000076bc2, a java.lang.Object),
which is held by "Thread-1"
Java stack information for the threads listed above:
===================================================
"Thread-1":
at com.myapp.Foo.method1(Foo.java:10)
- waiting to lock <0x000000076ab1>
at ...
Báo rõ thread nào, lock nào, ở đâu. Fix: change lock order hoặc dùng tryLock timeout.
High CPU 1 thread
# Tim thread an CPU cao
top -H -p 12345
# PID USER ... %CPU
# 12399 luatnq ... 98.7
# 12400 luatnq ... 2.3
# Convert TID sang hex de match nid trong jstack
printf '%x\n' 12399
# 3057
# Tim thread voi nid=0x3057 trong dump
jstack 12345 | grep -A 30 'nid=0x3057'
Stack thread đó cho biết method nào loop / busy spin.
Triple dump pattern
Hang state có thể là snapshot 1 thread tạm dừng — không phải bug. Để confirm:
for i in 1 2 3; do jstack 12345 > dump-$i.txt; sleep 5; done
# So sanh dump-1, dump-2, dump-3
# Thread van o cung stack frame qua 15s -> stuck
Real bug: thread cùng stack frame qua 3 dump. Nếu dump khác nhau → thread đang làm việc, không stuck.
4. jmap — heap dump và stats
Heap histogram (live object count + size)
jmap -histo:live 12345 | head -20
Output:
num #instances #bytes class name
----------------------------------------------
1: 500000 40000000 com.myapp.Order
2: 500000 32000000 java.lang.String
3: 1000000 16000000 java.util.HashMap$Node
4: 500000 12000000 [B (byte array)
...
Top class theo size. Tìm leak: class lạ với count cao = candidate.
:live ép GC trước → chỉ thấy live object. Bỏ → thấy cả object chờ GC, noisy.
Heap dump (full snapshot)
jmap -dump:live,format=b,file=heap.hprof 12345
Dump 1 file .hprof chứa toàn bộ object trong heap. Size = heap size (vài GB cho production typical). Chậm vài giây — pause app trong khi dump.
-XX:+HeapDumpOnOutOfMemoryError config sẵn JVM dump tự khi OOM.
Mở file bằng:
- Eclipse MAT (Memory Analyzer Tool) — desktop, đầy đủ feature.
- VisualVM — bundled JDK.
- JDK Mission Control — production tool, có heap analyzer.
Tip: dump 2 lần cách nhau 5 phút, so sánh diff trong MAT — class tăng nhiều = leak candidate.
5. jstat — realtime metric
Theo dõi GC, class loading, JIT realtime.
GC overview
jstat -gc 12345 1000
# Sample mỗi 1000ms
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
8192.0 8192.0 0.0 7234.5 65536.0 45123.2 131072.0 78901.2 20480.0 19234.5 2048.0 1923.4 145 1.234 3 0.567 1.801
8192.0 8192.0 4521.3 0.0 65536.0 12345.6 131072.0 79123.4 20480.0 19234.5 2048.0 1923.4 146 1.245 3 0.567 1.812
Cột:
S0C/S1C/EC/OC/MC: capacity (KB) Survivor 0, Survivor 1, Eden, Old, Metaspace.S0U/S1U/EU/OU/MU: used.YGC/YGCT: young GC count + total time.FGC/FGCT: full GC count + total time.GCT: total GC time.
Theo dõi OU tăng dần qua sample → old gen growing → leak hoặc promote rate cao.
Tính toán nhanh
jstat -gcutil 12345 1000
# S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
# 0.00 88.31 68.85 60.20 93.92 91.83 145 1.234 3 0.567 1.801
-gcutil show % thay vì KB — dễ đánh giá health. O 60% healthy, O 95% red flag.
6. jcmd — Swiss-army knife
jcmd (Java 8+) thay nhiều tool cũ. Ưu tiên dùng jcmd trong production.
jcmd 12345 help # Liet ke command available
jcmd 12345 GC.heap_info # Heap stats hien tai
jcmd 12345 GC.run # Force GC (don't usually)
jcmd 12345 GC.class_histogram # = jmap -histo
jcmd 12345 GC.class_stats # Per-class loaded info
jcmd 12345 Thread.print # = jstack
jcmd 12345 VM.flags # JVM flag effective
jcmd 12345 VM.system_properties # System property
jcmd 12345 VM.uptime # JVM running time
jcmd 12345 VM.classloader_stats # Loader stats - debug metaspace leak
JFR control:
jcmd 12345 JFR.start name=profile duration=60s filename=profile.jfr
jcmd 12345 JFR.dump name=profile filename=profile.jfr
jcmd 12345 JFR.stop name=profile
Native memory tracking:
java -XX:NativeMemoryTracking=summary MyApp
jcmd 12345 VM.native_memory summary
# Show malloc breakdown: heap, class, thread, code, GC, internal, ...
Quan trọng debug "JVM ăn nhiều memory hơn -Xmx" — show native memory breakdown.
7. JFR — Java Flight Recorder
JFR là production-grade profiler built-in JVM. Capture event (GC, JIT, lock, IO, allocation, exception) với overhead <1% — bật được trong production.
Lịch sử
JFR được Oracle ship trong JRockit (commercial JVM mua lại từ BEA), sau merge vào HotSpot. Open-source từ Java 11 (JEP 328).
Trước Java 11: cần Oracle JDK + license. Java 11+: free, mainline OpenJDK.
Khởi động JFR
Pre-recorded (continuous)
java -XX:StartFlightRecording=duration=60s,filename=profile.jfr MyApp
Record 60s từ khi JVM start, dump file.
On-demand
# Start recording
jcmd 12345 JFR.start name=mysession duration=120s filename=/tmp/profile.jfr
# Check status
jcmd 12345 JFR.check
# Stop early
jcmd 12345 JFR.stop name=mysession
Always-on profile
java -XX:StartFlightRecording=settings=profile,maxsize=200m,maxage=1h,disk=true MyApp
JFR ghi liên tục, giữ 1 giờ gần nhất hoặc 200MB. On-demand JFR.dump lấy snapshot — debug post-incident: "30 phút trước hệ thống lag, có pattern gì?".
Profile preset: default (low overhead), profile (more events, ~2% overhead). Custom preset qua .jfc file XML.
Phân tích JFR file
JDK Mission Control (JMC) — desktop tool free, mở .jfr file:
- Garbage Collections: list cycle, pause time, cause.
- Method Profiling: flame graph CPU usage.
- Memory Allocation: top allocation site (class, thread, method).
- Thread: pause, contention, lock wait.
- JVM Internal: compilation event, deoptimization.
- I/O: file read, network read latency.
JMC interactive — drill down từ summary đến exact stack trace + line number.
CLI alternative:
jfr print --events GarbageCollection,MetaspaceOOM profile.jfr
Event quan trọng
jdk.GarbageCollection: mỗi GC cycle với cause + duration.jdk.AllocationOutsideTLAB: object alloc đặc biệt (large object) — tracking allocation hot.jdk.JavaMonitorWait/jdk.JavaMonitorEnter: thread đợi lock — contention analysis.jdk.Compilation/jdk.Deoptimization: JIT activity.jdk.SocketRead/jdk.SocketWrite/jdk.FileRead: I/O latency.jdk.ExceptionThrown: mỗi exception throw — debug "exception storm".
8. async-profiler — flame graph chi tiết
JFR overhead thấp, sample-based — đủ cho overview. Cần detail hot method? async-profiler (https://github.com/async-profiler/async-profiler) — open-source, không phải Oracle, profiling theo CPU sampling và allocation tracking.
# Profile 30s, output flame graph
asprof -d 30 -f /tmp/flame.html 12345
# CPU profile
asprof -e cpu -d 30 -f cpu.html 12345
# Allocation profile (where alloc happens)
asprof -e alloc -d 30 -f alloc.html 12345
# Lock profile (contention)
asprof -e lock -d 30 -f lock.html 12345
Flame graph HTML: stack visualization. Trục X = % time CPU, trục Y = stack depth. Method "wide" = ăn nhiều CPU. Click để zoom.
Pattern đọc:
- Plateau wide ở top: hot method dùng CPU thực sự.
- Tower cao: deep call chain — có thể inline opportunity.
- Multiple wide top: nhiều hot path — phân bổ optimize.
async-profiler ưu điểm vs JFR:
- Native stack: thấy JNI / native call (JFR chỉ Java frame).
- Flame graph beautiful: intuitive hơn JMC table.
- Allocation profiling chi tiết hơn: thấy chính xác alloc per method.
Dùng song song JFR (continuous, low overhead) + async-profiler (deep dive khi đã identify suspicious).
9. Eclipse MAT — heap dump analysis
Khi có heap leak, dump .hprof → mở MAT (Eclipse Memory Analyzer Tool).
Workflow điển hình
-
Generate dump:
jmap -dump:live,format=b,file=heap.hprof 12345Hoặc auto:
-XX:+HeapDumpOnOutOfMemoryError. -
Open MAT → File → Open Heap Dump.
-
Run "Leak Suspects" report — MAT auto-analyze, suggest top leak candidate.
-
Histogram view: list class theo retained size (memory bị giữ vì class này).
- Sort theo Retained Heap descending.
- Class với retained > 50% heap = strong candidate.
-
Dominator Tree: object dominate (giữ) memory nhiều nhất. Drill down: Object X giữ 1GB → ai giữ X?
-
Find GC Roots: class nghi leak → "Path to GC Roots" → MAT show chain reference giữ object live.
MyOrder (1.5 KB) └── elementData[42] of java.util.ArrayList (40 KB) └── orders field of com.myapp.OrderCache (instance) └── INSTANCE field of com.myapp.OrderCache (static) ← GC root! -
Identify root cause: static field
OrderCache.INSTANCE.ordersgiữ ArrayList grow vô hạn → leak.
OQL — query heap
MAT có DSL giống SQL query heap:
SELECT s.value FROM java.lang.String s WHERE s.value.length > 1000
Tìm String dài bất thường — log message, query SQL build vô tận, etc.
10. Workflow debug điển hình
Symptom: API p99 5s, CPU 100%
# 1. Tim PID
jps -l
# 2. Top thread CPU
top -H -p <PID>
# Note nhung TID an CPU cao
# 3. Convert TID sang hex
printf '%x\n' <TID>
# 4. Thread dump
jstack <PID> > dump.txt
# 5. Tim thread voi nid=0x<hex>
grep -A 30 'nid=0x<hex>' dump.txt
# 6. Stack frame: thay method nao busy spin
Nếu nhiều thread → flame graph:
asprof -d 30 -f cpu.html <PID>
# Mo browser, find wide method top
Symptom: OOM Java heap space
# 1. Auto dump (config truoc khi crash)
JAVA_OPTS="-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/heap"
# 2. Sau crash, copy heap.hprof ve local
scp server:/var/log/heap/java_pid12345.hprof .
# 3. Mo MAT
# Run "Leak Suspects Report"
# Investigate top retained class
# 4. Look at GC roots holding suspicious instance
# Identify static field / cache / collection growing
Symptom: GC chạy liên tục, throughput thấp
# 1. jstat realtime
jstat -gcutil <PID> 1000
# Pattern xau:
# - YGC tang nhanh (vai giay 1 lan)
# - O% leo cao (~95%) lien tuc
# - FGC tang -> co full GC
# - GCT chiem >5% uptime
# 2. JFR capture allocation hot
jcmd <PID> JFR.start name=alloc duration=60s filename=alloc.jfr
# 3. Mo JMC, Memory Allocation tab
# - Top allocation method
# - Allocation rate
# 4. Refactor code: cat alloc thua (string concat trong loop, intermediate stream object, vong loop tao throw-away object)
Symptom: app hang, không response
# 1. Triple dump confirm hang
for i in 1 2 3; do
jstack <PID> > dump-$i.txt
sleep 5
done
# 2. Diff dump
diff dump-1.txt dump-2.txt
# 3. Check deadlock
grep "Found one Java-level deadlock" dump-1.txt
# 4. Check tat ca thread state
grep "Thread.State" dump-1.txt | sort | uniq -c
# Nhieu BLOCKED -> contention
# Tat ca WAITING -> deadlock luc nha hoac cho external event
# 5. Tim lock contention
grep -A 5 "BLOCKED" dump-1.txt | grep "waiting to lock"
11. Pitfall tổng hợp
❌ Nhầm 1: Restart trước khi capture diagnosis.
Production hang -> restart -> mat thread state -> khong root cause
✅ Capture jstack + jmap (nếu RAM cho phép) trước restart.
❌ Nhầm 2: Heap dump trên production đang serve traffic.
jmap -dump 12345 # Pause app vai giay
✅ Drain traffic / fail-over trước. Hoặc trigger qua -XX:+HeapDumpOnOutOfMemoryError (chỉ khi đã OOM).
❌ Nhầm 3: Đọc thread dump 1 lần.
1 dump = snapshot 1 thoi diem. Co the thread tinh co o stack do, khong stuck.
✅ Triple dump, so sánh.
❌ Nhầm 4: Bỏ qua JFR vì nghĩ "tốn performance".
JFR overhead <1% production-grade. Bat always-on co loi hon overhead.
✅ Always-on JFR với rotation 1 giờ là standard production setup.
❌ Nhầm 5: Cài MAT trên production.
MAT cần GUI, chiếm RAM ngang heap dump (load full).
✅ Copy .hprof về dev machine, MAT local.
❌ Nhầm 6: Chỉ dùng jstack cho mọi vấn đề.
Memory leak -> jstack vo nghia. CPU high -> jstack thieu chi tiet method.
✅ Match tool với symptom (mục 10).
❌ Nhầm 7: Tin số jstat cho trend dài hạn.
jstat sample tung thoi diem, khong tat ca event GC.
✅ JFR cho long-term, jstat cho realtime quick check.
12. 📚 Deep Dive Oracle
Spec / reference chính thức:
- Troubleshooting Guide for HotSpot VM (Java 21) — manual chính thức, đọc theo symptom.
- JEP 328: Flight Recorder — JFR open-source.
- JEP 349: JFR Event Streaming — Java 14, stream event realtime.
- JDK Mission Control — JMC download, free.
- Eclipse MAT — Memory Analyzer Tool.
- async-profiler — flame graph, low-overhead.
- OpenJDK Code Tools: AsyncGetCallTrace — internal API mà async-profiler dùng.
- "Java Performance: The Definitive Guide" - Scott Oaks — sách chuẩn về JVM tuning + tooling.
Ghi chú: Troubleshooting Guide official có "decision tree" theo symptom — bookmark khi on-call. JFR Event Streaming (JEP 349) cho phép subscribe event realtime → ship vào monitoring (Prometheus, Datadog) — production-grade observability không cần restart. async-profiler dùng AsyncGetCallTrace — internal HotSpot API safe-point free, lý do sample được mọi method (kể cả native) với overhead thấp. Scott Oaks book là reference depth nhất về performance + tooling, đặc biệt chương GC tuning và profiling.
13. Tóm tắt
jps -lv: list JVM process với args.jstack <pid>: thread dump. State: RUNNABLE, BLOCKED, WAITING. Tự detect deadlock.- Triple dump (3 lần cách 5s) confirm thread thực sự stuck.
- High CPU 1 thread:
top -H -p <pid>→ TID hex → matchnidtrong jstack. jmap -histo:live <pid>: count + size per class. Tìm leak candidate.jmap -dump:live,format=b,file=heap.hprof <pid>: full heap dump. Pause app vài giây.-XX:+HeapDumpOnOutOfMemoryError: auto dump khi OOM.jstat -gcutil <pid> 1000: realtime % usage. Old > 95% red flag.jcmdthay nhiều tool cũ.jcmd <pid> helplist command. Ưu tiên dùng.- JFR (Java Flight Recorder): production profiler, overhead <1%. Always-on với rotation 1 giờ.
- JMC (Mission Control): GUI phân tích
.jfrfile — GC, allocation, lock contention, exception, IO. - async-profiler: flame graph CPU + allocation + lock. Native stack support. Dùng song song JFR.
- Eclipse MAT: heap dump analyzer. "Leak Suspects" report tự suggest. Dominator Tree show object retain memory nhiều. Path to GC Roots tìm chain leak.
- Workflow CPU 100%: top -H → jstack → identify hot method.
- Workflow OOM: heap dump (auto) → MAT → leak suspect → trace GC roots.
- Workflow GC liên tục: jstat → JFR → JMC allocation hot → refactor cut alloc.
- Workflow hang: triple jstack → diff → grep BLOCKED / WAITING / deadlock.
- Container:
docker exec <container> jcmd ...để chạy tool trong namespace JVM. - Capture diagnostic trước khi restart — restart mất state.
14. Tự kiểm tra
Q1Workflow nào để debug "API p99 latency 5s, CPU 100% trên 1 core" trên production?▸
Step-by-step:
- List PID:
jps -l # Tim com.myapp.MainApplication PID = 12345 - Tìm thread ăn CPU cao:
top -H -p 12345 # %CPU column - thread TID 12399 dang 98% - Convert TID sang hex (jstack dùng hex):
printf '%x\n' 12399 # 3057 - Thread dump + grep:Stack frame top cho biết method nào busy.
jstack 12345 > /tmp/dump.txt grep -A 30 'nid=0x3057' /tmp/dump.txt - Triple dump confirm:
for i in 1 2 3; do jstack 12345 > dump-$i.txt; sleep 5; done diff dump-1.txt dump-2.txt # Thread van o stack do qua 15s -> stuck busy
Nếu CPU spread nhiều thread (không 1 thread dominate) → dùng async-profiler:
asprof -e cpu -d 30 -f /tmp/cpu.html 12345
# Mo /tmp/cpu.html trong browser
# Wide method top = hot pathCommon pattern phát hiện:
- Infinite loop: stack ở method với
while(true)không exit condition. - Regex catastrophic backtracking: stack ở
java.util.regex.Pattern$.... - Hash collision attack: stack ở
HashMap.putvới chain dài. - Lock contention: nhiều thread BLOCKED cùng monitor → 1 thread trong synchronized làm chậm.
Tránh: restart trước capture — mất diagnosis. Capture xong (jstack + flame graph) mới restart nếu cần.
Q2Khác biệt JFR và async-profiler — khi nào dùng cái nào?▸
JFR (Java Flight Recorder):
- Built-in JVM, không cài thêm. Java 11+ open-source.
- Overhead cực thấp <1%. Production safe always-on.
- Event-based: ghi event GC, JIT, lock, IO, allocation, exception. Rich metadata mỗi event.
- Java frame only: không thấy native / JNI stack.
- JMC GUI phân tích — table-based, drill down sâu.
async-profiler:
- External tool, cần cài (binary release).
- Overhead 1-3% (sampling rate cao hơn JFR).
- Sampling-based: mỗi N ms snapshot stack tất cả thread.
- Native + Java stack: thấy JNI, native lib, kernel call (perf integration).
- Flame graph SVG/HTML — visualization rất intuitive.
- Multiple event source: CPU (cycles, instructions), allocation, lock, page-fault.
Khi nào dùng JFR:
- Always-on production monitoring: 1-hour rotation, dump on incident.
- Cần event chi tiết: GC cause, lock owner, exception thrown count.
- Pure Java stack đủ: app không dùng native nhiều.
Khi nào dùng async-profiler:
- Flame graph để identify hot path: visual hơn JMC.
- Native stack quan trọng: app dùng JNI, native crypto, native DB driver.
- Allocation profiling chi tiết: per-method allocation rate.
- Lock contention deep: per-lock visualization.
Production setup chuẩn: JFR always-on (continuous safety net) + async-profiler on-demand (deep dive when needed). Không phải either/or.
Cả 2 dùng AsyncGetCallTrace JVM internal API safe-point free — lấy stack mà không pause thread, lý do overhead thấp.
Q3Heap dump 8GB. MAT report top "Retained Heap": com.myapp.OrderCache 7GB. Workflow tiếp theo để xác định leak?▸
com.myapp.OrderCache 7GB. Workflow tiếp theo để xác định leak?Đã có suspect — giờ trace nguyên nhân:
- Open class trong MAT: Histogram → search "OrderCache" → right-click → "List objects" → "with incoming references".
- Tìm GC root: select instance OrderCache → "Path to GC Roots" → "exclude weak/soft references".
Result example: OrderCache@0x... (7 GB retained) cache field of OrderService@0x... instance field of OrderManager@0x... INSTANCE field of OrderManager (static) ← GC root - Inspect content: select OrderCache → "List objects" → field
orders(Map) → drill down.- Size map: 5,000,000 entry → cache không bound.
- Key: OrderId String hash → user history hoặc transient ID.
- Value: Order với 50 field → mỗi entry ~1.4 KB.
- Histogram retained per class trong subtree: select OrderCache → "Show retained set" → list class chiếm memory.
- OQL query: nếu nghi cụ thể, query:Tìm Order vượt 1 ngày tuổi vẫn trong cache → confirm không evict.
SELECT * FROM com.myapp.Order o WHERE o.timestamp < currentTimeMillis() - 86400000 - Identify root cause trong code:
// File: OrderManager.java public static final OrderManager INSTANCE = new OrderManager(); private final Map<String, Order> cache = new HashMap<>(); // Khong bound! public void save(Order o) { cache.put(o.id, o); // Add forever db.persist(o); } - Fix:
- Thay HashMap → Caffeine cache với max size + TTL:
Caffeine.newBuilder().maximumSize(10_000).expireAfterWrite(Duration.ofMinutes(30)).build(). - Hoặc remove cache hoàn toàn nếu DB query đủ nhanh.
- Verify với heap dump sau deploy fix → OrderCache size stable.
- Thay HashMap → Caffeine cache với max size + TTL:
Pattern leak phổ biến tìm bằng MAT:
- Static collection growing: như case này.
- Listener không unregister: subscriber giữ reference event source.
- ThreadLocal không remove: Tomcat redeploy classloader leak.
- Inner class reference outer: anonymous class giữ enclosing instance.
- Cache mã hash không clean: WeakHashMap không đủ aggressive nếu key referenced elsewhere.
Always 2-step: identify retained (MAT histogram) + trace path GC root (Path to GC Roots). Cả 2 cùng vẽ ra root cause.
Q4Vì sao `jstat -gcutil` thấy "Old 95%" liên tục lại là red flag, dù chưa OOM?▸
Old 95% liên tục nghĩa:
- Promote rate cao: object survive young GC → promote sang Old. Old fill nhanh.
- GC không reclaim được nhiều: object trong Old phần lớn vẫn live → mark-sweep-compact không free đủ.
Hệ quả chuỗi:
- Old gần đầy → trigger major GC / mixed GC thường xuyên hơn.
- Major GC pause dài (hàng trăm ms với heap GB) → latency spike.
- Major GC không free đủ → tiếp tục đầy → full GC last resort, pause vài giây.
- Full GC vẫn không free → OOM "GC overhead limit exceeded" hoặc heap space.
Pattern detection:
# Healthy
S0 S1 E O M YGC YGCT FGC FGCT
0.00 88.31 68.85 60.20 93.92 145 1.234 3 0.567
0.00 88.50 70.12 60.85 93.92 146 1.245 3 0.567
# Red flag - O lien tuc tang, FGC nhieu
0.00 88.31 68.85 85.20 93.92 145 1.234 8 2.567
0.00 90.50 70.12 90.85 93.92 146 1.245 12 4.123
0.00 92.50 72.12 95.20 93.92 147 1.256 18 7.890
# FGC tang nhanh = pressure -> sap OOM2 nguyên nhân thường:
- Memory leak: object không bao giờ unreachable → Old grow vĩnh viễn. Heap dump → MAT.
- Heap quá nhỏ: app workload thực sự cần nhiều memory. Tăng
-Xmx.
Phân biệt:
- Heap dump 2 lần cách 5 phút.
- So sánh class top retained.
- Nếu class app growing (Order, User, Cache entry) → leak.
- Nếu workload steady, mọi class proportional → undersized heap.
Quick fix:
- Tăng
-Xmxtạm — buy time tìm leak. - Bật
-XX:+HeapDumpOnOutOfMemoryErrorđể có dump nếu crash. - JFR allocation profile → tìm hot allocation site.
Long-term: monitor Old % qua time series (Prometheus / Datadog), alert khi Old % > 80% sustained 10 phút.
Q5Vì sao thread dump phải đọc 3 lần (triple dump) thay vì 1 lần?▸
1 thread dump = snapshot 1 microsecond. Có nhiều cách đọc sai:
- Thread tình cờ ở method bạn nghi ngờ — không stuck, đang chạy bình thường.
- Tất cả thread WAITING trong lúc dump — không phải hang, chỉ là pool worker đang idle giữa task.
- Stack trace 1-2 frame — không đủ context (thread vừa enter method, chưa làm gì).
Triple dump (3 lần cách 5 giây):
- Capture 3 snapshot khác thời điểm.
- So sánh: thread state + stack frame.
- Confirm pattern qua time.
Pattern phân tích:
Thread X:
Dump 1: BLOCKED, waiting on lock 0xABC, frame: Service.process(L42)
Dump 2: BLOCKED, waiting on lock 0xABC, frame: Service.process(L42)
Dump 3: BLOCKED, waiting on lock 0xABC, frame: Service.process(L42)
=> Thread stuck waiting lock 0xABC suot 15s -> contention real
Thread Y:
Dump 1: RUNNABLE, frame: ArrayList.add(L100)
Dump 2: RUNNABLE, frame: HashMap.put(L200)
Dump 3: RUNNABLE, frame: String.equals(L80)
=> Thread dang lam viec binh thuong qua nhieu method -> khong stuck
Thread Z:
Dump 1: RUNNABLE, frame: MyService.compute(L50)
Dump 2: RUNNABLE, frame: MyService.compute(L50)
Dump 3: RUNNABLE, frame: MyService.compute(L50)
=> Stuck o 1 method -> infinite loop hoac hot spinDiff command:
# Diff state count
for f in dump-*.txt; do
echo "=== $f ==="
grep "Thread.State" "$f" | sort | uniq -c
done
# Diff specific thread
diff <(grep -A 10 'http-nio-8080-exec-1' dump-1.txt) \
<(grep -A 10 'http-nio-8080-exec-1' dump-2.txt)Production guideline: khi capture diagnostic cho hang/CPU issue, luôn 3 dump cách 5-10s. Single dump cho deadlock detection (jstack tự detect).
Tool tự động: jcmd <pid> Thread.print hoặc script:
for i in 1 2 3; do
jstack <PID> > /tmp/dump-$i.txt
sleep 5
done
echo "Captured 3 dumps in /tmp/"Q6Vì sao jcmd được khuyến khích thay jstack / jmap trong production?▸
jcmd được khuyến khích thay jstack / jmap trong production?Lý do kỹ thuật:
- `jcmd` dùng cùng JVM attach API nhưng implementation tốt hơn:
jstack/jmapdùng SA (Serviceability Agent) — pause toàn JVM trong khi dump.jcmdcommand nhưThread.print,GC.heap_dumpdùng VM internal call — pause ngắn hơn, đôi khi không pause.
- `jcmd` thống nhất tool: 1 binary cho mọi operation. Không cần nhớ
jstackvsjmapvsjstatvsjinfo. - JFR control qua jcmd:
JFR.start,JFR.dump,JFR.stop— JFR là production profiler chuẩn, jcmd là interface chính thức. - Native Memory Tracking (NMT):
jcmd VM.native_memory— debug "JVM ăn memory hơn -Xmx" — không có tool tương đương cũ. - Active development: command mới (heap analyzer integration, snapshot, ZGC stats) thêm vào
jcmd. Tool cũ frozen.
So sánh cụ thể:
| Tool cũ | jcmd equivalent |
|---|---|
jstack <pid> | jcmd <pid> Thread.print |
jmap -histo <pid> | jcmd <pid> GC.class_histogram |
jmap -dump:format=b,file=h.hprof <pid> | jcmd <pid> GC.heap_dump h.hprof |
jinfo <pid> | jcmd <pid> VM.flags + VM.system_properties |
| — | jcmd <pid> JFR.start ... (JFR) |
| — | jcmd <pid> VM.native_memory (NMT) |
Container ưu thế:
docker exec <container> jcmd <pid> ...— chạy trong namespace JVM, không dependency host.- Tool cũ đôi khi cần libs host khớp — vấn đề trong container minimal.
Khi dùng tool cũ vẫn hợp lý:
- Script CI/CD:
jstackoutput format stable hơn, dễ parse cho grep. - Java <8:
jcmdchưa có nhiều command. - Dev local: muscle memory, đỡ học mới.
Production: jcmd + JFR là combo chuẩn 2025. Tool cũ giữ cho legacy script.
Bài tiếp theo: Mini-challenge: Debug memory leak với JFR và heap dump
Bài này có giúp bạn hiểu bản chất không?
Bình luận (0)
Đang tải...