Java Internals & Concurrency/Object Header và Compressed OOP — tại sao mỗi object Java tốn ít nhất 16 byte
22/26
Bài 22 / 26~20 phútJVM InternalsMiễn phí lượt xem

Object Header và Compressed OOP — tại sao mỗi object Java tốn ít nhất 16 byte

Mark word, klass pointer, field layout, padding, và compressed OOP: cơ chế giúp JVM tiết kiệm 30-50% heap. Hiểu 32GB cliff anti-pattern và cách dùng JOL để đo đạc.

TL;DR: Mỗi object Java trong HotSpot JVM 64-bit mang một header 12-16 byte gồm mark word (8 byte: lưu hash code, GC age, lock state) và klass pointer (4 byte khi compressed OOP bật, 8 byte khi tắt). Sau header là instance field sắp xếp theo kích thước (long trước, byte cuối), rồi padding về bội số 8 byte. Compressed OOP (mặc định bật khi heap dưới 32 GB) biểu diễn reference bằng 32-bit thay 64-bit, tiết kiệm 30-50% bộ nhớ cho reference-heavy object. Heap chạm ngưỡng 32 GB thì JVM tự tắt compressed OOP — object phình to 30-50%. Đặt heap 31 GB compressed thường chứa được nhiều object hơn 33 GB uncompressed.

Bài 04 đã cho thấy Integer chiếm 16 byte và nói qua về compressed OOP. Bài này đi sâu: cấu trúc bit của mark word, cách klass pointer trỏ đến metaspace, thuật toán JVM sắp xếp field, và cơ chế toán học behind compressed OOP. Dùng JOL để thực nghiệm kiểm chứng.

1. Scenario — cache 10 triệu object nhỏ tốn gần gấp 3 lần dự kiến

Team xây in-memory cache cho hệ thống recommendation. Mỗi item trong cache là một UserPreference:

class UserPreference {
    long userId;       // 8 byte
    int  score;        // 4 byte
    byte category;     // 1 byte
}

Tính theo data: 10 triệu item × (8 + 4 + 1) = 130 MB. Thực tế sau khi load, heap monitor báo gần 400 MB. Chênh lệch 3 lần. Vì sao?

Câu trả lời nằm ở object headerpadding — 2 thành phần ẩn mà Java developer ít khi nghĩ đến. Layout thực tế của UserPreference:

Mark Word    :  8 byte
Klass Ptr    :  4 byte  (compressed OOP)
userId       :  8 byte  (long)
score        :  4 byte  (int)
category     :  1 byte  (byte)
padding      :  3 byte  (align to 8)
             --------
Total        : 28 byte

Với padding: 28 byte × 10M = 280 MB. Cộng array reference overhead ArrayList hoặc HashMap.Node thêm 16-32 byte per entry → gần 400 MB. Header và padding chiếm phần lớn overhead.

2. Object header — 2 thành phần

Object header trong HotSpot JVM gồm 2 phần bắt buộc nằm ở đầu mọi object heap:

  1. Mark word — 8 byte (trên 64-bit JVM): chứa metadata runtime của object.
  2. Klass pointer — 4 byte (compressed) hoặc 8 byte (uncompressed): trỏ đến cấu trúc Klass của class trong metaspace.

Array object có thêm 1 trường array length (4 byte), nên header array = 16 byte.

Analogy — phong bì thư

Mark word là phần mã bưu chính + tem trên phong bì — hệ thống vận chuyển (JVM) cần để phân loại, định tuyến, ưu tiên. Klass pointer là địa chỉ người gửi — nói rõ object thuộc class nào. Dữ liệu thực (field) là nội dung thư bên trong. Dù thư chỉ 1 tờ giấy (1 int field), vẫn phải có phong bì (header).

3. Mark word — 64-bit packed state

Mark word là 8 byte chứa nhiều thông tin ép vào nhau tuỳ trạng thái lock của object. HotSpot JVM dùng 2 bit thấp (tag bits) để biết object đang ở trạng thái nào:

