99 Ngày Java — Ngày 75: Profiling & công cụ
Bốn ngày qua ta học bộ nhớ, GC, nạp lớp và JIT. Tất cả đều kết thúc ở một lời khuyên: đo, đừng đoán. Hôm nay là bộ công cụ để đo. Tin tốt: mọi thứ cần thiết đã có sẵn trong JDK 21 (jcmd, jfr, JFR, JDK Mission Control tải riêng) và VisualVM miễn phí. Ta dùng một chương trình cố tình có ba bệnh để tập trả lời ba câu hỏi: CPU đang đi đâu, bộ nhớ đang bị ai giữ, và các luồng đang chờ cái gì.

1. Chương trình bệnh nhân
import java.util.*;
import java.util.concurrent.*;
// Chương trình có ba "bệnh" để tập chẩn đoán: tốn CPU vô ích, rò rỉ bộ nhớ, và luồng chờ nhau.
public class ProfilingDemo {
static final Map<String, byte[]> CACHE = new HashMap<>(); // bệnh 2: chỉ thêm, không bao giờ bớt
static final Object LOCK = new Object();
static String slowKey(int i) { // bệnh 1: nối chuỗi trong vòng lặp
String key = "";
for (int k = 0; k < 200; k++) key += (char) ('a' + (i + k) % 26);
return key;
}
public static void main(String[] args) throws Exception {
ExecutorService pool = Executors.newFixedThreadPool(4, r -> new Thread(r, "worker"));
for (int i = 0; ; i++) {
final int n = i;
pool.submit(() -> {
String key = slowKey(n);
synchronized (LOCK) { // bệnh 3: khóa quá rộng, 4 luồng xếp hàng
CACHE.put(key, new byte[16 * 1024]);
try { Thread.sleep(2); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
});
Thread.sleep(1);
}
}
}
// Chạy: java -Xmx256m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./oom.hprof ProfilingDemo.javaChạy nó vài phút: CPU của bốn worker cao, heap tăng đều rồi OOM, và throughput thấp hơn nhiều so với bốn luồng đáng ra làm được. Ba triệu chứng, ba công cụ.
2. Java Flight Recorder: hộp đen chạy được trên production
# 1. Bật JFR trên tiến trình ĐANG CHẠY (không cần khởi động lại) — chi phí ~1–2%
jcmd <pid> JFR.start name=demo settings=profile duration=60s filename=demo.jfr
# hoặc ngay từ lúc khởi động:
java -XX:StartFlightRecording=settings=profile,duration=60s,filename=demo.jfr ProfilingDemo.java
# 2. Xem nhanh không cần GUI (Java 21 có 'jfr view' với ~70 view dựng sẵn)
jfr summary demo.jfr
jfr view hot-methods demo.jfr # phương thức chiếm nhiều mẫu CPU nhất
jfr view allocation-by-class demo.jfr # lớp nào cấp phát nhiều nhất
jfr view contention-by-site demo.jfr # khóa nào bị tranh chấp, ở đâu
jfr view gc demo.jfr # từng lần GC: loại, pause, heap trước/sau
# 3. Đào sâu bằng GUI: JDK Mission Control (jmc) mở demo.jfr — flame graph, timeline, allocationJFR được thiết kế cho production: lấy mẫu stack định kỳ thay vì đo từng lời gọi, nên chi phí thường dưới 2% và không làm méo kết quả như profiler instrumentation. Bật bằng
jcmdtrên tiến trình đang chạy, không cần restart.jfr view hot-methodsvớiProfilingDemosẽ chỉ thẳngslowKeyvàString.concat/StringBuilder: nối chuỗi trong vòng lặp tạo 200 chuỗi trung gian mỗi lần.allocation-by-classxác nhận bằng số byte cấp phát.contention-by-sitecho biếtLOCKở dòng nào bị tranh chấp và luồng chờ tổng bao lâu: đó là bệnh thứ ba, nhìn thấy mà không cần đọc thread dump.Bản ghi
settings=profilechi tiết hơndefault(mẫu dày hơn, ngưỡng thấp hơn), vẫn an toàn cho vài phút trên production. Ghi liên tục vớimaxage=1hvàjcmd <pid> JFR.dumpkhi sự cố xảy ra cho bạn một giờ lịch sử trước lúc có chuyện.JDK Mission Control mở file
.jfrvới flame graph, timeline GC, phân bổ theo lớp, và trang “Automated Analysis” tự gợi ý vấn đề. Dùng khijfr viewchưa đủ.
3. Heap dump: ai đang giữ bộ nhớ
# Chụp heap của tiến trình đang chạy (chỉ object còn sống → đã ép một full GC, ứng dụng dừng vài giây)
jcmd <pid> GC.heap_dump ./live.hprof
# Hoặc tự động khi OOM — đặt sẵn trên MỌI service production, không tốn gì cho tới khi cần:
java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/dumps/ App
# Thống kê nhanh không cần dump: lớp nào chiếm nhiều bộ nhớ nhất
jcmd <pid> GC.class_histogram | head -15
# num #instances #bytes class name
# 1: 41230 675545600 [B ← byte[] của CACHE
# 2: 41230 1319360 java.lang.String
# Đọc dump: Eclipse MAT → Leak Suspects · Dominator Tree · Histogram
# Retained size (giữ bao nhiêu nếu nó biến mất) mới là số cần nhìn, không phải shallow size.Bắt đầu rẻ nhất bằng
GC.class_histogram: nếu top 3 làbyte[],Stringvà một lớp của bạn, câu hỏi tiếp theo là ai giữ chúng. Histogram không trả lời được; heap dump thì có.Trong Eclipse MAT, Dominator Tree sắp object theo retained size:
CACHEsẽ đứng đầu vì xóa nó là giải phóng gần như toàn bộ heap. Leak Suspects tự tìm đúng kết luận đó trong vài giây. Từ object, chọn “Path to GC Roots” để thấy chuỗi tham chiếu: static fieldProfilingDemo.CACHE→HashMap→ mảng entry →byte[].Dump chỉ object sống ép một full GC và dừng ứng dụng vài giây với heap vài GB; file dump lớn bằng heap. Trên production, ưu tiên
-XX:+HeapDumpOnOutOfMemoryErrorđặt sẵn: không tốn gì cho tới khi cần, và khi OOM xảy ra bạn có bằng chứng thay vì phải tái hiện.Heap dump chứa dữ liệu thật (mật khẩu trong
String, token, dữ liệu khách hàng). Xử lý như dữ liệu nhạy cảm: không đính vào ticket công khai, xóa sau khi phân tích.
4. Thread dump: các luồng đang chờ gì
# Thread dump: mọi luồng đang làm gì NGAY LÚC NÀY
jcmd <pid> Thread.print > dump1.txt
# ...đợi 10 giây, chụp lại 2 lần nữa: luồng nào đứng yên qua cả 3 lần mới là vấn đề
"worker" #23 prio=5 ... waiting for monitor entry [0x...]
java.lang.Thread.State: BLOCKED (on object monitor)
at ProfilingDemo.lambda$main$0(ProfilingDemo.java:20)
- waiting to lock <0x000000070a0b1c20> (a java.lang.Object) ← 3 luồng chờ cùng một LOCK
"worker" #22 ...
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(...)
- locked <0x000000070a0b1c20> (a java.lang.Object) ← luồng đang giữ khóa
# Java 21: Thread.print KHÔNG liệt kê virtual thread — dùng dump dạng JSON:
jcmd <pid> Thread.dump_to_file -format=json ./threads.json
# Deadlock: JVM tự phát hiện, cuối dump có "Found one Java-level deadlock"Một dump là ảnh chụp; ba dump cách nhau 10 giây mới là bằng chứng. Luồng
BLOCKEDở cùng một dòng qua cả ba lần là nút thắt; luồngRUNNABLEở cùng một stack là vòng lặp nóng hoặc I/O chậm đang chờ trong native.Đọc từ dưới lên: dòng
locked <0x...>cho biết ai giữ khóa, dòngwaiting to lock <0x...>cho biết ai chờ. Cùng một địa chỉ xuất hiện ở nhiều luồng là dấu hiệu khóa quá rộng, nhưLOCKbao cảThread.sleeptrong ví dụ.Với deadlock, JVM đã làm việc phân tích: cuối dump có
Found one Java-level deadlockkèm vòng chờ. Ngày 66 đã tạo deadlock chủ ý; nay bạn biết bằng chứng trông thế nào.Java 21 với virtual thread:
Thread.printchỉ liệt kê platform thread. DùngThread.dump_to_file -format=jsonđể thấy cả virtual thread, gom theo executor; đây là công cụ khi hàng nghìn virtual thread cùng chờ một tài nguyên.
5. VisualVM và checklist chọn công cụ
Triệu chứng → Công cụ đầu tiên
CPU cao, đáp ứng chậm → JFR hot-methods / flame graph trong JMC
Bộ nhớ tăng dần, OOM → GC log (Ngày 72) → GC.class_histogram → heap dump + MAT
Treo, throughput thấp mà CPU thấp → 3 thread dump cách 10s; tìm BLOCKED / WAITING lặp lại
GC pause dài → jfr view gc · -Xlog:gc*
Chậm lúc khởi động → JFR từ lúc start · -Xlog:class+load (Ngày 73)
Muốn nhìn trực tiếp trên laptop → VisualVM: Monitor · Sampler (CPU / Memory) · ThreadsVisualVM (visualvm.github.io) tự thấy JVM cục bộ; tab Monitor cho heap/CPU/thread theo thời gian thực, tab Sampler lấy mẫu CPU và bộ nhớ với chi phí thấp, tab Threads là thread dump có màu. Đủ cho laptop; trên server hãy dùng JFR rồi tải file về.
Sampler (lấy mẫu định kỳ) và Profiler (instrument từng phương thức) khác nhau ở chi phí và độ méo: instrument làm phương thức nhỏ trông đắt hơn thật vì thêm mã đo vào chính nó. Mặc định dùng sampler; chỉ instrument khi cần đếm chính xác số lần gọi.
jstat -gcutil <pid> 1000in tỷ lệ đầy của từng vùng heap mỗi giây: cách nhìn nhanh nhất để biết old gen có đang tăng không, trước khi mở bất kỳ file nào.Thứ tự luôn là: triệu chứng → giả thuyết → công cụ đo đúng giả thuyết → sửa → đo lại. Mở profiler mà không có câu hỏi cụ thể là cách chắc nhất để tối ưu nhầm chỗ.
6. Tự kiểm tra
Chạy
ProfilingDemovới JFR 60 giây; dùngjfr view hot-methodsvàcontention-by-siteđể chỉ ra hai bệnh đầu trước khi đọc lại mã nguồn.Để chương trình OOM với
HeapDumpOnOutOfMemoryError; mở dump trong MAT, chạy Leak Suspects và tìm Path to GC Roots của mảngbyte[]lớn nhất.Chụp ba thread dump; đếm số luồng
BLOCKEDtrênLOCKmỗi lần. Thu hẹp khóa (đưasleepra ngoàisynchronized) và chụp lại.Sửa
slowKeydùngStringBuildervà đặt giới hạn choCACHE; ghi lại JFR mới và so sánh hot-methods cùng allocation trước/sau.
Tóm lại: JFR là công cụ đầu tiên cho CPU, cấp phát và tranh chấp khóa, chạy được trên production; heap dump với MAT trả lời “ai giữ bộ nhớ” bằng retained size và đường tới GC root; ba thread dump trả lời “luồng chờ gì”; VisualVM đủ cho máy dev; và mỗi phiên đo bắt đầu bằng một câu hỏi. Ngày 76 quay lại heap với một lớp công cụ tinh tế hơn: Soft, Weak và Phantom reference, và cách viết cache không thành rò rỉ như CACHE hôm nay.
Tài liệu đối chiếu
JFR và lệnh jfr (Java 21): https://docs.oracle.com/en/java/javase/21/jfapi/ ; jcmd: https://docs.oracle.com/en/java/javase/21/docs/specs/man/jcmd.html ; JDK Mission Control: https://www.oracle.com/java/technologies/jdk-mission-control.html ; Eclipse MAT: https://eclipse.dev/mat/ ; VisualVM: https://visualvm.github.io/
Site Admin
Engineer and writer. Building things with TypeScript and distributed systems.
Bình luận (0)
Bạn cần đăng nhập bằng Google để bình luận.
Hãy là người bình luận đầu tiên.


