JVM thread ↔ OS thread
Tiếp đi: thread của Java và thread của Linux. ps chỉ thấy đúng một process java, nhưng top -H lại hiện cả trăm dòng "java". Một new Thread() trong code thì Linux thấy cái gì?
Bài 3.1 đã nói một process java có nhiều thread. Bài này trả lời: mỗi thread đó là gì trong kernel, và dùng điều đó để chẩn đoán.
1. Bài toán
ps -o pid,nlwp,cmd -p $(pgrep -f order/app.jar)
PID NLWP CMD
4123 214 java -Xmx1g -jar /opt/order/app.jar
214 thread. Sau một đợt tăng traffic, log báo:
java.lang.OutOfMemoryError: unable to create native thread: possibly out of memory or process/resource limits reached
Heap vẫn còn trống. Vậy "memory" nào bị hết?
2. Vấn đề
- Lỗi này không liên quan heap. Nó xảy ra khi JVM xin kernel tạo một OS thread mới và bị từ chối.
- Nguyên nhân thường: chạm giới hạn số task (
ulimit -u,pids.maxcủa cgroup,threads-max) hoặc không đủ bộ nhớ ảo cho stack.
3. Giải pháp: mô hình 1:1
- Từ Java 1.3 trên Linux, mỗi platform thread (
new Thread(), thread trong pool) = đúng một OS thread. JVM gọipthread_create→ kernelclone(). - Trong kernel, thread và process đều là task. Thread của cùng process chia sẻ bộ nhớ, fd; mỗi thread có TID riêng (còn gọi LWP). TID của thread đầu tiên = PID.
- Scheduler của Linux quyết định thread nào chạy (bài 10.3), không phải JVM.
- Mỗi thread có stack riêng, kích thước tối đa do
-Xss(mặc định 1MB trên Linux x64). Đây là vùng reserve; RAM thật chỉ cấp cho phần stack đã dùng (bài 10.2). - Trong thread dump,
nid=0x...chính là TID của Linux, viết ở hệ 16.
4. Code từng bước
Bước 1: Thấy thread trong /proc
PID=$(pgrep -f order/app.jar)
ls /proc/$PID/task | wc -l # 214
cat /proc/$PID/task/*/comm | sort | uniq -c | sort -rn | head
200 http-nio-8080-e ← tên thread bị cắt còn 15 ký tự
4 GC Thread#0..3
2 C2 CompilerThre
1 VM Thread
1 HikariPool-1 ho
JVM đặt tên OS thread theo tên Java thread, nên đặt tên pool rõ ràng giúp chẩn đoán từ phía Linux:
@Bean
ThreadPoolTaskExecutor invoiceExecutor() {
var ex = new ThreadPoolTaskExecutor();
ex.setThreadNamePrefix("invoice-"); // ngắn, hiện đủ trong top -H
ex.setCorePoolSize(4);
ex.setMaxPoolSize(8);
ex.setQueueCapacity(500);
return ex;
}
Bước 2: Nối OS thread với Java thread
top -H -p $PID -n 1 | head -12
PID USER %CPU COMMAND
4188 order 97.0 invoice-3
printf '%x\n' 4188 # 105c
jcmd $PID Thread.print | grep -A12 'nid=0x105c'
"invoice-3" #61 prio=5 os_prio=0 tid=0x00007f... nid=0x105c runnable
java.lang.Thread.State: RUNNABLE
at java.util.regex.Pattern$Loop.match(...)
Quy trình đầy đủ khi CPU 100% (nhiều dump, flame graph) ở 2.6.
Bước 3: Kiểm tra giới hạn tạo thread
grep -E 'Max processes' /proc/$PID/limits # ulimit -u (đếm theo user, gồm cả thread)
cat /proc/sys/kernel/threads-max
cat /proc/sys/kernel/pid_max
systemctl show order-service -p TasksMax # systemd giới hạn task qua cgroup pids
cat /sys/fs/cgroup/system.slice/order-service.service/pids.current
[Service]
TasksMax=4096
Bước 4: Chỉnh stack size khi cần
-Xss512k # cho mọi thread
new Thread(null, task, "deep-parser", 4 * 1024 * 1024).start(); // riêng thread cần stack sâu
Giảm -Xss không phải cách chính. Cách chính là giới hạn pool, hoặc dùng virtual thread cho tác vụ chờ I/O.
5. Cách hoạt động
Platform thread (1:1)
Java: Thread "http-nio-8080-exec-1" Thread "invoice-3" Thread "GC Thread#0"
│ │ │
JVM: pthread_create ──────────────── clone() ────────────────┘
▼ ▼ ▼
Kernel: task TID 4130 task TID 4188 task TID 4125
(stack 1MB reserve) (nid=0x105c)
└──────── cùng PID 4123: chung heap, chung fd ────────┘
Linux scheduler chọn task nào lên core
Virtual thread (M:N)
VT#1 VT#2 VT#3 ... VT#10000 (object trên heap, stack lưu ở heap khi park)
\ | / |
mount/unmount
carrier "ForkJoinPool-1-worker-1..N" (N ≈ số core, là platform thread)
▼
Kernel: chỉ thấy N task
6. So sánh
| Platform thread | Virtual thread | |
|---|---|---|
| Linux thấy | Một task mỗi thread | Chỉ thấy carrier thread |
Hiện trong top -H | Có, đúng tên | Không; CPU tính cho ForkJoinPool-1-worker-N |
| Stack | Reserve theo -Xss, ngoài heap | Lưu trên heap, co giãn |
| Giới hạn số lượng | ulimit -u, pids.max, RAM | Chủ yếu là heap |
| Thread dump | jstack/jcmd Thread.print | jcmd PID Thread.dump_to_file -format=json file.json |
| Ai schedule | Kernel | JVM (lên carrier), rồi kernel (carrier lên core) |
7. Khi nào nên dùng / không nên dùng
- Đặt tên pool ngắn, rõ: tìm được ngay trong
top -H. - Theo dõi số thread (
jvm.threads.live) vàpids.current; thread tăng mãi là rò rỉ thread. - Virtual thread: hợp với tác vụ chờ I/O. Lưu ý
top -Hkhông chỉ ra request nào, phải dùng thread dump JSON hoặc JFR.
Vậy Java thread priority (setPriority(10)) có làm Linux cho thread đó chạy nhiều hơn không?
Bạn đoán hợp lý, nhưng trên Linux câu trả lời là gần như không.
- Mặc định HotSpot trên Linux bỏ qua Java priority: mọi thread có cùng
os_prio=0(bạn thấy trong thread dump). - Có cờ
-XX:ThreadPriorityPolicy=1để map sang nice, nhưng hạ nice (ưu tiên cao hơn) cần quyền root, nên thường không dùng. - Muốn việc nền nhường CPU: tách nó ra process riêng và dùng
nice/CPUWeight(bài 10.3), hoặc giới hạn số thread của pool đó.
Không nên dựa vào thread priority để đảm bảo đúng logic hay hiệu năng.
Với virtual thread, nếu top -H thấy ForkJoinPool-1-worker-2 ăn 100% CPU thì làm sao biết request nào gây ra?
Đúng là khó hơn, vì carrier chỉ là "xe chở". Virtual thread nào đang ngồi trên carrier thì thay đổi liên tục.
jcmd $PID Thread.dump_to_file -format=json /tmp/vt.json
# tìm virtual thread đang RUNNABLE, xem stack của nó
jq '.threadDump.threadContainers[].threads[] | select(.state=="RUNNABLE") | .stack[0:5]' /tmp/vt.json
- Lấy vài dump cách nhau vài giây; stack nào lặp lại ở trạng thái RUNNABLE là thủ phạm.
- Tốt hơn: JFR hoặc async-profiler cho flame graph theo method, không phụ thuộc thread nào.
- Nếu carrier bị chiếm lâu bởi code CPU-bound, các virtual thread khác phải chờ. Việc CPU nặng nên chạy trên pool platform thread riêng (bàn ở 3.11).
Phỏng vấn hỏi Java thread và OS thread quan hệ thế nào thì trả lời sao?
Q1. Java thread map với OS thread thế nào?
🇻🇳 Tiếng Việt:
Platform thread là 1:1: mỗi Java thread là một OS thread, kernel schedule, có TID riêng và stack riêng theo -Xss. Virtual thread là M:N: nhiều virtual thread chạy trên một số ít carrier thread, kernel chỉ thấy carrier.
🇬🇧 English:
Platform threads are one-to-one: each Java thread is an OS thread with its own TID and an -Xss sized stack, scheduled by the kernel. Virtual threads are many-to-many: many virtual threads run on a small number of carrier threads, and the kernel only sees the carriers.
Q2. Tìm thread Java đang ăn CPU trên Linux thế nào?
🇻🇳 Tiếng Việt:
Em dùng top -H -p PID để thấy TID tốn CPU, đổi sang hex, rồi tìm nid đó trong jcmd Thread.print. nid chính là TID của Linux. Đặt tên thread pool rõ ràng giúp nhận ra ngay từ top -H.
🇬🇧 English:
I run top -H -p PID to find the TID using CPU, convert it to hex and search for that nid in jcmd Thread.print. The nid is the Linux TID. Clear thread pool names make it recognizable directly in top -H.
Q3. "unable to create native thread" nghĩa là gì?
🇻🇳 Tiếng Việt:
JVM xin kernel tạo OS thread mới và bị từ chối, không phải hết heap. Em kiểm tra số thread hiện tại, ulimit -u, pids.max hoặc TasksMax của cgroup, và bộ nhớ cho stack. Thường gốc rễ là pool không giới hạn hoặc rò rỉ thread, nên sửa code trước rồi mới nâng limit.
🇬🇧 English:
The JVM asked the kernel for a new OS thread and was refused; it is not about the heap. I check the current thread count, ulimit -u, the cgroup pids.max or TasksMax, and memory for stacks. Usually the root cause is an unbounded pool or a thread leak, so I fix the code before raising limits.
Cách nhớ ngắn gọn
- Platform thread = 1 task Linux. nid (hex) = TID.
top -H→printf %x→jcmd Thread.print.- -Xss = stack mỗi thread, nằm ngoài heap.
- Virtual thread: kernel chỉ thấy carrier; dùng dump JSON/JFR.
Tự kiểm tra
Thread dump ghi nid=0x105c. Trong top -H tìm số nào?
0x105c = 4188, là TID trong top -H.
Ở đâu trong /proc thấy các thread của process?
/proc/PID/task/, mỗi thư mục con là một TID.
"unable to create native thread" khi heap còn trống, kiểm tra gì?
Số thread, ulimit -u, pids.max/TasksMax, bộ nhớ cho stack.
10.000 virtual thread thì ls /proc/PID/task | wc -l tăng 10.000?
Không. Kernel chỉ thấy carrier thread (khoảng số core) cộng các thread khác của JVM.
setPriority(10) có tác dụng trên Linux mặc định không?
Không, HotSpot bỏ qua priority trên Linux mặc định (os_prio=0).