Trạng thái    Tag bits   Nội dung còn lại (62 bit)
-----------   --------   -----------------------------------------------
Unlocked      01         [unused:25][hash:31][age:4][biased:0][01]
Biased lock   01         [thread_id:54][epoch:2][age:4][biased:1][01]
Lightweight   00         [pointer-to-lock-record:62][00]
Heavyweight   10         [pointer-to-monitor:62][10]
Marked-GC     11         [forwarding pointer:62][11]

4 trường quan trọng trong trạng thái unlocked (thường gặp nhất):

  • Hash code (31 bit): giá trị trả về bởi Object.hashCode(). Tính lazy — chỉ compute lần đầu gọi hashCode(), rồi cache vào mark word. Object chưa gọi hashCode() → bit hash bằng 0.
  • GC age (4 bit): số lần object survive qua minor GC. Tăng 1 mỗi lần GC copy object. Đạt MaxTenuringThreshold (default 15, giới hạn bởi 4 bit) → promote sang Old generation.
  • Biased lock bit (1 bit): phân biệt unlocked vs biased. Deprecated trong Java 15 (JEP 374) và bị tắt theo mặc định, removed hoàn toàn trong Java 18.
  • Tag bits (2 bit): báo hiệu trạng thái lock.

Khi object bị synchronized, mark word chuyển sang lightweight lock (pointer đến lock record trên stack của thread đang giữ lock), hoặc heavyweight monitor nếu contention cao.

JEP 374 — Biased Locking Deprecation (Java 15)

Biased locking là optimization cũ: nếu 1 thread thường xuyên lock object độc quyền, JVM "bias" mark word sang thread ID đó — lần sau lock không cần CAS. Nhưng revoke biased lock (khi thread khác cần lock) tốn STW pause. Java 15 deprecated, Java 18 tắt hẳn. Bài safepoint (bài 09) giải thích tại sao revoke tốn STW.

4. Klass pointer — trỏ đến metaspace

Klass pointer (viết Klass — C++ class trong HotSpot source, không phải Java Class) là con trỏ đến metadata của class trong metaspace. Klass chứa:

  • Method table (vtable): bảng pointer đến phương thức ảo, dùng cho dynamic dispatch.
  • Interface table (itable): tương tự vtable cho interface.
  • Constant pool runtime: các hằng số đã resolve (string intern, class reference).
  • Field layout info: thứ tự và offset từng field.
  • Superclass + interfaces: chain kế thừa để instanceof check nhanh.

Mỗi lần gọi method ảo (obj.doSomething() qua interface hay abstract class), JVM đọc klass pointer → tìm vtable → tìm pointer đến compiled method. Quá trình này O(1) với offset cố định sau JIT.

Mỗi lần instanceof hoặc cast, JVM đi theo chain klass pointer để check hierarchy — cũng O(depth), thường O(1) sau JIT inline cache.

5. Field layout — JVM reorder để minimize padding

Theo nguồn JVM specification, JVM không bắt buộc phải giữ thứ tự khai báo field trong memory. HotSpot JVM tự sắp xếp field để minimize padding (wasted bytes). Quy tắc sắp xếp trong HotSpot:

Nhóm 1 (8 byte): long, double
Nhóm 2 (4 byte): int, float
Nhóm 3 (2 byte): short, char
Nhóm 4 (1 byte): byte, boolean
Nhóm 5 (4 byte): reference (compressed OOP)

Trong một class, HotSpot sắp xếp field theo nhóm từ trên xuống, không theo thứ tự khai báo. Sau đó parent class field được đặt trước child class field.

Ví dụ — thứ tự khai báo gây padding không cần thiết:

// Khai bao nguyen ban (source order)
class BadLayout {
    byte  a;   // 1 byte
    long  b;   // 8 byte
    byte  c;   // 1 byte
    int   d;   // 4 byte
}

Nếu giữ source order: a(1) + padding(7) + b(8) + c(1) + padding(3) + d(4) = 24 byte.

Sau khi HotSpot reorder: b(8) + d(4) + a(1) + c(1) + padding(2) = 16 byte sau header. Tiết kiệm 8 byte mỗi object.

// HotSpot thuc te layout (dung jol-cli de verify):
// header mark word  : 8 byte
// header klass ptr  : 4 byte
// b (long)          : 8 byte  <- sau header, align 8
// d (int)           : 4 byte
// a (byte)          : 1 byte
// c (byte)          : 1 byte
// padding           : 2 byte  (align total to 8)
//                   ------
//                   28 byte
Superclass field trước, subclass field sau

