Memory layout — heap, metaspace, stack, object header
Đối tượng Java sống ở đâu, header chiếm bao nhiêu byte, vì sao Integer 16 byte mà int 4 byte. Heap young/old generation, metaspace cho class metadata, stack cho frame method, native memory cho buffer ngoài heap. 4 loại OOM khác nhau.
Câu hỏi: 1 đối tượng Integer chiếm bao nhiêu byte trong JVM 64-bit?
Đoán phổ biến: 4 byte (vì int 4 byte). Sai. Đáp án: 16 byte trên HotSpot 64-bit với compressed oops bật. Padding gấp 4 lần data thực tế.
+--------+--------+----+----+
| header | klass | int | pad|
| 8 byte | 4 byte | 4 | 0 |
+--------+--------+----+----+
Total: 16 byte (already aligned 8)
Giải thích chi tiết bài này. Nhưng câu hỏi gợi ra: mọi object Java đều đắt hơn data nó chứa. Chương trình tạo 100M object Integer → 1.6GB heap, không phải 400MB. List Long 200M element → 4.8GB chỉ cho box. Đó là lý do Java có IntStream, int[], và Project Valhalla (value type) đang chuẩn bị thay đổi cuộc chơi.
Bài này map đầy đủ memory layout JVM:
- Heap: nơi object sống, chia young/old generation.
- Metaspace: class metadata (Java 8+ thay PermGen).
- Stack: frame method, local variable.
- Native memory: buffer ngoài heap (DirectByteBuffer, JNI).
- Object header: 12-16 byte mỗi object — overhead.
- 4 loại OOM: heap, metaspace, native, stack — mỗi cái có symptom + fix riêng.
1. Analogy — Toà nhà công ty nhiều tầng
Một toà nhà công ty:
- Heap = warehouse: kho rộng nhất, chứa "vật phẩm" (object). Chia 2 khu: young (vật phẩm mới, kiểm tra/dọn thường xuyên) và old (vật phẩm cũ, sống lâu, ít kiểm tra).
- Metaspace = thư viện kế hoạch: bản thiết kế (class metadata) — không thay đổi sau khi load, kích thước theo số bản thiết kế.
- Stack = bàn làm việc nhân viên: mỗi nhân viên (thread) 1 bàn riêng, để giấy tờ đang làm (frame method + local). Nhỏ, nhanh, tự dọn khi xong việc.
- Native memory = sân ngoài: vật phẩm quá lớn không vừa kho (file mmap, DirectByteBuffer cho I/O), hoặc vật phẩm thuê ngoài (JNI).
- Object header = nhãn dán mỗi vật phẩm: ID + loại + trạng thái lock + GC marker. Tốn ~12-16 byte mỗi vật phẩm.
| Đời thường | JVM |
|---|---|
| Warehouse (kho) | Heap |
| Khu mới / khu cũ | Young / Old generation |
| Thư viện kế hoạch | Metaspace |
| Bàn làm việc | Thread stack |
| Sân ngoài | Native memory |
| Nhãn dán | Object header |
| Vật phẩm | Object instance |
| Bản thiết kế | Class object trong metaspace |
4 region: Heap (object), Metaspace (class meta), Stack (frame method), Native (off-heap). Heap chia young/old. Mỗi object có header 12-16 byte. OOM mỗi region khác nhau, error message chỉ rõ region nào.
2. Heap — nơi object sống
Layout heap
flowchart LR
subgraph YoungGen[Young Generation]
E[Eden]
S0[Survivor S0]
S1[Survivor S1]
end
subgraph OldGen[Old Generation Tenured]
O[Old objects]
end
YoungGen --> OldGen- Young generation (~30% heap default): object mới alloc vào Eden. GC minor scan young thường xuyên, copy survivor giữa S0/S1.
- Old generation (~70% heap): object survive nhiều GC minor → promote sang old. GC major (full) scan old — chậm hơn nhiều.
Generational hypothesis (bài 5): đa số object chết trẻ. 90% object alloc xong dùng vài microsecond rồi không reference nữa. Tách young/old để scan nhanh — chỉ scan young thường xuyên.
Allocation
new Foo() → bump pointer trong Eden. Eden là contiguous memory:
[obj1][obj2][obj3]_______pointer_______________________
Pointer next-free advance theo size. Allocation O(1), không syscall, ~10ns. Lý do Java alloc nhanh ngang C++ malloc tuned.
Eden đầy → GC minor. Young objects còn live copy sang S0 (hoặc S1), Eden clear. Object survive nhiều round → promote old.
Tune heap size
java -Xms512m -Xmx4g MyApp
-Xms(initial size): JVM alloc lúc start.-Xmx(max size): giới hạn cứng, vượt → OOM.-XX:NewRatio=2(default): old/young ratio. 2 nghĩa old = 2 × young (young = 1/3 heap).-XX:SurvivorRatio=8(default): eden/survivor. Eden = 8 × survivor.
Production tip: set -Xms = -Xmx cho server long-running. Không cần resize, GC predictable hơn.
OutOfMemoryError: Java heap space
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.foo.Bar.<init>(Bar.java:42)
Heap đầy, GC không reclaim đủ. Nguyên nhân:
- Memory leak: reference lưu mãi trong static map / cache không bound (bài 7 mini-challenge).
- Heap quá nhỏ: workload thực sự cần nhiều — tăng
-Xmx. - Object lớn: vd cache 1GB byte array, heap 512MB — fail ngay.
Debug: -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heap.hprof — tự dump khi OOM. Mở trong Eclipse MAT (bài 6).
3. Object layout — anatomy 1 instance
HotSpot 64-bit layout
+----------+----------+----------+----------+
| Mark Word | 8 byte
+----------+----------+----------+----------+
| Klass Pointer | 4 byte (compressed) or 8 byte
+----------+----------+----------+----------+
| Field 1 |
| Field 2 |
| ... |
+----------+----------+----------+----------+
| Padding (align to 8 byte) |
+----------+----------+----------+----------+
Mark Word (8 byte): chứa state cho:
- Hash code (lazy compute lúc gọi
hashCode()). - GC age (số GC minor đã survive).
- Lock state: biased lock, lightweight lock, heavyweight lock, GC marker.
Cấu trúc bit packed phụ thuộc state:
unlocked: [hash:31][age:4][biased:1][tag:2][unused:24]
biased: [thread:54][epoch:2][age:4][biased:1][tag:2]
lightweight: [pointer to stack:62][tag:2]
heavyweight: [pointer to monitor:62][tag:2]
Klass Pointer: con trỏ đến Class object trong metaspace. Default compressed oops (Java 64-bit, heap < 32GB) → 4 byte. Heap > 32GB → 8 byte.
Field: theo thứ tự khai báo, group theo size (long → int → short → byte → reference) để minimize padding.
Padding: align object size về bội số 8 byte (alignment requirement HotSpot).
Tính size — Integer
class Integer {
private final int value;
// ... methods
}
Layout:
header (mark) : 8 byte
klass : 4 byte
value (int) : 4 byte
----------
16 byte (already aligned)
Integer = 16 byte. int thuần = 4 byte. Box overhead 4x.
Tính size — Long
header : 8 byte
klass : 4 byte
padding : 4 byte (align long to 8 byte boundary)
value (long) : 8 byte
----------
24 byte
Long = 24 byte. long thuần = 8 byte. Overhead 3x.
Tính size — String
Java 9+ (compact string):
class String {
private final byte[] value; // 4 byte ref
private final byte coder; // 1 byte
private int hash; // 4 byte
}
Layout:
header : 8 byte
klass : 4 byte
value (ref) : 4 byte
coder : 1 byte
padding : 3 byte (align next int)
hash : 4 byte
----------
24 byte (header + fields)
+ byte[] separately
String 10 char ASCII (Java 9+ compact):
- String header + fields: 24 byte.
- byte[] header + length: 16 byte.
- byte[] data 10 byte + padding 6 = 16 byte.
- Total: ~56 byte.
10 char trông ít — runtime overhead 5x.
Compressed oops
Reference (con trỏ object) trong heap default 4 byte (compressed) trên JVM 64-bit, heap < 32GB. JVM dùng "oop = pointer >> 3" — dịch 3 bit → đại diện được 32GB với 4 byte (4GB × 8 alignment).
Heap > 32GB → tự động tắt compressed oops → reference 8 byte. Mọi object lớn lên ~50%. Vì vậy chọn heap 31GB thường tốt hơn 33GB — 31GB compressed > 33GB uncompressed về effective memory.
java -XX:+PrintFlagsFinal -version | grep CompressedOops
# UseCompressedOops = true
4. Metaspace — class metadata
Trước Java 7: PermGen (Permanent Generation) — region trong heap cho class metadata. Fixed size, hay OOM khi load nhiều class.
Java 8+: Metaspace thay PermGen. Nằm ngoài heap, trong native memory. Default unbound — tự grow (giới hạn bởi RAM máy).
Chứa gì?
- Class metadata: cấu trúc Class object (method table, constant pool runtime, field info).
- Method bytecode + JIT compiled code (trong CodeCache, sub-region).
- Static field của class (trước Java 8 trong PermGen, Java 8+ trong heap nhưng metaspace giữ reference).
Mỗi class load ~kilobyte metadata. App typical 5000 class → ~50MB metaspace.
Tune
java -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m MyApp
-XX:MetaspaceSize: trigger GC khi vượt (initial high-water mark).-XX:MaxMetaspaceSize: giới hạn cứng. Vượt →OutOfMemoryError: Metaspace.
Production tip: set -XX:MaxMetaspaceSize để metaspace không phình vô hạn ăn RAM. Default unbound nguy hiểm với app dynamic gen class (Spring AOP proxy, Hibernate, Mockito).
OutOfMemoryError: Metaspace
Nguyên nhân:
- Class loader leak: classloader cũ không GC được vì static reference giữ → class metadata không free. Hot reload (Spring DevTools, Tomcat redeploy) hay gây.
- Dynamic class generation runtime: Spring
@Configurationtạo CGLIB proxy mỗi bean, Hibernate tạo entity proxy, Mockito tạo mock — mỗi cái 1 class metadata. - Maxsize quá thấp: app thực sự cần nhiều class.
Debug:
jcmd <pid> GC.class_stats
# Liet ke class load voi count + size
Hoặc heap dump → MAT class loader explorer.
5. Stack — frame method
Mỗi thread có 1 stack riêng. Default size: 1MB (Linux/macOS, JVM 64-bit).
java -Xss512k MyApp # Giảm stack size mỗi thread xuống 512KB
Stack chứa frame mỗi method invocation:
+---------------------+
| Frame method goi 3 | <- top
+---------------------+
| Frame method goi 2 |
+---------------------+
| Frame method goi 1 | <- main
+---------------------+
Frame chứa:
- Operand stack (bài 2) — temp value cho bytecode.
- Local variable array —
this, parameter, local. - Return address.
- Reference to constant pool.
Frame size compute compile time (max_stack + max_locals từ bytecode).
StackOverflowError
Recursion sâu vượt stack size:
void recurse() {
recurse();
}
recurse(); // StackOverflowError sau ~10000 lần (depend stack frame size)
Stack 1MB / frame 100 byte = 10000 level. Method có nhiều local + sâu → fewer level.
Fix:
- Giảm depth — tail recursion → iterate (Java không có tail call optimization).
- Tăng
-Xss2m(2MB stack mỗi thread). Trade-off: 1000 thread × 2MB = 2GB chỉ cho stack. - Refactor data structure để tránh recursion sâu (vd tree đường kính 100k → dùng iterative DFS với explicit stack).
Stack vs Heap
| Aspect | Stack | Heap |
|---|---|---|
| Alloc | Push frame, O(1), automatic | new, qua TLAB hoặc lock-free bump |
| Free | Pop frame khi method return | GC reclaim khi unreachable |
| Size | 1MB/thread default | Tens of GB |
| Concurrent | Mỗi thread riêng — no sync | Shared, cần sync khi alloc |
| Lifetime | Method invocation | Tới khi unreachable |
Object luôn trên heap (trừ escape analysis stack-allocate, mục bài 3). Local variable type primitive (int, long, ...) trên stack. Local variable type reference: reference trên stack, object pointed-to trên heap.
6. Native memory — ngoài heap
Heap không phải toàn bộ memory JVM. Native memory (off-heap) chứa:
Direct ByteBuffer
ByteBuffer buf = ByteBuffer.allocateDirect(1024 * 1024 * 100); // 100MB
allocateDirect cấp phát ngoài heap qua malloc. Dùng cho I/O (NIO FileChannel.read zero-copy), networking — kernel có thể access trực tiếp không qua heap copy.
Tradeoff:
- Alloc chậm hơn heap ~10x.
- Free phụ thuộc finalizer / Cleaner — không quyết định khi nào.
- Không đếm vào
-Xmx.
-XX:MaxDirectMemorySize=512m giới hạn — vượt → OutOfMemoryError: Direct buffer memory.
JNI / native library
Code C/C++ qua JNI alloc memory với malloc riêng. JVM không tracker. Leak C → leak native memory không show trong heap dump.
Thread stack
Đã nói mục 5. 1000 thread × 1MB = 1GB native memory chỉ cho stack.
Code cache (JIT compiled native code)
-XX:ReservedCodeCacheSize=240m # Default
Đầy → JIT ngừng compile, app chạy interpreter — chậm khủng khiếp. Theo dõi qua JFR event hoặc jcmd VM.codecache_info.
OutOfMemoryError: Direct buffer memory
App dùng nhiều DirectByteBuffer (Netty, Cassandra driver, gRPC) hay gặp. Fix: tăng -XX:MaxDirectMemorySize, hoặc audit code không leak buffer.
7. Tổng kết memory budget
JVM container Docker memory 4GB. Phân bổ thực tế:
Heap (-Xmx) : 2.5 GB
Metaspace (max) : 256 MB
Direct memory (max) : 256 MB
Code cache : 240 MB
Thread stack (200 thread) : 200 MB
Native (JNI, internal) : ~300 MB
----------
Total : ~3.7 GB
----------
Buffer for OS / overhead : 300 MB
Set -Xmx quá cao (vd 4GB cho container 4GB) → JVM allocate hết heap, mọi region khác đẩy native memory vượt → kernel OOM kill JVM (silent, no Java exception).
Rule: -Xmx ≤ 50-70% container memory. Giữ buffer cho non-heap.
8. Pitfall tổng hợp
❌ Nhầm 1: -Xmx = container memory.
docker run -m 4g java -Xmx4g MyApp # OOM Killed
✅ -Xmx 2.5g cho container 4GB.
❌ Nhầm 2: Static map cache không bound.
private static Map<String, Item> cache = new HashMap<>();
public static void put(String k, Item v) { cache.put(k, v); }
// Khong evict -> heap leak
✅ Dùng Caffeine hoặc LinkedHashMap LRU bound.
❌ Nhầm 3: Heap > 32GB.
java -Xmx48g MyApp # Compressed oops tat -> object lon them 50%
✅ Heap 31GB compressed > 33GB uncompressed về effective memory.
❌ Nhầm 4: Box collection.
List<Integer> nums = new ArrayList<>(); // Integer 16 byte, int 4 byte
for (int i = 0; i < 1_000_000; i++) nums.add(i);
// 16 MB chi cho box, 4 MB la data thuc
✅ int[] hoặc IntStream cho hot path.
❌ Nhầm 5: Tăng -Xss để tránh StackOverflowError.
-Xss10m # 10MB / thread, 1000 thread = 10GB
✅ Refactor recursion thành iterate. Tăng stack tốn nhiều, không giải quyết root cause.
❌ Nhầm 6: Catch OutOfMemoryError.
try { ... } catch (OutOfMemoryError e) { retry(); } // Vo nghia
✅ OOM nghĩa heap không reclaim được → retry trong cùng JVM cũng OOM. Restart hoặc fix root cause.
❌ Nhầm 7: Hot reload nhiều, không restart → Metaspace OOM.
Spring DevTools 100 reload -> 100 ClassLoader cu retain class metadata -> OOM Metaspace
✅ Restart full sau N reload, hoặc set -XX:MaxMetaspaceSize để fail fast khi leak.
9. 📚 Deep Dive Oracle
Spec / reference chính thức:
- JVMS §2.5 Run-Time Data Areas — heap, stack, method area spec.
- HotSpot Glossary - Heap — implementation chi tiết.
- JEP 122: Remove the Permanent Generation — Java 8 thay PermGen bằng Metaspace.
- JEP 254: Compact Strings — Java 9 String dùng byte[] thay char[] cho ASCII.
- JEP 142: Reduce Cache Contention on Specified Fields —
@Contendedannotation. - JOL (Java Object Layout) — tool inspect object layout exact byte-by-byte.
- Project Valhalla — value type / primitive class, loại bỏ box overhead.
Ghi chú: JOL là tool ưa thích để hiểu layout — chạy java -jar jol-cli.jar internals java.lang.Integer in ra layout chi tiết. JEP 254 (Compact Strings) là optimization quan trọng — String ASCII (90% trong realistic app) tốn 1 byte/char thay vì 2 byte/char. Project Valhalla khi stable sẽ thay đổi cuộc chơi: value class Point {...} sẽ inline vào caller, không header, không box — performance như C struct.
10. Tóm tắt
- 4 region memory JVM: heap (object), metaspace (class metadata, native), stack (frame method, per-thread), native (DirectByteBuffer, JNI, code cache).
- Heap chia young (Eden + 2 Survivor) + old. Object alloc Eden, GC promote sang old khi survive nhiều round.
- Allocation Eden = bump pointer O(1) ~10ns. Lý do Java alloc nhanh.
- Object header ~12-16 byte (Mark Word 8 byte + Klass pointer 4-8 byte). Ngoài header có field + padding align 8.
Integer= 16 byte (header 12 + int 4).Long= 24 byte. Box overhead 3-4x vs primitive.- Compressed oops (default heap < 32GB): reference 4 byte. Heap > 32GB tự tắt → reference 8 byte → object lớn ~50%.
- Metaspace thay PermGen (Java 8+). Native memory, default unbound. Nên set
-XX:MaxMetaspaceSizecho production. - Class loader leak (Spring DevTools hot reload, Tomcat redeploy không clean) gây metaspace bloat.
- Stack mỗi thread 1MB default. Recursion sâu →
StackOverflowError. Refactor iterative thay vì tăng-Xss. - Native memory: DirectByteBuffer ngoài heap (NIO, Netty), JNI, code cache (JIT compiled), thread stack. Không đếm
-Xmx. - 4 loại OOM:
Java heap space→ heap đầy, tăng-Xmxhoặc fix leak.Metaspace→ class metadata phình, fix loader leak.Direct buffer memory→MaxDirectMemorySizequá nhỏ hoặc leak buffer.unable to create new native thread→ vượt OS limit (ulimit -u) hoặc native memory cạn.
- Container Docker:
-Xmx≤ 50-70% container memory. Đủ buffer cho non-heap, tránh kernel OOM Kill silent. - Tool: JOL inspect object layout exact. HeapDumpOnOutOfMemoryError auto dump khi OOM.
- Tương lai: Project Valhalla (value type) loại box overhead —
Integervề 4 byte nhưint.
11. Tự kiểm tra
Q1Vì sao 1 đối tượng Integer chiếm 16 byte trong khi int chỉ 4 byte?▸
Integer chiếm 16 byte trong khi int chỉ 4 byte?Mọi object Java có overhead "header" mà primitive không có:
Integer layout (HotSpot 64-bit, compressed oops):
Mark Word : 8 byte (hash, GC age, lock state)
Klass Pointer : 4 byte (con tro Class object trong metaspace)
int value : 4 byte
: 16 byte total (already 8-byte aligned)Header (Mark + Klass) = 12 byte là "thuế" mọi object phải đóng:
- Mark Word: chứa hash code (lazy compute), GC age (số GC survive), lock state (biased / lightweight / heavyweight). Cần thiết cho GC và synchronization.
- Klass Pointer: con trỏ đến Class object — JVM dispatch method, type check, reflection cần.
Padding align về bội 8 byte — alignment requirement HotSpot. Total = 16 byte.
Hệ quả với collection:
List<Integer>1M element: 16 MB chỉ object Integer + 4 MB array reference + overhead List = ~20+ MB.int[]1M element: 16 byte header + 4M byte data = 4 MB.
Khác biệt 5x. Lý do hot path performance dùng primitive array hoặc IntStream, không box collection.
Tương lai: Project Valhalla (value type) cho phép Integer inline trực tiếp như primitive — không header, không box. Khi stable, code idiomatic Java sẽ nhanh ngang dùng primitive.
Q2Heap chia young/old để làm gì? Object di chuyển giữa 2 region thế nào?▸
Generational hypothesis: đa số object chết trẻ. Profile thực tế: 90-95% object alloc xong dùng vài microsecond rồi không reference nữa (vd local variable trong method, intermediate stream object).
Tách heap thành 2 generation:
- Young (~30% heap): nơi alloc. GC scan thường xuyên (vài giây 1 lần) — vì 90% object đã chết.
- Old (~70% heap): object survive nhiều GC young → "promote". GC scan ít thường xuyên (vài phút / giờ) — vì object già khả năng cao vẫn live.
Cơ chế di chuyển:
new Foo()→ alloc trong Eden (sub-region young).- Eden đầy → GC minor: scan young. Object live copy sang Survivor S0. Eden clear (1 swipe — tất cả object Eden chết).
- Lần GC sau: object live (kể cả S0 và Eden) copy sang S1. S0 + Eden clear.
- Lần GC kế tiếp: copy sang S0. S1 + Eden clear. (S0/S1 luân phiên — pattern "from-space / to-space" cho copying GC.)
- Object survive N round (default ~15) → promote sang Old. Hoặc nếu survivor đầy → promote sớm.
- Old đầy → GC major (full GC): scan toàn heap. Chậm hơn nhiều (hàng trăm ms cho heap GB).
Tại sao 2 Survivor (S0/S1) thay vì 1?
Copying GC cần "from-space" (đọc) và "to-space" (ghi) riêng để compact contiguous. 1 Survivor không đủ — cần luân phiên. Eden + 1 active Survivor = "from", Survivor còn lại = "to". Sau swap.
Hệ quả tune: nếu app tạo nhiều object short-lived → young size lớn (giảm GC frequency). Long-lived object dominant → old size lớn. Default 30/70 fit web app điển hình.
Q3Khác biệt giữa Metaspace và PermGen, vì sao Java 8 thay?▸
PermGen (Java 7 trở xuống): Permanent Generation — region trong heap cho class metadata. Cố định size, set qua -XX:MaxPermSize.
Vấn đề:
- Fixed size khó tune: app load class dynamic (Spring, Hibernate) → khó dự đoán cần bao nhiêu. Set thấp → OOM. Set cao → waste memory.
- Trong heap: GC class metadata cùng cycle với object → phức tạp. Class loader unload kém.
- Lỗi phổ biến
OutOfMemoryError: PermGen space: hot reload / redeploy classloader leak → quickly fill PermGen.
Metaspace (Java 8+): native memory, ngoài heap.
- Dynamic size: tự grow theo nhu cầu (giới hạn bởi RAM máy nếu không set max).
- Tách khỏi heap GC: GC class metadata theo trigger riêng (khi metaspace vượt high-water mark).
- Class loader unload tốt hơn: khi loader unreachable, JVM free toàn bộ class metadata loader đó.
Lý do thay:
- App modern dùng nhiều framework dynamic class (Spring AOP CGLIB proxy, Hibernate entity proxy, Mockito mock, Groovy DSL) — số class hard-to-predict.
- Container deployment (Tomcat, JBoss) hot redeploy → class loader leak phổ biến → PermGen OOM thường xuyên.
- Native memory dễ scale theo OS RAM thực tế — không pre-allocate trong JVM heap.
Caveat: Metaspace default unbound — class loader leak có thể ăn hết RAM máy → kernel OOM kill JVM. Production nên set -XX:MaxMetaspaceSize=512m (hoặc tùy app) để fail fast với OOM Java thay vì silent OOM kill.
Q4Vì sao heap vượt 32GB lại "tốn" memory hơn 31GB?▸
JVM 64-bit default bật compressed oops: reference object 4 byte thay vì 8 byte. JVM lưu reference shifted: oop_address = base + (compressed_oop << 3). Shift 3 bit (8-byte alignment) → 4 byte int đại diện được 4GB × 8 = 32GB.
Heap đến 32GB: compressed oops on. Mọi reference (field type Object, array element reference) 4 byte.
Heap vượt 32GB: compressed oops tự tắt (không đại diện đủ). Reference 8 byte.
Hệ quả: mọi object có reference field bỗng lớn ~50%:
String: 24 byte → 32 byte.HashMap.Node: 48 byte → 64 byte.ArrayListarray: mỗi reference 4 → 8 byte. List 10M element = 40 MB → 80 MB chỉ cho array.
Effective memory: heap 31 GB compressed có thể chứa nhiều object hơn heap 33 GB uncompressed. Ví dụ:
- Heap 31 GB, object trung bình 100 byte (compressed) → ~310M object.
- Heap 33 GB, object trung bình 130 byte (uncompressed +30% references) → ~250M object.
Best practice: nếu cần vượt 32GB heap, jump 64GB hẳn (gain bù pointer overhead). Nếu app fit 31GB, giữ ở đó. Đừng set 33-50GB — tệ nhất cả 2 mặt.
Check: java -XX:+PrintFlagsFinal -version | grep CompressedOops show on/off.
Modern alternative: ZGC (bài 5) hỗ trợ heap đến TB với "colored pointer" — không bị penalty này.
Q54 loại OutOfMemoryError khác nhau thế nào? Mỗi loại fix bằng cách gì?▸
OutOfMemoryError khác nhau thế nào? Mỗi loại fix bằng cách gì?Java heap space:- Heap đầy, GC không reclaim đủ.
- Nguyên nhân: memory leak (static map không bound, listener không unregister), heap nhỏ thực sự, object lớn (cache 1GB).
- Fix:
-XX:+HeapDumpOnOutOfMemoryError→ analyze MAT tìm leak. Hoặc tăng-Xmx.
Metaspace:- Class metadata phình.
- Nguyên nhân: classloader leak (hot reload, redeploy), dynamic class gen (Spring AOP, Mockito, Groovy DSL).
- Fix:
jcmd VM.classloader_statstìm loader giữ nhiều class. Audit static reference giữ classloader. Restart full thay hot reload.
Direct buffer memory:MaxDirectMemorySizevượt.- Nguyên nhân: app dùng Netty / NIO / gRPC alloc nhiều DirectByteBuffer, finalizer chậm free.
- Fix: tăng
-XX:MaxDirectMemorySize. Audit code release buffer (NettyByteBuf.release()). Force GC để Cleaner chạy.
unable to create new native thread:- OS từ chối tạo thread mới.
- Nguyên nhân: vượt OS limit (
ulimit -uLinux), hết native memory cho thread stack (mỗi thread 1MB), file descriptor cạn. - Fix:
ulimit -u 65535(hoặc systemd unit limit). Giảm-Xssnếu có thể. Audit thread leak — pool không bound, executor không shutdown.
Bonus loại 5: OutOfMemoryError: GC overhead limit exceeded — JVM tốn vượt 98% time GC mà reclaim chưa đến 2% heap → declare giving up. Nguyên nhân tương tự "heap space" (leak hoặc heap nhỏ), nhưng GC còn cố hết sức trước khi fail.
Loại 6: Kernel OOM Kill (Linux) — không phải Java exception, không stack trace. dmesg show "killed process X (java)". Xảy ra khi JVM tổng memory (heap + metaspace + native + stack) vượt cgroup limit. Fix: tổng các region nhỏ hơn container memory, đặc biệt set -Xmx conservative.
Q6Vì sao DirectByteBuffer (off-heap) đôi khi nhanh hơn ByteBuffer (heap), và khi nào nên dùng?▸
Khác biệt cốt lõi: location.
- Heap ByteBuffer: byte[] trên heap, JVM quản qua GC.
- Direct ByteBuffer:
mallocngoài heap, JVM giữ reference + Cleaner để free.
Direct nhanh hơn cho I/O vì:
- Zero-copy với syscall I/O:
FileChannel.read(directBuf)→ kernel đọc trực tiếp vào memory address của directBuf. Heap ByteBuffer phải copy từ kernel buffer → JVM internal native buffer → heap byte[] (3 hops). - Stable address: GC không di chuyển direct buffer. Heap object có thể bị GC compact → di chuyển → kernel call trong syscall blocking sẽ vỡ. JVM phải pin heap buffer trước syscall, unpin sau — overhead.
- Không GC pressure: direct buffer không trong young gen, không trigger minor GC.
Direct chậm hơn cho:
- Alloc/free:
mallocchậm hơn bump pointer eden ~10x. Free phụ thuộc Cleaner — non-deterministic. - Random access: get/put primitive cần JNI bridge. Heap ByteBuffer có Unsafe access trực tiếp.
Nên dùng direct khi:
- I/O lớn: file copy, network transfer ≥ 10 KB. Zero-copy benefit dominate alloc cost.
- Long-lived buffer: alloc 1 lần, dùng nhiều lần. Vd Netty pool buffer, Cassandra page cache.
- Off-heap storage: cache lớn không muốn ăn heap (vd Chronicle Map).
Không dùng khi:
- Buffer nhỏ ngắn hạn: alloc cost vượt benefit.
- Random read/write nhiều: heap ByteBuffer nhanh hơn cho access pattern này.
Production: thường dùng pool buffer (Netty ByteBufAllocator, gRPC ByteBufPool) — giảm alloc cost bằng reuse, giữ benefit zero-copy I/O.
Set -XX:MaxDirectMemorySize=512m để tránh leak — default unbounded nguy hiểm.
Bài tiếp theo: Garbage Collection — G1, ZGC, Parallel
Bài này có giúp bạn hiểu bản chất không?
Bình luận (0)
Đang tải...