Công cụ jstack trong JVM: Phân tích và gỡ lỗi

jstack là công cụ chẩn đoán được tích hợp trong JDK, dùng để in ra thông tin stack trace của tất cả các thread (Thread Dump) trong một tiến trình Java. Đây là lệnh cốt lõi để gỡ lỗi các vấn đề như CPU tăng đột biến, deadlock, thread bị chặn, phản hồi chậm.

Cú pháp cơ bản

jstack [tùy chọn] <pid>
jstack [tùy chọn] <thực thi> <core>
jstack [tùy chọn] [id_server@]<địa chỉ IP hoặc hostname của server từ xa>

Cách dùng phổ biến nhất là: jstack <pid> Trong đó <pid> là ID tiến trình Java thu được từ jps hoặc ps.

Các tùy chọn chi tiết

1. -l (chữ l thường) — Hiển thị thông tin khóa bổ sung (khuyến nghị mạnh)

jstack -l <pid>
  • Ngoài stack thread, còn hiển thị:
  • Thông tin về việc giữ và chờ khóa đồng bộ (monitor)
  • Chi tiết khóa của java.util.concurrent (như ReentrantLock, khóa nội bộ của ThreadPoolExecutor)
  • Trong output sẽ xuất hiện:
- parking to wait for  <0x000000076b8c3450> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)

Mục đích: Xác định chính xác deadlock, nguyên nhân chờ thread.

Khuyến nghị: Nên luôn dùng -l trừ khi hiệu năng cực kỳ nhạy cảm.

2. -F (Force) — In ép buộc (khi tiến trình không phản hồi)

jstack -F <pid>
  • Dùng khi JVM mục tiêu bị treo, không phản hồi yêu cầu Attach
  • Bypass giao tiếp socket thông thường, sử dụng cơ chế ptrace ở cấp hệ điều hành để đọc bộ nhớ
  • Điều kiện tiên quyết:
  • Người dùng hiện tại có quyền ptrace (trên Linux thường cần root hoặc cùng user)
  • Hệ thống không vô hiệu hóa ptrace (như Kubernetes cho phép mặc định, nhưng một số chính sách bảo mật sẽ hạn chế)

⚠️ Lưu ý:

  • Trong chế độ -F sẽ không xuất thông tin khóa của -l
  • Có thể thất bại do quyền hạn hoặc hạn chế kernel (đặc biệt trong môi trường container, gVisor, Kata)

Kịch bản điển hình:

# jstack bình thường thất bại
jstack 12345
# Lỗi: Unable to open socket file...

# Thử ép buộc
jstack -F 12345

3. -mChế độ hỗn hợp (in native + Java stack) [đã lỗi thời/không khuyến nghị]

jstack -m <pid>
  • Cố gắng in đồng thời stack Java + native (C/C++)
  • Nhưng trong JDK hiện đại (JDK 8+) gần như vô dụng vì:
  • HotSpot không giữ lại toàn bộ symbol native stack
  • Cần JVM debug hoặc tùy chọn biên dịch đặc biệt
  • Native frame thường hiển thị dưới dạng [0x00007f...] không có ý nghĩa thực tế

Kết luận: Không dùng -m, thay vào đó dùng perf + async-profiler để phân tích native stack.

4. -h / -help — Hiển thị trợ giúp

jstack -help

In ra hướng dẫn sử dụng ngắn gọn.

Các tổ hợp không được hỗ trợ

Tổ hợpKết quả-F -lVô hiệu. Chế độ -F không hỗ trợ thông tin khóa-F -mVô hiệu. -F không hỗ trợ native stack-l -m️ Có thể thực thi, nhưng -m không có tác dụng thực tế

Cách dùng đúng chỉ có hai:

  1. jstack -l <pid> → Trường hợp bình thường, có thông tin khóa
  2. jstack -F <pid> → Khi tiến trình không phản hồi, ép buộc (không có thông tin khóa)

Phân tích định dạng output (các trường quan trọng)

Một đoạn thread dump điển hình:

"Worker-1" #8 prio=5 os_prio=0 tid=0x00007f8a1c000000 nid=0x303e runnable [0x00007f8a0dffe000]
   java.lang.Thread.State: RUNNABLE
	at com.example.Service.process(Service.java:42)
	at com.example.Service.lambda$execute$0(Service.java:28)
	- locked <0x000000076b8c3450> (a java.lang.Object)
	- waiting to lock <0x000000076b8c3460> (a java.lang.Object) owned by "Worker-2" #9

TrườngÝ nghĩa"Worker-1"Tên thread (có thể đặt bằng Thread.setName())#8ID thread nội bộ JVMprio=5Mức ưu tiên Java (1~10)os_prio=0Mức ưu tiên hệ điều hànhtid=0x...Địa chỉ thread trong JVMnid=0x303eNative Thread ID (thập lục phân) → Dùng để đối chiếu với top -H``runnableTrạng thái thread (runnable, waiting, timed_waiting, blocked)java.lang.Thread.StateTrạng thái thread ở cấp Java- locked ...Khóa đang giữ- waiting to lock ... owned by "Worker-2"Khóa đang chờ và người giữ → Dấu hiệu deadlock!

Mẹo sử dụng thực chiến

1. Tìm thread tiêu thụ CPU cao (quy trình kinh điển)

# Bước 1: Tìm PID Java
jps -l

# Bước 2: Xem CPU từng thread
top -H -p <PID>

# Bước 3: Tìm ID thread CPU cao (thập phân), chuyển sang thập lục phân
printf "%x\n" <thread_id>  # Ví dụ 12350 → 303e

# Bước 4: Lấy stack thread
jstack -l <PID> > threaddump.txt

# Bước 5: Tìm "nid=0x303e" trong dump
grep -A 30 "nid=0x303e" threaddump.txt

2. Phát hiện deadlock

jstack -l <PID>

Nếu có deadlock, đầu output sẽ chỉ rõ:

Found one Java-level deadlock:
=============================
"Worker-2":
  waiting to lock monitor 0x00007f8a1c000000 (object 0x000000076b8c3450, a java.lang.Object),
  which is held by "Worker-1"
"Worker-1":
  waiting to lock monitor 0x00007f8a1c000001 (object 0x000000076b8c3460, a java.lang.Object),
  which is held by "Worker-2"

3. Giải pháp thay thế trong container (khi jstack thất bại)

# Giải pháp 1: Dùng kill -3 (đáng tin cậy nhất!)
kill -3 <PID>
# Stack thread sẽ xuất ra stdout, trong container có thể xem qua kubectl logs

# Giải pháp 2: Dùng jcmd (hiện đại hơn)
jcmd <PID> Thread.print -l

kill -3 không phụ thuộc vào cơ chế Attach, 100% hiệu quả!

4. Lấy liên tục (phân tích vấn đề không ổn định)

# Lấy mỗi 5 giây, tổng 3 lần
for i in {1..3}; do
  jstack -l <PID> >> /tmp/threaddump.$(date +%s).txt
  sleep 5
done

So sánh nhiều dump, có thể phát hiện thread luôn ở trạng thái RUNNABLE (có thể là vòng lặp vô hạn).

⚠️ ### Lỗi phổ biến và cách khắc phục

Lỗi 1:

Unable to open socket file: target process not responding or HotSpot VM not loaded
  • Nguyên nhân: Cơ chế Attach thất bại (/tmp không ghi được, Alpine image, tiến trình treo)
  • Giải pháp:
  • Dùng jstack -F
  • Hoặc dùng kill -3 <PID>
  • Hoặc dùng jcmd <PID> Thread.print

Lỗi 2:

Not enough space for virtual memory
  • Nguyên nhân: Container thiếu bộ nhớ, không thể cấp phát buffer dump
  • Giải pháp: Mở rộng bộ nhớ container trước, hoặc dùng trực tiếp kill -3

Lỗi 3: Không có output hoặc output không đầy đủ

  • Nguyên nhân: Khi redirect không dùng -l, hoặc tiến trình thoát quá nhanh
  • Giải pháp: Đảm bảo tiến trình còn sống, dùng jstack -l > file

jstack vs jcmd Thread.print

Đặc tínhjstack``jcmd <pid> Thread.printCần file AttachCóCóHỗ trợ -l thông tin khóaCóCần thêm -lCó được khuyến nghị chính thức️ Công cụ cũJDK 7+ khuyến nghịMở rộng chức năngCố địnhCó thể quản lý cùng các lệnh jcmd khácKhả năng tương thích containerKémTốt hơn

Khuyến nghị: Dự án mới ưu tiên dùng jcmd, nhưng jstack vẫn tương thích rộng.

Tóm tắt best practices

  1. Mặc định thêm -l: jstack -l <pid> để lấy thông tin khóa đầy đủ
  2. Khi CPU cao, kết hợp top -H + nid để định vị
  3. Trong container ưu tiên kill -3 <pid> để tránh vấn đề Attach
  4. Kiểm tra deadlock bằng "Found one Java-level deadlock" ở đầu dump
  5. Không dùng -m, phân tích native stack dùng async-profiler

Lệnh tham khảo nhanh

# Thread dump cơ bản (có khóa)
jstack -l 12345 > threaddump.log

# Dump ép buộc (tiến trình không phản hồi)
jstack -F 12345 > threaddump_force.log

# Giải pháp hiện đại (khuyến nghị)
jcmd 12345 Thread.print -l > threaddump_jcmd.log

# Đáng tin cậy nhất (thân thiện container)
kill -3 12345  # Log trong stdout

Nắm vững jstack, bạn sẽ có "máy X-quang" để nhìn thấu hành vi của thread Java!

Thẻ: JVM jstack thread dump debugging deadlock

Đăng vào ngày 17 tháng 05 lúc 19:48