Metaspace, stack và native memory — đọc chữ ký OOM từng vùng
Ngoài heap, JVM còn metaspace, thread stack và native memory. Mỗi vùng cạn theo một chữ ký OOM riêng — nhìn dòng lỗi là biết vùng nào hỏng và fix ở đâu.
TL;DR: Heap không phải toàn bộ bộ nhớ JVM. Class metadata sống trong metaspace (native memory, thay PermGen từ Java 8); frame của mỗi lời gọi method sống trong thread stack (mỗi thread một stack riêng, mặc định ~1 MB); buffer I/O và JIT code sống trong native memory ngoài heap. Điều đáng giá nhất khi debug production: mỗi vùng cạn theo một chữ ký lỗi riêng. OutOfMemoryError: Java heap space là heap; : Metaspace là class metadata phình; dòng chứa direct buffer memory là direct buffer; unable to create native thread là hết chỗ cho thread stack; còn StackOverflowError (không phải OOM) là một stack đơn lẻ tràn vì đệ quy sâu. Nhìn đúng dòng chữ ký là biết ngay vùng nào hỏng và fix ở đâu.
Bài 05 đã mổ heap: young/old, bump-the-pointer. Nhưng khi production ném OutOfMemoryError, tăng -Xmx chưa chắc cứu được — vì có thể vùng cạn không phải heap. Một app hot-reload tới lần thứ ~50 chết vì metaspace vượt 512 MB; một app Netty rò direct buffer chết dù heap còn trống; một app tạo ~4000 thread chết vì native cạn. Cả ba đều là "OutOfMemoryError" nhưng ba nguyên nhân, ba cách fix khác nhau.
Bài này dạy bạn đọc chữ ký OOM: mỗi vùng ngoài heap là gì, hỏng vì sao, và dòng lỗi nào tố cáo nó.
1. Analogy — toà nhà văn phòng nhiều loại không gian
Heap là nhà kho (bài 05). Nhưng một công ty còn nhiều loại không gian khác, mỗi loại đầy theo cách riêng:
| Không gian văn phòng | Vùng JVM | "Đầy" nghĩa là |
|---|---|---|
| Thư viện bản thiết kế | Metaspace (class metadata) | Quá nhiều bản thiết kế (class) |
| Bàn làm việc mỗi nhân viên | Thread stack (frame method) | Một người xếp giấy chồng quá cao (đệ quy sâu) |
| Sân ngoài, kho thuê ngoài | Native / direct memory | Hết đất trống ngoài kho chính |
Nhân viên (thread) ai cũng có bàn riêng; đồ đang làm (frame) xếp trên bàn. Bản thiết kế (class metadata) để trong thư viện dùng chung. Hàng quá khổ hoặc thuê ngoài để ngoài sân (native). Khi một loại không gian đầy, biển báo lỗi khác nhau — và đó chính là chữ ký OOM.
flowchart TB
subgraph JVM[Bo nho JVM process]
H["Heap (-Xmx)<br/>object: young + old"]
M["Metaspace (native)<br/>class metadata"]
S["Thread stacks<br/>1 stack / thread, ~1 MB"]
N["Native / direct<br/>DirectByteBuffer, code cache, JNI"]
end
H -.->|"OOM: Java heap space"| H
M -.->|"OOM: Metaspace"| M
S -.->|"StackOverflowError"| S
N -.->|"OOM: Direct buffer / native thread"| NNgoài heap (object) còn ba vùng: metaspace (class metadata), stack (frame method, mỗi thread một cái), native (buffer I/O, JIT code). Mỗi vùng có một dòng lỗi riêng — nhớ dòng lỗi là chẩn được vùng.
2. Metaspace — vì sao Java 8 thay PermGen?
Metaspace chứa class metadata: cấu trúc Klass (method table, runtime constant pool, field layout), bytecode của method đã load. Mỗi class load tốn vài tới chục kilobyte; app điển hình 5000 class → khoảng 25–50 MB metaspace.
Trước Java 8, vùng này là PermGen (Permanent Generation) — nằm trong heap, kích thước cố định qua -XX:MaxPermSize. Ba vấn đề khiến nó bị thay:
- Kích thước cố định khó tune: app load class động (Spring CGLIB proxy, Hibernate entity proxy, Mockito mock) khó dự đoán cần bao nhiêu. Đặt thấp → OOM; đặt cao → phí RAM.
- Nằm trong heap: GC class metadata trộn chung chu kỳ với object, class-loader unload kém.
OutOfMemoryError: PermGen spacethành lỗi kinh điển khi container hot redeploy.
Java 8 (JEP 122) chuyển sang Metaspace trong native memory, mặc định không giới hạn — tự grow theo nhu cầu (chặn bởi RAM máy). Class-loader unload tốt hơn: loader unreachable thì JVM free toàn bộ metadata của loader đó.
java -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m MyApp
-XX:MetaspaceSize: mốc high-water ban đầu (mặc định ~21 MB) — vượt thì trigger một lần GC dọn class.-XX:MaxMetaspaceSize: trần cứng. Vượt →OutOfMemoryError: Metaspace.
Metaspace mặc định unbounded nghĩa là class-loader leak sẽ ăn hết RAM máy rồi bị kernel OOM kill (im lặng, không stack trace Java). Production nên đặt -XX:MaxMetaspaceSize để fail nhanh bằng OutOfMemoryError: Metaspace (có stack trace, debug được) thay vì để kernel giết âm thầm.
3. Thread stack — frame method và StackOverflowError
Mỗi thread có một stack riêng, mặc định khoảng 1 MB (Linux x64; tuỳ nền tảng, chỉnh bằng -Xss). Stack chứa một frame cho mỗi lời gọi method đang chạy — frame giữ local variable, operand stack, return address. Method return thì frame pop; toàn bộ tự dọn, không cần GC.
Đệ quy quá sâu chồng frame vượt dung lượng stack:
void recurse() {
recurse(); // khong co diem dung
}
Method recurse() trên gọi chính nó vô hạn. Nó sẽ ném lỗi gì? Đó có phải một OutOfMemoryError không, và vì sao? Viết ra câu trả lời trước khi đọc tiếp.
Nó ném StackOverflowError — một stack đơn lẻ tràn, không phải toàn bộ bộ nhớ JVM cạn (nên không phải OOM). Với stack 1 MB và mỗi frame cỡ trăm byte, giới hạn rơi vào khoảng chục nghìn mức đệ quy; method nhiều local + sâu thì ít mức hơn.
Cách fix theo thứ tự đúng:
- Sửa thuật toán: chuyển đệ quy sâu thành vòng lặp với explicit stack (ví dụ DFS trên cây đường kính lớn). Đây là fix gốc.
- Tăng
-Xsschỉ khi đệ quy hợp lệ mà cần sâu hơn — trade-off: 1000 thread × 2 MB stack = 2 GB native memory chỉ cho stack.
Tăng -Xss để "chữa" đệ quy vô hạn là vô nghĩa — chỉ đẩy điểm tràn xa hơn chút rồi vẫn tràn.
4. Native / direct memory — ngoài heap, ngoài -Xmx
Heap và metaspace chưa phải hết. Native memory (off-heap) chứa nhiều thứ không tính vào -Xmx:
- Direct ByteBuffer:
ByteBuffer.allocateDirect(n)cấp phát quamallocngoài heap, cho I/O hiệu năng cao (NIOFileChannel, Netty) — tránh một lần copy staging qua heap (GC không di chuyển buffer native), kernel đọc/ghi thẳng vào buffer. Bộ nhớ native của buffer được Cleaner (cơ chế phantom-reference) tự giải phóng khi buffer bị GC, nhưng chạy trễ nên buffer tồn dư một lúc. Trần:-XX:MaxDirectMemorySize(mặc định xấp xỉ-Xmx). Vượt →OutOfMemoryError: Cannot reserve N bytes of direct buffer memory (...)— dấu nhận là cụmdirect buffer memory(Java 8 in gọnDirect buffer memory; từ JDK 11 là câu đầy đủ này). - JIT code cache: native code do JIT biên dịch. Trần
-XX:ReservedCodeCacheSize(mặc định 240 MB). Đầy → JIT ngừng compile, app rớt về interpreter (chậm hẳn) — theo dõi bằngjcmd <pid> VM.codecache_info. - Thread stacks: 1000 thread × 1 MB = 1 GB native. Hết chỗ tạo thread mới →
OutOfMemoryError: unable to create native thread: possibly out of memory or process/resource limits reached(thường do vượt OS limitulimit -uhoặc native memory cạn). - JNI (Java Native Interface) / thư viện native: code C/C++ tự
malloc, JVM không track — leak ở đây không hiện trong heap dump.
Một app Netty xử lý nhiều kết nối chết với dòng: java.lang.OutOfMemoryError: Cannot reserve 16777216 bytes of direct buffer memory (allocated: ..., limit: ...). Vùng nào cạn? Tăng -Xmx có cứu được không, và vì sao? Tự trả lời trước khi đọc mục 5.
5. Bảng chữ ký OOM — nhìn dòng lỗi là biết vùng
Đây là phần cốt lõi. Trước khi xem bảng đầy đủ, với mỗi dòng lỗi dưới tự điền hai thứ: (1) vùng nào cạn, (2) tăng -Xmx có cứu được không — rồi so với đáp án:
...: Java heap space→ vùng nào?...: Metaspace→ vùng nào?...: Direct buffer memory→ vùng nào?...: unable to create native thread→ vùng nào?StackOverflowError→ vùng nào?
Đáp án và nguyên nhân gốc:
| Chữ ký (dòng lỗi) | Vùng cạn | Nguyên nhân gốc | Fix hướng tới |
|---|---|---|---|
OutOfMemoryError: Java heap space | Heap | Leak (static map không bound), heap quá nhỏ, hoặc object quá lớn | Heap dump → MAT tìm leak; hoặc tăng -Xmx |
OutOfMemoryError: Metaspace | Metaspace (native) | Class-loader leak (hot reload, redeploy), sinh class động vô hạn | jcmd VM.classloader_stats; restart thay hot reload; đặt MaxMetaspaceSize |
OOM: Cannot reserve N ... direct buffer memory (...) | Native (direct) | Nhiều DirectByteBuffer không release (Netty, gRPC), Cleaner chậm | Tăng MaxDirectMemorySize; audit release buffer |
OOM: unable to create native thread: ... | Native (thread stack) | Vượt OS limit ulimit -u, hoặc native cạn, thread leak | ulimit -u cao hơn; audit pool/executor không shutdown |
StackOverflowError | Một thread stack | Đệ quy quá sâu (thường không có điểm dừng) | Chuyển sang vòng lặp; -Xss chỉ khi đệ quy hợp lệ |
GC overhead limit exceeded vẫn là heap: GC tốn vượt 98% thời gian mà reclaim chưa tới 2% heap → JVM bỏ cuộc sớm (họ hàng "Java heap space"). Kernel OOM Kill (Linux) KHÔNG phải Java exception — không stack trace; dmesg hiện "killed process (java)". Xảy ra khi tổng heap + metaspace + native + stack vượt cgroup limit (Linux control group — trần RAM cấp cho container) của container.
6. Ngân sách bộ nhớ trong container
Vì native memory không tính vào -Xmx, đặt -Xmx bằng đúng RAM container là công thức bị kernel OOM kill:
Container 4 GB:
Heap (-Xmx) 2.5 GB
Metaspace (max) 256 MB
Direct memory (max) 256 MB
Code cache 240 MB
Thread stacks (200) 200 MB
Native/JNI/internal ~300 MB
--------------------------------
Tong ~3.7 GB -> chua 300 MB dem cho OS
Quy tắc: -Xmx chỉ nên chiếm khoảng 50–70% RAM container, chừa phần còn lại cho non-heap. Đặt -Xmx4g cho container 4 GB thì heap phình đẩy native vượt limit → kernel giết JVM im lặng.
7. Pitfall tổng hợp
❌ Nhầm 1: Gặp bất kỳ OutOfMemoryError nào cũng tăng -Xmx.
✅ Đọc chữ ký trước: Metaspace/Direct buffer memory/unable to create native thread đều KHÔNG cứu được bằng -Xmx — có khi tăng -Xmx còn làm native tệ hơn (ép kernel OOM).
❌ Nhầm 2: Hot reload liên tục (Spring DevTools, Tomcat redeploy) mà không restart.
✅ Mỗi lần reload để lại một ClassLoader cũ giữ class metadata → OutOfMemoryError: Metaspace. Restart đủ chu kỳ, hoặc đặt MaxMetaspaceSize để fail nhanh khi leak.
❌ Nhầm 3: catch (OutOfMemoryError e) { retry(); }.
✅ OOM nghĩa vùng không reclaim được — retry trong cùng JVM cũng OOM. Fix root cause hoặc restart, đừng nuốt lỗi.
8. 📚 Deep Dive Oracle
Spec / reference chính thức:
- JVMS §2.5 Run-Time Data Areas — spec định nghĩa heap, method area (metaspace hiện thực nó), Java stack.
- JEP 122 — Remove the Permanent Generation — vì sao Java 8 bỏ PermGen thay bằng Metaspace.
- HotSpot Native Memory Tracking (NMT) — bật
-XX:NativeMemoryTracking=summary+jcmd VM.native_memoryđể thấy từng vùng native tiêu bao nhiêu. javatool —-Xss,-XX:MaxDirectMemorySize,-XX:ReservedCodeCacheSize— mô tả từng flag và mặc định.
Ghi chú: Khi nghi native memory (không phải heap) là thủ phạm, Native Memory Tracking là công cụ chuẩn — nó chia rõ heap / metaspace / thread / code cache / direct, thứ mà heap dump không thấy.
9. Liên hệ các bài khác
- Bài 05 — Heap generational — vùng heap và
OutOfMemoryError: Java heap space; bài này lo phần ngoài heap và các chữ ký OOM còn lại. - Bài 09 — Object header và Compressed OOP — klass pointer trong object header trỏ tới đúng
Klassmetadata nằm trong metaspace mô tả ở đây. - Bài 07 — CLI tools (jcmd/jstat) —
jcmdchẩn từng vùng:VM.classloader_stats,VM.native_memory,VM.codecache_infokhi đọc chữ ký OOM. - Bài 13 — Mini-challenge: debug memory leak — vận dụng: dựng heap dump, đọc chữ ký, truy leak.
10. Tóm tắt
- Ngoài heap (object), JVM còn metaspace (class metadata, native), thread stack (frame method, mỗi thread một cái), native/direct (buffer I/O, JIT code, JNI).
- Metaspace thay PermGen từ Java 8 (JEP 122): native memory, mặc định không giới hạn; class-loader leak (hot reload) làm nó phình →
OutOfMemoryError: Metaspace. - Thread stack ~1 MB/thread (Linux x64); đệ quy quá sâu tràn một stack →
StackOverflowError(không phải OOM). Fix gốc là chuyển sang vòng lặp, không phải tăng-Xss. - Native/direct không tính vào
-Xmx: DirectByteBuffer (trầnMaxDirectMemorySize≈-Xmx), code cache (240 MB), thread stacks, JNI. - Chữ ký OOM chỉ đích vùng:
Java heap space(heap),Metaspace,direct buffer memory(direct),unable to create native thread(native/stack). Đọc đúng dòng là biết fix ở đâu — tăng-Xmxkhông phải luôn đúng. - Container:
-Xmxchỉ nên 50–70% RAM, chừa cho non-heap, tránh kernel OOM kill im lặng.
11. Tự kiểm tra
Q1Metaspace khác PermGen thế nào, và vì sao Java 8 thay?▸
PermGen (Java 7 trở xuống) nằm trong heap, kích thước cố định (-XX:MaxPermSize). Ba vấn đề: khó tune cho app sinh class động (Spring/Hibernate/Mockito); GC class metadata trộn chung chu kỳ heap, unload kém; lỗi OutOfMemoryError: PermGen space phổ biến khi hot redeploy.
Metaspace (Java 8+, JEP 122) nằm trong native memory, mặc định không giới hạn, tự grow, class-loader unload tốt hơn. Caveat: unbounded nghĩa là leak có thể ăn hết RAM rồi bị kernel OOM kill im lặng — production nên đặt -XX:MaxMetaspaceSize để fail nhanh bằng OutOfMemoryError: Metaspace có stack trace.
Q2Đọc từng dòng lỗi sau, gọi tên vùng cạn và nêu một nguyên nhân gốc: (a) Metaspace (b) unable to create native thread (c) dòng chứa direct buffer memory.▸
Metaspace (b) unable to create native thread (c) dòng chứa direct buffer memory.- (a) Metaspace (native) — class metadata phình. Gốc: class-loader leak (hot reload, redeploy giữ ClassLoader cũ), hoặc sinh class động vô hạn. Fix:
jcmd VM.classloader_stats, restart thay hot reload, đặtMaxMetaspaceSize. - (b) Native memory / thread stack — hết chỗ tạo thread mới. Gốc: vượt OS limit
ulimit -u, native cạn (mỗi thread ~1 MB stack), hoặc thread leak (pool/executor không shutdown). Fix: nângulimit -u, audit thread leak. - (c) Native (direct buffer) — DirectByteBuffer vượt trần. Gốc: nhiều buffer không release (Netty, gRPC), Cleaner chậm. Fix: tăng
MaxDirectMemorySize, auditrelease()buffer.
Điểm chung: cả ba đều không cứu được bằng tăng -Xmx — chúng nằm ngoài heap.
Q3Vì sao StackOverflowError không phải một OutOfMemoryError, và vì sao tăng -Xss hiếm khi là fix đúng?▸
StackOverflowError không phải một OutOfMemoryError, và vì sao tăng -Xss hiếm khi là fix đúng?StackOverflowError là một thread stack đơn lẻ tràn (frame chồng vượt ~1 MB), không phải toàn bộ bộ nhớ JVM cạn — nên nó thuộc họ Error khác, không phải OOM. Nguyên nhân gần như luôn là đệ quy quá sâu (thường thiếu điểm dừng).
Tăng -Xss chỉ dời điểm tràn xa hơn chút: đệ quy vô hạn vẫn tràn, còn đệ quy hợp lệ nhưng rất sâu thì nên chuyển thành vòng lặp với explicit stack (fix gốc). Ngoài ra -Xss lớn nhân với số thread tốn native memory nhanh (1000 thread × 2 MB = 2 GB).
Q4Vì sao đặt -Xmx bằng đúng RAM container (ví dụ -Xmx4g cho container 4 GB) lại gây kernel OOM kill?▸
-Xmx bằng đúng RAM container (ví dụ -Xmx4g cho container 4 GB) lại gây kernel OOM kill?Vì -Xmx chỉ giới hạn heap, không tính các vùng non-heap: metaspace, thread stacks, direct buffer, JIT code cache, JNI. Tổng bộ nhớ JVM = heap + tất cả những vùng đó.
Nếu heap được phép chiếm trọn 4 GB, thì metaspace/native/stack đẩy tổng vượt cgroup limit của container → kernel giết process (SIGKILL) im lặng, không Java exception, không stack trace (chỉ dmesg ghi lại). Vì vậy quy tắc là -Xmx ≈ 50–70% RAM container, chừa phần còn lại cho non-heap.
Q5Direct buffer (off-heap) khác byte array trên heap ở điểm nào, và khi nào nên dùng?▸
Heap ByteBuffer là byte[] trên heap, JVM quản qua GC. Direct ByteBuffer cấp phát qua malloc ngoài heap, JVM giữ reference + Cleaner để free. Direct nhanh hơn cho I/O vì tránh một lần copy staging qua heap (kernel đọc/ghi thẳng vào địa chỉ buffer) và địa chỉ ổn định (GC không di chuyển như object trên heap).
Nên dùng direct cho I/O lớn và buffer sống lâu, tái dùng nhiều lần (Netty pool buffer); tránh cho buffer nhỏ ngắn hạn vì malloc/free chậm hơn bump-pointer heap và free phụ thuộc Cleaner. Luôn đặt -XX:MaxDirectMemorySize để tránh leak âm thầm.
Bài tiếp theo: Garbage Collection — reachability và ba thuật toán nền
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên