Virtual memory: RSS, VSZ, swap và OOM killer
Tiếp đi: bộ nhớ của process. order-service chạy -Xmx1g, nhưng top báo cột VIRT 5.8g, RES 1.6g. Vậy rốt cuộc app dùng bao nhiêu RAM? Và sao lại vượt cả -Xmx?
Hai con số đó đo hai thứ khác nhau. Muốn đọc đúng, cần hiểu virtual memory: mỗi process tưởng mình có một vùng nhớ riêng rất lớn, còn RAM thật chỉ được cấp khi thật sự dùng.
1. Bài toán
top -p $(pgrep -f order/app.jar)
PID USER VIRT RES SHR S %CPU %MEM COMMAND
4123 order 5.8g 1.6g 28m S 12 20.5 java
VM có 8GB. Ops hỏi: "app khai 1GB heap mà ăn 1.6GB, còn 5.8g là gì? Có leak không?"
2. Vấn đề
- Nhìn VIRT thì tưởng app ăn 5.8GB → hoảng, đặt limit sai.
- Tưởng -Xmx là tổng RAM của JVM → đặt RAM cho VM/container quá sát → bị OOM killer giết.
3. Giải pháp: hiểu virtual vs physical
- Virtual memory: mỗi process có không gian địa chỉ riêng (64-bit: cực lớn). Địa chỉ mà Java/C dùng đều là địa chỉ ảo.
- Page: bộ nhớ chia thành trang 4KB. Bảng page table map trang ảo → trang vật lý (RAM).
- Page fault: process chạm vào trang ảo chưa có RAM → CPU báo kernel → kernel cấp một trang RAM rồi cho chạy tiếp. Minor fault: chỉ cấp RAM, rẻ. Major fault: phải đọc từ disk (file hoặc swap), đắt.
- VSZ/VIRT: tổng vùng địa chỉ ảo đã reserve. Không phải RAM.
- RSS/RES: số trang đang thật sự nằm trong RAM. Đây mới là con số cần theo dõi.
JVM reserve sẵn vùng ảo lớn (heap tối đa, code cache, metaspace, stack từng thread…) nên VIRT cao là bình thường.
Vì sao RSS > -Xmx? -Xmx chỉ giới hạn heap. JVM còn nhiều vùng ngoài heap (đã giới thiệu ở 2.2):
RSS của java ≈ heap đang dùng (≤ Xmx)
+ metaspace (class)
+ code cache (JIT)
+ thread stacks (≈ số thread × phần stack đã chạm)
+ GC structures
+ direct buffers (Netty, NIO)
+ malloc của thư viện native, libc arenas
4. Code từng bước
Bước 1: Đọc số liệu từ Linux
PID=$(pgrep -f order/app.jar)
grep -E 'VmSize|VmRSS|VmSwap|Threads' /proc/$PID/status
VmSize: 6081232 kB ← VSZ
VmRSS: 1677340 kB ← RSS
VmSwap: 0 kB
Threads: 214
ps -o pid,vsz,rss,maj_flt,min_flt -p $PID # số page fault
Bước 2: Bật Native Memory Tracking để chia RSS theo vùng JVM
ExecStart=/usr/bin/java -Xmx1g -XX:NativeMemoryTracking=summary -jar /opt/order/app.jar
sudo -u order jcmd $PID VM.native_memory summary
- Java Heap (reserved=1048576KB, committed=1048576KB)
- Class (reserved=1056768KB, committed= 98304KB)
- Thread (reserved= 220000KB, committed= 28000KB)
- Code (reserved= 251904KB, committed= 60000KB)
- GC (reserved= 90000KB, committed= 80000KB)
- Internal / Other ... (direct buffers)
Bước 3: Giới hạn các vùng ngoài heap
-Xmx1g
-XX:MaxMetaspaceSize=256m
-XX:ReservedCodeCacheSize=128m
-XX:MaxDirectMemorySize=256m
-Xss512k # nếu stack sâu không cần 1MB, xem bài 10.5
Quy tắc nhẩm: RAM cấp cho app ≈ Xmx × 1.3–1.5 + số thread × vài trăm KB.
Bước 4: Kiểm tra swap và OOM killer
free -h
swapon --show
cat /proc/sys/vm/swappiness
sudo dmesg -T | grep -iE 'killed process|out of memory'
[Mon Oct 13 03:12:44 2026] Out of memory: Killed process 4123 (java)
total-vm:6081232kB, anon-rss:7012340kB, ... oom_score_adj:0
journalctl -k | grep -i oom
5. Cách hoạt động
Process java (địa chỉ ảo) page table RAM vật lý
┌──────────────────────┐
│ heap 0x7f00.. (1GB) │──── trang đã dùng ──────► [frame 812]
│ │──── trang chưa chạm ──► (chưa có, chỉ reserve)
│ metaspace │──────────────────────► [frame 77]
│ thread stack #1..#214│──── vài trang đầu ─────► [frame ...]
│ libjvm.so (file map) │──── shared ────────────► [page cache]
└──────────────────────┘
chạm trang chưa có → page fault → kernel cấp frame
VSZ = tổng ô bên trái RSS = số mũi tên đang trỏ vào RAM
RAM cạn:
1. đẩy page cache (file) ra
2. swap trang anonymous ra disk (nếu có swap)
3. vẫn không đủ → OOM killer chọn process có oom_score cao nhất → SIGKILL
6. So sánh
| Chỉ số | Ý nghĩa | Dùng để |
|---|---|---|
| VSZ / VIRT | Vùng địa chỉ ảo đã reserve | Gần như không dùng để đánh giá RAM |
| RSS / RES | Trang đang ở trong RAM (gồm cả trang share) | Theo dõi RAM thực của process |
| SHR | Phần RSS share với process khác (thư viện .so) | Hiểu vì sao cộng RSS các process > RAM |
| -Xmx | Giới hạn heap Java | Chỉ một phần của RSS |
Page cache (buff/cache) | File đã đọc, kernel giữ lại | Có thể giải phóng; "free thấp" chưa chắc là thiếu RAM, xem cột available |
7. Khi nào nên dùng / không nên dùng
- Theo dõi RSS (và
availablecủafree), không theo dõi VIRT. - Bật NMT khi RSS tăng mà heap ổn: chi phí khoảng 5–10%, bật tạm khi điều tra.
- Không đặt RAM của VM = Xmx. Chừa 30–50% cho native và OS.
- Trên server chạy JVM: tắt swap hoặc
swappinessthấp (1–10).
Swap là thêm RAM miễn phí mà, sao người ta hay khuyên tắt swap trên server chạy Java?
Bạn hiểu gần đúng: swap giúp process không chết, nhưng với JVM nó thường biến "chết nhanh" thành "chậm kéo dài", khó phát hiện hơn.
- Swap chậm hơn RAM hàng nghìn lần. Trang bị swap ra, lúc dùng lại là major fault.
- GC đi qua toàn bộ heap. Nếu một phần heap nằm trong swap, mỗi lần GC phải kéo hết về → GC pause từ 50ms thành vài giây.
- Health check vẫn trả 200 nhưng p99 latency tăng vọt. Load balancer không gỡ instance ra.
vmstat 1
r b swpd free si so ...
2 3 812000 40000 950 1200 ← si/so khác 0 liên tục = đang swap
Khuyến nghị: tắt swap (swapoff -a, xóa khỏi /etc/fstab) hoặc vm.swappiness=1, và cấp đủ RAM. Kubernetes mặc định cũng chạy node không swap vì lý do tương tự. Thà OOM rồi restart sạch còn hơn chậm âm thầm.
OOM killer chọn giết ai? Lần trước VM mình có cả Postgres và order-service, nó giết order-service. Có cách nào để nó giết process khác không?
Đúng, OOM killer có cách chọn rõ ràng. Mỗi process có oom_score, chủ yếu tỉ lệ với RAM nó dùng, cộng thêm oom_score_adj (−1000 đến 1000). Process điểm cao nhất bị SIGKILL.
cat /proc/$(pgrep -f order/app.jar)/oom_score
cat /proc/$(pgrep -x postgres | head -1)/oom_score
JVM thường RSS lớn nhất nên hay "trúng". Có thể chỉnh trong unit file:
[Service]
OOMScoreAdjust=-500 # ít bị chọn hơn (−1000 = không bao giờ)
Nhưng đó chỉ là chọn ai chết. Cách đúng:
- Tách DB và app ra máy/container khác nhau.
- Giới hạn mỗi service bằng cgroups (
MemoryMax=trong systemd, hoặc limit của container, bài 10.6) để một service quá tay chỉ tự chết, không kéo theo cả máy. - Phân biệt:
OutOfMemoryErrorlà JVM tự báo hết heap, có stack trace (2.5). OOM killer là kernel giết bằng SIGKILL, log app không có gì, phải xemdmesg, exit code 137.
Phỏng vấn hỏi virtual memory và RAM của JVM thì trả lời sao?
Q1. RSS và VSZ khác nhau thế nào?
🇻🇳 Tiếng Việt:
VSZ là tổng vùng địa chỉ ảo mà process đã reserve, phần lớn chưa có RAM thật. RSS là số trang đang nằm trong RAM vật lý. Để đánh giá RAM thực của process thì em xem RSS; VSZ của JVM cao là bình thường.
🇬🇧 English:
VSZ is the total virtual address space a process has reserved, most of which has no physical memory behind it. RSS is the number of pages actually resident in RAM. To judge real memory use I look at RSS; a high VSZ for a JVM is normal.
Q2. Vì sao RSS của JVM lớn hơn -Xmx?
🇻🇳 Tiếng Việt:
-Xmx chỉ giới hạn heap. JVM còn metaspace, code cache, thread stack, cấu trúc của GC, direct buffer và malloc của native library. Em dùng Native Memory Tracking để xem từng vùng và cấp RAM khoảng 1.3 đến 1.5 lần heap.
🇬🇧 English:
-Xmx only limits the heap. The JVM also uses metaspace, the code cache, thread stacks, GC data structures, direct buffers and native library allocations. I use Native Memory Tracking to break it down and give the process about 1.3 to 1.5 times the heap size.
Q3. OOM killer là gì, khác OutOfMemoryError thế nào?
🇻🇳 Tiếng Việt:
OOM killer là cơ chế của kernel: khi RAM cạn, nó chọn process có oom_score cao nhất và gửi SIGKILL. Log app không có gì, exit code 137, phải xem dmesg. OutOfMemoryError là JVM tự ném khi heap hoặc metaspace đầy, có stack trace và có thể tạo heap dump.
🇬🇧 English:
The OOM killer is a kernel mechanism: when memory runs out it picks the process with the highest oom_score and sends SIGKILL. The application log shows nothing, the exit code is 137, and you need dmesg to see it. OutOfMemoryError is thrown by the JVM itself when the heap or metaspace is full, with a stack trace and an optional heap dump.
Q4. Vì sao swap không tốt cho JVM?
🇻🇳 Tiếng Việt:
GC phải đi qua heap, nếu trang heap bị swap ra disk thì mỗi lần GC gây major page fault, pause từ mili giây thành giây. App vẫn sống nhưng chậm, khó phát hiện. Em tắt swap hoặc để swappiness thấp và cấp đủ RAM.
🇬🇧 English:
The GC walks the heap, so if heap pages are swapped out every collection triggers major page faults and pauses grow from milliseconds to seconds. The app stays alive but slow, which is hard to detect. I disable swap or use a low swappiness and size memory properly.
Cách nhớ ngắn gọn
- VSZ = đã hứa, RSS = đang dùng thật.
- RSS JVM = heap + metaspace + code cache + stacks + direct + native.
- Swap + GC = pause dài.
- OOM killer: không log, exit 137, xem
dmesg.
Tự kiểm tra
Page fault là gì? Minor và major khác gì?
Process chạm trang ảo chưa có trong RAM. Minor: kernel chỉ cấp frame. Major: phải đọc từ disk/swap, chậm.
VIRT 5.8g có phải app dùng 5.8GB RAM?
Không. Đó là vùng ảo reserve. Xem RSS.
Công cụ nào chia RSS của JVM theo vùng?
Native Memory Tracking: -XX:NativeMemoryTracking=summary + jcmd PID VM.native_memory summary.
Dấu hiệu đang swap trong vmstat?
Cột si/so khác 0 liên tục.
App chết, log không có lỗi, exit 137. Kiểm tra gì đầu tiên?
dmesg -T | grep -i killed để xem OOM killer.