Java — Từ Zero đến Senior/Memory layout — heap, metaspace, stack, object header
~25 phútJVM InternalsMiễn phí

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ườngJVM
Warehouse (kho)Heap
Khu mới / khu cũYoung / Old generation
Thư viện kế hoạchMetaspace
Bàn làm việcThread stack
Sân ngoàiNative memory
Nhãn dánObject header
Vật phẩmObject instance
Bản thiết kếClass object trong metaspace
💡 Cách nhớ

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 @Configuration tạ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

AspectStackHeap
AllocPush frame, O(1), automaticnew, qua TLAB hoặc lock-free bump
FreePop frame khi method returnGC reclaim khi unreachable
Size1MB/thread defaultTens of GB
ConcurrentMỗi thread riêng — no syncShared, cần sync khi alloc
LifetimeMethod invocationTớ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

📚 Deep Dive Oracle

Spec / reference chính thức:

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:MaxMetaspaceSize cho 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 -Xmx hoặc fix leak.
    • Metaspace → class metadata phình, fix loader leak.
    • Direct buffer memoryMaxDirectMemorySize quá 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 — Integer về 4 byte như int.

11. Tự kiểm tra

Tự kiểm tra
Q1
Vì sao 1 đối tượng 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.

Q2
Heap 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:

  1. new Foo() → alloc trong Eden (sub-region young).
  2. Eden đầy → GC minor: scan young. Object live copy sang Survivor S0. Eden clear (1 swipe — tất cả object Eden chết).
  3. Lần GC sau: object live (kể cả S0 và Eden) copy sang S1. S0 + Eden clear.
  4. 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.)
  5. Object survive N round (default ~15) → promote sang Old. Hoặc nếu survivor đầy → promote sớm.
  6. 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.

Q3
Khá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:

  1. 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.
  2. Container deployment (Tomcat, JBoss) hot redeploy → class loader leak phổ biến → PermGen OOM thường xuyên.
  3. 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.

Q4
Vì 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.
  • ArrayList array: 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.

Q5
4 loại OutOfMemoryError khác nhau thế nào? Mỗi loại fix bằng cách gì?
  1. 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.
  2. 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_stats tìm loader giữ nhiều class. Audit static reference giữ classloader. Restart full thay hot reload.
  3. Direct buffer memory:
    • MaxDirectMemorySize vượ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 (Netty ByteBuf.release()). Force GC để Cleaner chạy.
  4. unable to create new native thread:
    • OS từ chối tạo thread mới.
    • Nguyên nhân: vượt OS limit (ulimit -u Linux), 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 -Xss nế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.

Q6
Vì 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: malloc ngoài heap, JVM giữ reference + Cleaner để free.

Direct nhanh hơn cho I/O vì:

  1. 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).
  2. 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.
  3. Không GC pressure: direct buffer không trong young gen, không trigger minor GC.

Direct chậm hơn cho:

  • Alloc/free: malloc chậ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...