Virtual memory: RSS, VSZ, swap và OOM killer

4 câu hỏi~25 phútTừ khóa: virtual memory, physical memory, page, page fault, RSS, VSZ, Native Memory Tracking, swap, swappiness, OOM killer, oom_score, dmesg

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 đề

3. Giải pháp: hiểu virtual vs physical

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ĩaDùng để
VSZ / VIRTVùng địa chỉ ảo đã reserveGần như không dùng để đánh giá RAM
RSS / RESTrang đang ở trong RAM (gồm cả trang share)Theo dõi RAM thực của process
SHRPhần RSS share với process khác (thư viện .so)Hiểu vì sao cộng RSS các process > RAM
-XmxGiới hạn heap JavaChỉ một phần của RSS
Page cache (buff/cache)File đã đọc, kernel giữ lạiCó 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

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.

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:

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


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.