JFR và profiling — Flight Recorder, JMC, async-profiler
JFR là production profiler built-in JVM với overhead dưới 1% — always-on được. JMC phân tích GC, allocation, lock contention. async-profiler vẽ flame graph CPU/alloc/lock với native stack. Workflow profiling từ GC pressure đến hot allocation site.
TL;DR: CLI tool (bài 07) cho snapshot tại một thời điểm; khi cần timeline — hệ thống lag 30 phút trước vì gì — bạn cần profiler ghi liên tục. JFR (Java Flight Recorder) là profiler built-in JVM, capture event (GC, JIT, lock, I/O, allocation, exception) với overhead dưới 1% — bật always-on trong production với rotation theo maxage/maxsize, dump snapshot khi có incident. Phân tích bằng JDK Mission Control (JMC). Cần flame graph chi tiết hơn (kể cả native stack)? async-profiler — sampling qua AsyncGetCallTrace + perf_events, không chờ safepoint. Setup chuẩn production: JFR always-on (safety net) + async-profiler on-demand (deep dive).
Bài 07 dừng ở các tool snapshot: jstack chụp 1 khoảnh khắc, jstat đọc số hiện tại. Nhưng câu hỏi on-call thường gặp là về quá khứ: "30 phút trước hệ thống lag — lúc đó GC thế nào, thread nào contention, allocation từ đâu?" Snapshot sau sự kiện không trả lời được. Cần một "hộp đen máy bay" ghi liên tục.
Bài này đi qua: JFR (Java Flight Recorder — production profiler), JMC (Mission Control — đọc recording), async-profiler (flame graph CPU/alloc/lock), và workflow profiling điển hình: từ GC pressure đến hot allocation site.
1. JFR — Java Flight Recorder
JFR là production-grade profiler built-in JVM. Capture event (GC, JIT, lock, IO, allocation, exception) với overhead dưới 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 qua jcmd
# 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ì?".
jcmd <PID> JFR.dump filename=/tmp/incident-$(date +%s).jfr
Profile preset: default (low overhead), profile (more events, ~2% overhead). Custom preset qua .jfc file XML.
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".
2. JMC — 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
3. 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).
4. Workflow — GC chạy liên tục, throughput thấp
# 1. jstat realtime (bai 07) - confirm pattern
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 dang ke uptime (vuot 5%)
# 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)
Xác nhận lại bằng async-profiler allocation flame graph nếu cần đến mức per-method:
asprof -e alloc -d 30 -f alloc.html <PID>
5. Pitfall tổng hợp
❌ Nhầm 1: 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 2: CPU flame graph cho vấn đề memory.
App cham vi GC pressure -> CPU profile chi thay GC thread.
Phai profile allocation (-e alloc / JFR Memory tab).
✅ Chọn event type theo symptom: cpu / alloc / lock.
❌ Nhầm 3: Profile trên dev machine rồi kết luận cho production.
Dev: data nho, JIT chua warm, khong contention.
Production: data lon, 50 thread, cache miss khac han.
✅ Profile trên production (JFR overhead thấp cho phép) hoặc môi trường tải tương đương.
6. 📚 Deep Dive Oracle
Spec / reference chính thức:
- JEP 328: Flight Recorder — JFR open-source.
- JEP 349: JFR Event Streaming — Java 14, stream event realtime.
- JDK Mission Control — JMC download, free.
- async-profiler — flame graph, low-overhead.
- OpenJDK Code Tools: AsyncGetCallTrace — internal API mà async-profiler dùng.
- "Java Performance: The Definitive Guide" - Scott Oaks — chương profiling.
Ghi chú: 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 safepoint-free, lý do sample được mọi method (kể cả native) với overhead thấp.
7. Tóm tắt
- JFR (Java Flight Recorder): production profiler built-in, overhead dưới 1%. Open-source từ Java 11 (JEP 328).
- 3 mode: record từ start (
-XX:StartFlightRecording), on-demand (jcmd JFR.start/stop), always-on vớimaxsize/maxagerotation. - Always-on +
JFR.dumpkhi incident = debug được quá khứ — pattern "hộp đen máy bay". - JMC (Mission Control): GUI phân tích
.jfr— GC, allocation, lock contention, exception, I/O. - async-profiler: flame graph CPU + allocation + lock. Native stack support. Sampling qua
AsyncGetCallTrace, không chờ safepoint. - Flame graph: trục X = % time, plateau rộng trên đỉnh = hot method; tower cao = call chain sâu.
- Chọn event theo symptom: CPU cao →
-e cpu; GC pressure → JFR Memory tab /-e alloc; chậm vì lock →-e lock. - Workflow GC liên tục: jstat confirm → JFR allocation profile → JMC top allocation site → refactor cắt alloc.
- Setup chuẩn production: JFR always-on (safety net) + async-profiler on-demand (deep dive). Không phải either/or.
8. Tự kiểm tra
Q1Khá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.
Q2Vì sao JFR đạt overhead dưới 1% trong khi profiler instrumentation truyền thống tốn 10-30%?▸
Profiler instrumentation (kiểu cũ): chèn bytecode đo đạc vào đầu và cuối mọi method — mỗi lời gọi method gánh thêm vài instruction + ghi dữ liệu. Method nhỏ gọi hàng triệu lần/giây → overhead phình 10-30%, và tệ hơn: instrumentation làm JIT inline khác đi → số đo không còn phản ánh app thật.
JFR thiết kế khác từ gốc:
- Event-based, nằm sẵn trong JVM: GC, JIT, lock... vốn đã xảy ra trong JVM — JFR chỉ ghi lại metadata tại chỗ, không chèn code vào app.
- Method profiling bằng sampling (chụp stack định kỳ) thay vì đo mọi call — cost cố định theo sampling rate, không theo số lần gọi method.
- Per-thread buffer lock-free: mỗi thread ghi event vào buffer riêng, không tranh lock; buffer đầy mới flush sang global ring buffer / disk.
- Binary format + threshold filter: event ngắn hơn ngưỡng (vd lock wait dưới 10ms) bị bỏ qua từ đầu, không tốn công ghi.
Trade-off chấp nhận: sampling có thể bỏ sót method hiếm gặp, và threshold filter ẩn event nhỏ. Đổi lại được con số trung thực trên production — nơi instrumentation profiler không dám bật.
Q3Always-on JFR với maxage=1h,maxsize=200m hoạt động thế nào, và vì sao là setup chuẩn production?▸
maxage=1h,maxsize=200m hoạt động thế nào, và vì sao là setup chuẩn production?Cơ chế: JFR ghi event liên tục vào ring buffer trên disk (khi disk=true). Khi dữ liệu vượt maxage (1 giờ) hoặc maxsize (200MB), chunk cũ nhất bị xoá — luôn giữ "1 giờ gần nhất". Giống camera hành trình ô tô: ghi đè liên tục, chỉ giữ đoạn mới.
Khi incident xảy ra (alert lúc 3h05):
jcmd <PID> JFR.dump filename=/tmp/incident.jfrFile dump chứa toàn bộ 1 giờ trước đó — bao gồm chính khoảnh khắc 3h00 khi hệ thống lag. Đây là điều không tool snapshot nào (jstack, jstat) làm được: chúng chỉ thấy hiện tại, không thấy quá khứ.
Vì sao là setup chuẩn:
- Overhead dưới 1% — chi phí thường trực không đáng kể so với giá trị khi incident.
- Incident hiếm khi reproduce được: bug production thường phụ thuộc traffic pattern, data cụ thể, timing. Recording sẵn = không cần reproduce.
- Bounded resource: maxsize chặn disk usage, không lo log phình.
Lưu ý vận hành: đặt filename vào volume còn chỗ trống; với multi-instance, gắn PID/hostname vào tên file dump; tự động hoá dump trong alert handler để khỏi thao tác tay lúc 3h sáng.
Q4Đọc flame graph: "plateau rộng trên đỉnh" và "tower cao" nói lên điều gì khác nhau?▸
Flame graph: trục X = tỷ lệ sample (thời gian), trục Y = độ sâu stack. Mỗi khối là 1 frame; khối con nằm trên khối cha (callee trên caller).
Plateau rộng trên đỉnh (khối rộng, không có khối con phía trên): method tự nó đốt CPU — sample dừng ở đó nghĩa là CPU đang chạy chính body method này (loop tính toán, regex, copy array). Đây là ứng viên optimize số 1: thu hẹp plateau = giảm CPU trực tiếp.
Tower cao (chuỗi khối chồng cao nhưng hẹp): call chain sâu — A gọi B gọi C... 50 tầng. Bản thân không tốn CPU nhiều (hẹp), nhưng gợi ý về kiến trúc: abstraction chồng tầng, recursive call, framework overhead. Nếu tower vừa cao vừa rộng → mỗi tầng cộng dồn cost (vd serialization lồng nhau) — xem có short-circuit được không.
Pattern khác đáng nhớ:
- Nhiều plateau rộng rải rác: không có hot spot đơn lẻ — optimize 1 chỗ không cứu được, cần nhìn tổng thể (algorithm, data structure).
- Plateau ở GC thread /
__memcpynative: vấn đề không phải code logic mà allocation pressure — chuyển sang allocation profile (-e alloc).
Sai lầm phổ biến: nhìn flame graph CPU khi app chậm vì I/O wait — thread chờ socket không ăn CPU nên gần như vô hình trong CPU profile. Dùng wall-clock mode (-e wall) hoặc JFR I/O events cho case đó.
Q5Vì sao async-profiler sample được cả native stack và không bị "safepoint bias" như profiler thường?▸
Profiler sampling truyền thống (VisualVM, JMX-based) lấy stack qua API Thread.getStackTrace — API này yêu cầu thread phải dừng tại safepoint (bài 11) để stack ổn định. Hệ quả gọi là safepoint bias: sample chỉ rơi vào các vị trí có safepoint poll (method return, loop back-edge), không bao giờ rơi vào giữa đoạn code nóng — hot loop được JIT bỏ safepoint poll trở nên "vô hình", profile méo.
async-profiler tránh bằng 2 cơ chế:
AsyncGetCallTrace: internal API của HotSpot cho phép lấy Java stack ngay trong signal handler, tại bất kỳ điểm nào thread đang chạy — không cần chờ safepoint. Sample trúng cả giữa hot loop.- perf_events (Linux): kernel gửi signal định kỳ theo CPU cycles; signal handler ghép native stack (từ kernel) + Java stack (từ AsyncGetCallTrace) → thấy được JNI, lib native, thậm chí kernel frame.
JFR method sampling cũng dùng cơ chế safepoint-free tương tự cho Java frame, nhưng không ghép native stack đầy đủ như async-profiler — đây là lý do app nặng JNI/native (crypto, DB driver, compression) nên deep-dive bằng async-profiler.
Trade-off: AsyncGetCallTrace là internal API không có spec chính thức — hiếm khi nhưng có thể fail trên 1 số frame (báo [unknown_Java]). Chấp nhận được cho profiling.
Bài tiếp theo: Object Header và Compressed OOP — tại sao mỗi object Java tốn ít nhất 16 byte
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên