Java Internals & Concurrency/Memory layout — heap, metaspace, stack, object header
19/26
Bài 19 / 26~25 phútJVM InternalsMiễn phí lượt xem

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.

Liên kết khoá học khác

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?

Hỏi đáp về bài này

Chưa có câu hỏi

Đặt 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