Nếu class B extends A, field của A được đặt trước field của B trong memory — bất kể visibility. Nếu A kết thúc ở offset không align với 8, phần đầu của field B có thể bị padding lãng phí. Pattern superclass có 1 field int (4 byte) → 4 byte padding trước field long đầu tiên của subclass — mất 4 byte mỗi object.

6. Padding — quy tắc align 8 byte

HotSpot yêu cầu mỗi object phải có tổng kích thước là bội số của 8 byte. Lý do: JVM allocation bằng bump pointer, alignment đảm bảo pointer luôn nhảy theo bước 8, đơn giản hoá free-list management và compressed OOP encoding.

Nếu header + fields chưa đủ bội 8, JVM thêm padding bytes vào cuối:

class Example {
    int x;   // 4 byte
    int y;   // 4 byte
}
// Header mark: 8, klass: 4, x: 4, y: 4 = 20 byte total -> pad 4 bytes -> 24 byte
class Example2 {
    long x;  // 8 byte
}
// Header mark: 8, klass: 4, padding 4, x: 8 = 24 byte (long must align to 8)

Có thể thay đổi object alignment bằng -XX:ObjectAlignmentInBytes=N (N phải là power of 2, từ 8 đến 256). Tăng alignment cho phép compressed OOP bao phủ heap lớn hơn 32 GB — nhưng mỗi object tốn nhiều padding hơn.

7. Sơ đồ layout tổng quát

flowchart TB
    subgraph ObjectMemory[Object Memory Layout - 64-bit HotSpot]
        A["Mark Word — 8 byte
        [hash:31 | age:4 | bias:1 | tag:2 | unused:24]"]
        B["Klass Pointer — 4 byte (compressed) / 8 byte
        → points to Klass struct in Metaspace"]
        C["Instance Fields
        (reordered: long/double → int/float → short/char → byte/boolean → ref)"]
        D["Padding
        (align total to multiple of 8)"]
    end
    A --> B --> C --> D

    subgraph MetaspaceKlass[Klass in Metaspace]
        E["vtable (method dispatch)"]
        F["itable (interface dispatch)"]
        G["Constant pool runtime"]
        H["Field layout info"]
    end
    B -. "klass pointer" .-> MetaspaceKlass

8. Compressed OOP — cơ chế và ý nghĩa

Compressed OOP (Ordinary Object Pointer) là cơ chế JVM biểu diễn reference đến object bằng 32 bit thay vì 64 bit đầy đủ. Cờ JVM là -XX:+UseCompressedOops, bật theo mặc định từ Java 7 update 6 khi heap nhỏ hơn 32 GB.

Cơ chế toán học:

JVM biết mọi object được align theo bội 8 byte. Do đó, địa chỉ thực luôn có 3 bit thấp = 0. JVM bỏ 3 bit này khi lưu, khôi phục khi dùng:

compressed_oop = (actual_address - heap_base) >> 3
actual_address = heap_base + (compressed_oop << 3)

Với 32-bit compressed pointer: giá trị tối đa = 2^32 - 1. Sau shift: 2^32 × 2^3 = 2^35 byte = 32 GB. Đây là giới hạn 32 GB.

Lợi ích cụ thể:

  • Mỗi reference field: 8 byte → 4 byte (tiết kiệm 50%).
  • Klass pointer: 8 byte → 4 byte.
  • Array of reference: mỗi slot tiết kiệm 4 byte.
  • ArrayList 1 triệu Integer: array reference 4M byte (compressed) vs 8M byte (uncompressed).

Với object điển hình (nhiều reference field), tổng tiết kiệm thường 30-50% heap.

Kiểm tra trạng thái compressed OOP:

java -XX:+PrintFlagsFinal -version | grep CompressedOops
# UseCompressedOops = true   (heap < 32 GB)
# UseCompressedOops = false  (heap >= 32 GB)

9. Pitfall — "32 GB cliff" anti-pattern

Đây là một trong những pitfall JVM quan trọng nhất mà nhiều team đã gặp trong production.

Khi heap chạm ngưỡng 32 GB, JVM tự động tắt compressed OOP. Reference field mỗi object đột ngột tăng từ 4 byte lên 8 byte. Klass pointer từ 4 byte lên 8 byte. Kết quả: mọi object có reference field tăng kích thước trung bình 30-50%.

# ANTI-PATTERN: heap chinh xac 32 GB hoac vuot qua mot chut
java -Xmx32g MyApp   # compressed OOP TAT
java -Xmx33g MyApp   # compressed OOP TAT

# DUNG: giu heap duoi nguong
java -Xmx31g MyApp   # compressed OOP BAT, object nho hon

# Ket qua paradox: heap 31 GB co the chua nhieu object hon 33 GB

Ví dụ cụ thể với String (Java 9+ compact):

String (compressed OOP ON, heap 31 GB):
  mark: 8, klass: 4, value(ref): 4, coder: 1, pad: 3, hash: 4 = 24 byte

String (compressed OOP OFF, heap 33 GB):
  mark: 8, klass: 8, value(ref): 8, coder: 1, pad: 7, hash: 4 = 36 byte

String tăng từ 24 lên 36 byte — 50%. App cache 100 triệu String: 2.4 GB (compressed) vs 3.6 GB (uncompressed). Heap 31 GB compressed chứa nhiều String hơn heap 33 GB uncompressed.

Ngưỡng thực tế thấp hơn 32 GB: JVM dùng một vùng địa chỉ nhỏ phía trên cho internal structure. Compressed OOP tắt một chút sớm hơn 32 GB chính xác. Ngưỡng an toàn thực tế là khoảng 30-31 GB. Kiểm tra bằng PrintFlagsFinal với heap cụ thể.

Đừng set heap 32-48 GB nếu không cần

Nếu app cần dưới 31 GB, giữ heap dưới ngưỡng compressed OOP. Nếu cần vượt, nhảy thẳng lên 64 GB trở lên để gain throughput bù pointer overhead. Heap 32-50 GB là worst of both worlds: compressed OOP tắt làm object phình to, nhưng chưa đủ lớn để G1 hay ZGC phát huy lợi thế heap rất lớn.

10. Compressed Class Pointer — flag riêng biệt

Klass pointer trong object header có flag riêng biệt với compressed OOP: -XX:+UseCompressedClassPointers.

Khi -XX:+UseCompressedClassPointers bật (default khi heap nhỏ), klass pointer nằm trong object header chỉ 4 byte, và JVM dùng một vùng địa chỉ nhỏ riêng (Compressed Class Space, default 1 GB, cấu hình bằng -XX:CompressedClassSpaceSize) để lưu tất cả Klass struct.

Compressed class pointer có thể bật độc lập với compressed OOP — JVM cho phép tắt OOP nhưng vẫn giữ class pointer compressed khi heap lớn (nhưng số class ít). Trong practice, cả 2 thường cùng trạng thái.

java -XX:+PrintFlagsFinal -version | grep Compressed
# UseCompressedClassPointers = true
# UseCompressedOops           = true
# CompressedClassSpaceSize    = 1073741824  (1 GB default)

11. Công cụ đo đạc — JOL CLI

JOL (Java Object Layout) là công cụ chính thức từ OpenJDK project để inspect layout chính xác của bất kỳ object nào. Không đoán — đo.

# Download JOL CLI
wget https://repo1.maven.org/maven2/org/openjdk/jol/jol-cli/0.17/jol-cli-0.17-full.jar

# Inspect layout cua java.lang.Integer
java -jar jol-cli-0.17-full.jar internals java.lang.Integer

# Output (HotSpot 64-bit, compressed OOP):
# java.lang.Integer object internals:
# OFF  SZ   TYPE DESCRIPTION               VALUE
#   0   8        (object header: mark)     0x0000000000000001
#   8   4        (object header: class)    0x00000001
#  12   4    int Integer.value             0
# Instance size: 16 bytes

Có thể dùng JOL trong code Java:

// Maven dependency:
// <dependency>
//   <groupId>org.openjdk.jol</groupId>
//   <artifactId>jol-core</artifactId>
//   <version>0.17</version>
// </dependency>

import org.openjdk.jol.info.ClassLayout;

System.out.println(ClassLayout.parseClass(UserPreference.class).toPrintable());

JOL show đúng offset từng field, padding bytes, và tổng instance size — không phụ thuộc vào suy đoán.

12. Deep Dive

Deep Dive — spec và reference
  • HotSpot JVM internals wikigithub.com/openjdk/jdk/blob/master/src/hotspot/share/oops/ — source C++ của oop.hpp (mark word layout), klass.hpp (Klass struct), instanceOop.hpp.
  • JEP 374 — Biased Locking Deprecationopenjdk.org/jeps/374 — lý do biased locking bị tắt: overhead revocation vượt benefit khi app modern đa thread.
  • JEP 450 — Compact Object Headers (Java 24 preview)openjdk.org/jeps/450 — đề xuất giảm header từ 96/128 bit xuống 64 bit (gộp mark word + klass pointer). Nếu stable, Integer giảm từ 16 byte xuống 12 byte. Project Valhalla song hành.
  • Aleksey Shipilёv — "Objects Inside Out"shipilev.net/jvm/objects-inside-out/ — bài viết chuẩn về object memory layout, field reordering, padding, alignment. Kèm benchmark thực tế cache-line impact.
  • JOL (Java Object Layout)github.com/openjdk/jol — tool chính thức OpenJDK, hỗ trợ inspect layout + virtual machine graph + footprint analysis.
  • Project Valhallaopenjdk.org/projects/valhalla/ — value type / primitive class: object không có header, inline trực tiếp vào array hay field của object khác. Khi stable, Integer về 4 byte như int.

13. Self-check

Tự kiểm tra
Q1
Mark word trong object header chứa những thông tin gì, và kích thước bao nhiêu byte trên JVM 64-bit?

Mark word là 8 byte (trên JVM 64-bit), chứa metadata runtime của object, được pack bit tuỳ trạng thái lock:

  • Hash code (31 bit) — giá trị trả về bởi Object.hashCode(), tính lazy lần đầu gọi rồi cache.
  • GC age (4 bit) — số lần survive qua minor GC. Đạt MaxTenuringThreshold (default 15) → promote Old generation. 4 bit giới hạn max là 15.
  • Biased lock thread ID / epoch (56 bit) — khi biased lock bật (Java 17 trở xuống). Deprecated JEP 374, removed Java 18.
  • Lock state pointer — khi lightweight lock: pointer đến lock record trên stack. Khi heavyweight: pointer đến monitor object.
  • GC forwarding pointer — khi GC đang copy/relocate object, mark word chứa địa chỉ mới để update reference.

Tag bits (2 bit thấp nhất) phân biệt trạng thái: 01 = unlocked/biased, 00 = lightweight locked, 10 = heavyweight locked, 11 = GC marked. JVM check tag bits trước khi interpret phần còn lại.

Điểm cốt lõi: mark word là union — cùng 8 byte dùng cho nhiều mục đích khác nhau tuỳ state. Đây là lý do object header nhỏ gọn (không cần field riêng cho mỗi thông tin).

JEP 450 (Java 24 preview) đề xuất compact header: gộp mark word + klass pointer vào 64 bit thay 96-128 bit — giảm header overhead đáng kể.

Q2
Vì sao JVM reorder field trong object thay vì giữ thứ tự khai báo trong source code?

JVM specification không mandate thứ tự field trong memory — chỉ quy định behavior đọc/ghi field phải đúng, không quy định layout. HotSpot dùng tự do này để minimize padding.

Lý do kỹ thuật: mọi primitive và reference đều có alignment requirement — phải đặt ở địa chỉ là bội số của kích thước. long/double cần bội 8. int/float cần bội 4. short/char cần bội 2. byte/boolean không cần align.

Nếu giữ source order:

class Bad {
  byte a;    // offset 12 (sau header 12)
  long b;    // offset 13 -> pad 7 -> offset 20
  byte c;    // offset 28
  int  d;    // offset 29 -> pad 3 -> offset 32
             // total: 36 byte
}

Sau reorder (HotSpot): long trước, int sau, byte cuối:

// header: 12, align pad 4, b(long): 8, d(int): 4, a(byte): 1, c(byte): 1, pad: 2
// total: 28 byte -- tiet kiem 8 byte moi object

Với 10 triệu object: tiết kiệm 80 MB. Bất ngờ: reorder tự động của JVM thường tốt hơn lập trình viên tự sắp xếp trong source — không cần tự optimize source order cho memory layout.

Ngoại lệ: field của parent class luôn đặt trước child class. Nếu parent kết thúc ở offset không align, có thể có padding giữa parent và child fields — không tránh được bằng reorder trong child.

Q3
Compressed OOP hoạt động bằng cơ chế gì, và tại sao giới hạn ở 32 GB?

Compressed OOP lợi dụng một tính chất của HotSpot: mọi object đều được align theo bội 8 byte. Do đó, 3 bit thấp nhất của địa chỉ thực luôn bằng 0 — không cần lưu.

Encoding:

compressed = (actual_address - heap_base) >> 3
actual     = heap_base + (compressed << 3)

32-bit compressed pointer có thể biểu diễn 2^32 giá trị. Sau khi shift 3 bit: phạm vi địa chỉ = 2^32 × 2^3 = 2^35 byte = 32 GB. Đây là giới hạn toán học — không thể tăng mà không tốn thêm bit.

Lợi ích:

  • Mỗi reference field: 8 byte → 4 byte.
  • Klass pointer: 8 → 4 byte.
  • Array reference element: 8 → 4 byte mỗi slot.
  • Tổng tiết kiệm reference-heavy object: 30-50%.

JVM kiểm tra heap size khi start. Nếu -Xmx dưới 32 GB (thực tế ~30 GB để dự phòng): bật compressed OOP. Nếu vượt: tắt — 32-bit pointer không đủ đại diện địa chỉ heap lớn hơn.

Có thể mở rộng giới hạn bằng tăng alignment: -XX:ObjectAlignmentInBytes=16 cho phép 64 GB (shift 4 thay vì 3), nhưng mỗi object tốn padding nhiều hơn — không thực sự lợi.

Q4
"32 GB cliff" là gì và tại sao heap 31 GB đôi khi chứa được nhiều object hơn heap 33 GB?

32 GB cliff là tên gọi cho hiện tượng: khi heap vượt ngưỡng compressed OOP (~30-32 GB), JVM tắt compressed OOP, mọi reference từ 4 byte lên 8 byte, mọi klass pointer từ 4 byte lên 8 byte. Object phình to 30-50%.

Ví dụ với HashMap.Node:

// compressed OOP (heap 31 GB):
// mark: 8, klass: 4, hash: 4, next(ref): 4, key(ref): 4, value(ref): 4, pad: 4
// = 32 byte per node

// uncompressed OOP (heap 33 GB):
// mark: 8, klass: 8, hash: 4, pad 4, next(ref): 8, key(ref): 8, value(ref): 8
// = 48 byte per node -- 50% lon hon

HashMap 100 triệu entry:

  • Compressed: 100M × 32 byte = 3.2 GB node overhead.
  • Uncompressed: 100M × 48 byte = 4.8 GB node overhead.

Heap 31 GB compressed: 31 GB / 32 = ~970 triệu node slot có thể chứa.

Heap 33 GB uncompressed: 33 GB / 48 = ~690 triệu node slot — ít hơn dù heap lớn hơn.

Quy tắc kinh nghiệm:

  • Cần dưới 30 GB → giữ heap 28-30 GB compressed.
  • Cần vượt → nhảy 64 GB hẳn, không dừng 33-50 GB.
  • Heap 32-50 GB = worst of both worlds: object lớn hơn compressed, chưa đủ bù overhead.
  • Dùng ZGC nếu cần heap rất lớn — ZGC dùng colored pointer, không bị 32 GB cliff.
Q5
Klass pointer trỏ đến cấu trúc nào trong metaspace và chứa những gì?

Klass pointer trỏ đến Klass struct (C++ class trong HotSpot source, không phải Java Class object). Klass struct nằm trong metaspace — vùng native memory ngoài heap, không bị GC thông thường.

Klass struct chứa:

  • vtable (virtual method table): mảng pointer đến compiled method, indexing bằng slot cố định per method. Virtual call = đọc klass pointer → index vtable → jump. O(1).
  • itable (interface table): tương tự vtable nhưng cho interface. Phức tạp hơn vì 1 class có thể implement nhiều interface — itable tra cứu theo interface ID.
  • Constant pool runtime: bảng hằng số đã được resolve (class reference, method reference, string intern). Khác constant pool trong .class file — runtime pool đã resolve symbolic reference thành pointer.
  • Field layout descriptor: offset từng field, dùng khi reflective access hoặc JIT emit field load instruction.
  • Superclass chain + interface list: dùng cho instanceof check và casting.
  • Mirror (java.lang.Class): reference ngược lại Java Class object trên heap.

Tại sao Klass pointer quan trọng với performance:

  • Mỗi virtual call read klass pointer → vtable. Cache miss nếu object phân tán, klass ít được access → latency.
  • JIT inline cache: sau khi thấy object luôn cùng type, JIT bỏ klass lookup và hardcode call target. Giảm từ 2 memory access xuống 0.
  • Type check (instanceof) dùng klass chain — Java polymorphism không free nhưng được JIT optimize tốt.
Q6
Tại sao array object trong Java có header 16 byte trong khi object thường có header 12 byte (compressed OOP)?

Object thường: header = mark word (8) + klass pointer (4, compressed) = 12 byte.

Array object: header = mark word (8) + klass pointer (4, compressed) + array length (4 byte) = 16 byte.

Lý do array cần thêm length field:

  • Array length là thông tin runtime cần thiết để kiểm tra bound khi access: array[i] → JVM check 0 <= i < length → throw ArrayIndexOutOfBoundsException nếu vi phạm.
  • Length không encode được vào mark word (mark word dùng hết cho hash/age/lock state).
  • Length không thể lấy từ klass pointer (klass chỉ biết element type, không biết instance cụ thể dài bao nhiêu).

Hệ quả practical:

// int[] 0 element:
// header: 16, length: 0 element -> total: 16 byte
// int[] 1 element:
// header: 16, 1*4 = 4, pad 4 -> total: 24 byte
// int[] 2 element:
// header: 16, 2*4 = 8 -> total: 24 byte (no extra pad)
// int[] 100 element:
// header: 16, 100*4 = 400 -> total: 416 byte

Dùng JOL để verify: ClassLayout.parseInstance(new int[0]).toPrintable() sẽ show "OFF 16, SZ 0 int [] array".

Object[] (reference array): mỗi slot 4 byte (compressed OOP). Object[100]: 16 + 400 = 416 byte. Không tốn thêm vì element reference đã compressed.

Q7
Viết đoạn code Java dùng JOL để in layout của một class tự định nghĩa và giải thích output.

Thêm dependency JOL vào Maven:

<dependency>
  <groupId>org.openjdk.jol</groupId>
  <artifactId>jol-core</artifactId>
  <version>0.17</version>
</dependency>

Code in layout:

import org.openjdk.jol.info.ClassLayout;

class Point {
  int x;
  int y;
  String label;  // reference
}

System.out.println(ClassLayout.parseClass(Point.class).toPrintable());

Output mẫu (HotSpot 21, compressed OOP):

Point object internals:
OFF  SZ               TYPE DESCRIPTION
0   8                    (object header: mark)
8   4                    (object header: class)
12   4                int Point.x
16   4                int Point.y
20   4   java.lang.String Point.label
Instance size: 24 bytes
Gap losses: 0 bytes, Alignment losses: 0 bytes

Đọc output:

  • OFF: offset từ đầu object (byte).
  • SZ: kích thước field (byte).
  • TYPE: kiểu field.
  • Offset 0-7: mark word. Offset 8-11: klass pointer (4 byte compressed). Offset 12-15: x. Offset 16-19: y. Offset 20-23: label (4 byte compressed reference).
  • Total 24 byte — đã align về bội 8.

JOL cũng show "Gap losses" (padding trong field) và "Alignment losses" (padding cuối). Nếu class có field byte trước field long, JOL báo gap losses — giúp developer biết cần reorder (dù HotSpot thường tự reorder).

Dùng ClassLayout.parseInstance(new Point()) thay parseClass để thấy giá trị thực tế của mỗi field trong instance cụ thể.

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