Object header và field layout — vì sao object nhỏ vẫn tốn 16 byte
Mark word, klass pointer, cách HotSpot reorder field để giảm padding và quy tắc align 8 byte — vì sao mọi object mang header ẩn, đo chính xác bằng JOL.
TL;DR: Mỗi object Java trong HotSpot 64-bit mang một header ẩn: mark word 8 byte (đóng gói identity hash, GC age, lock state) + klass pointer 4 byte (trỏ tới Klass trong metaspace). Object thường có header 12 byte, array thêm 4 byte length nên header 16 byte. Sau header là instance field — HotSpot reorder field (long trước, byte cuối) để lấp khoảng trống và giảm padding, rồi bù byte để tổng object là bội số 8. Vì header + padding, một object chỉ có 1 int như Integer vẫn tốn 16 byte. Đừng đoán layout — đo bằng JOL (Java Object Layout).
Team xây in-memory cache cho hệ thống recommendation, mỗi item là một object nhỏ 3 field. Tính theo dữ liệu thô chỉ 130 MB, nhưng heap monitor báo gần 400 MB — chênh gần 3 lần, không rò rỉ, không leak. Thủ phạm là hai thành phần ẩn mà Java developer ít khi nghĩ tới: object header và padding.
Bài 05 — Memory: heap & metaspace đã hé lộ Integer chiếm 16 byte cho một int 4 byte. Bài này mổ chỗ 12 byte còn lại đi đâu: cấu trúc bit của mark word, klass pointer trỏ về đâu, thuật toán HotSpot sắp xếp field, và cách đo layout chính xác bằng JOL. Cơ chế nén reference (compressed OOP) và giới hạn 32 GB để bài 09b đi sâu.
1. Vì sao cache 10 triệu object nhỏ tốn gần gấp 3 lần?
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 dữ liệu: 10 triệu item nhân (8 + 4 + 1) = 130 MB. Thực tế sau khi load, heap báo gần 400 MB. Vì sao gấp 3?
Câu trả lời nằm ở header và padding. Layout thực tế (HotSpot tự reorder field — chi tiết mục 5):
Mark word : 8 byte (offset 0)
Klass ptr : 4 byte (offset 8, compressed class pointer)
score : 4 byte (int - lap vao khe ngay sau header, offset 12)
userId : 8 byte (long - offset 16, boi so cua 8)
category : 1 byte (offset 24)
padding : 7 byte (bu tong ve boi so 8)
--------
Total : 32 byte
32 byte nhân 10 triệu = 320 MB. Cộng array reference của ArrayList hoặc overhead per-entry của HashMap.Node là gần 400 MB. Header và padding chiếm phần lớn khoản phình.
2. Object header — hai thành phần
Object header trong HotSpot gồm 2 phần bắt buộc nằm ở đầu mọi object trên heap:
- Mark word — 8 byte (trên JVM 64-bit): metadata runtime của object.
- Klass pointer — 4 byte (khi compressed class pointer bật, mặc định) hoặc 8 byte: trỏ tới cấu trúc
Klasscủa class trong metaspace.
Object thường có header 12 byte. Array có thêm một trường length 4 byte (JVM cần biết độ dài để check bound), nên header array là 16 byte.
Mark word là 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. Dữ liệu thực (field) là nội dung thư bên trong. Dù thư chỉ một tờ giấy (một int field), vẫn phải có phong bì (header).
Vì header luôn có, một object chỉ chứa một int như Integer đã ngốn 16 byte:
8 byte
4 byte
4 byte
java.lang.Integer = 16 byte (12 header + 4 field, vừa đúng bội 8 nên không cần padding)
3. Mark word — trạng thái đóng gói 64-bit
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 dùng 2 bit thấp (tag bits) để biết object đang ở trạng thái nào — đọc tag trước rồi mới hiểu 62 bit còn lại:
Trang thai Tag Layout day du (tong = 64 bit)
----------- --- ----------------------------------------------------
Unlocked 01 [unused:25][hash:31][unused:1][age:4][biased:1][01]
Biased lock 01 [thread_id:54][epoch:2][unused:1][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]
Bốn thông tin quan trọng trong trạng thái unlocked (thường gặp nhất):
- Identity hash (31 bit): giá trị
Object.hashCode()mặc định. Tính lazy — chỉ compute lần đầu gọihashCode(), rồi cache vào mark word. Object chưa gọi thì vùng hash bằng 0. - GC age (4 bit): số lần object sống sót qua minor GC. 4 bit nên max là 15 — đúng bằng
MaxTenuringThresholdmặc định; đạt ngưỡng thì object được promote sang Old generation. - Biased lock bit (1 bit): phân biệt unlocked với biased. Biased locking đã bị deprecated và tắt mặc định từ Java 15 (JEP 374), và obsolete từ JDK 18 (JDK-8256425, code đã gỡ) — trên JDK 21 (bản LTS bài này nhắm tới) bit này thực chất không dùng.
- Tag bits (2 bit): báo trạng thái lock.
Khi object bị synchronized, mark word chuyển sang trỏ lock record trên stack (lightweight) hoặc monitor object (heavyweight khi contention cao). Điểm cốt lõi: mark word là union — cùng 8 byte tái sử dụng cho nhiều mục đích khác nhau tuỳ state, nhờ vậy header mới nhỏ gọn.
Biased locking là tối ưu cũ: nếu một thread thường xuyên lock một object độc quyền, JVM "bias" mark word sang thread ID đó — lần sau lock khỏi cần CAS. Nhưng khi thread khác cần lock, revoke bias tốn một STW pause. Với app đa thread hiện đại, chi phí revoke vượt lợi ích nên Java 15 tắt mặc định. Cơ chế STW này được bài 11 — Safepoint & STW giải thích.
4. Klass pointer — trỏ tới metaspace
Klass pointer (viết Klass — C++ struct trong HotSpot source, không phải Java Class) trỏ tới metadata của class trong metaspace. Klass chứa:
- vtable: bảng con trỏ tới phương thức ảo, dùng cho dynamic dispatch.
- itable: tương tự vtable nhưng cho interface.
- Constant pool runtime: hằng số đã resolve (class reference, string intern).
- Field layout info: offset từng field.
- Superclass + interface chain: dùng cho
instanceofvà cast.
Mỗi lần gọi method ảo (obj.doSomething()), JVM đọc klass pointer, tra vtable, nhảy tới compiled method — O(1) với offset cố định sau khi JIT inline cache. Klass pointer chỉ 4 byte khi compressed class pointer bật (mặc định) — cơ chế nén này là chủ đề bài 09b.
5. Field layout — HotSpot reorder để giảm padding
Theo JVM specification, JVM không bắt buộc giữ thứ tự khai báo field trong bộ nhớ — chỉ đảm bảo đọc/ghi đúng. HotSpot dùng tự do này để minimize padding. Quy tắc sắp xếp:
Nhom 1 (8 byte): long, double
Nhom 2 (4 byte): int, float
Nhom 3 (2 byte): short, char
Nhom 4 (1 byte): byte, boolean
Nhom 5 (4 byte): reference (compressed OOP)
HotSpot xếp field theo nhóm từ trên xuống (không theo source order), và luôn đặt field của parent class trước field của subclass.
Worked example — thứ tự khai báo gây padding thừa:
class BadLayout {
byte a; // 1 byte
long b; // 8 byte
byte c; // 1 byte
long d; // 8 byte
}
Nếu HotSpot giữ nguyên source order, mọi field phải nằm ở offset chia hết cho kích thước của nó (long cần bội 8):
header : offset 0-11
a (byte): offset 12
b (long): 13 khong chia het 8 -> pad 3 -> offset 16-23
c (byte): offset 24
d (long): 25 khong chia het 8 -> pad 7 -> offset 32-39
-> total 40 byte
Nhưng HotSpot reorder: hai long đặt liền từ offset 16, hai byte được lấp vào khe trống ngay sau header (offset 12-13):
header : offset 0-11
a (byte): offset 12 <- lap vao khe sau header
c (byte): offset 13
padding : offset 14-15 (2 byte)
b (long): offset 16
d (long): offset 24
-> total 32 byte (tiet kiem 8 byte moi object)
Với 10 triệu object, đó là 80 MB tiết kiệm — mà lập trình viên không phải làm gì. Reorder tự động của HotSpot thường tốt hơn tự sắp xếp source order.
Nếu B extends A, field của A luôn nằm trước field của B — bất kể visibility. Nếu A kết thúc ở offset không align 8, phần đầu field B có thể lãng phí padding. Ví dụ superclass chỉ có một int (4 byte) sẽ chèn 4 byte padding trước long đầu tiên của subclass — reorder trong subclass không cứu được vì ranh giới parent/child là cố định.
6. Padding — quy tắc align 8 byte
HotSpot yêu cầu tổng kích thước mỗi object là bội số của 8 byte. Lý do: allocation bằng bump pointer (xem bài 05) — alignment 8 khiến pointer luôn nhảy theo bước 8, đơn giản hoá free-list và encoding của compressed OOP. Thiếu thì JVM bù padding vào cuối:
class Example {
int x; // 4 byte
int y; // 4 byte
}
// header 12 + x 4 + y 4 = 20 -> pad 4 -> 24 byte
Chỉnh alignment bằng -XX:ObjectAlignmentInBytes=N (power-of-2, 8 tới 256) cho phép compressed OOP phủ heap lớn hơn 32 GB — nhưng mỗi object tốn padding nhiều hơn, thường không lời (xem bài 09b).
7. Sơ đồ layout tổng quát
flowchart TB
subgraph OBJ["Object tren heap - 64-bit HotSpot"]
A["Mark word - 8 byte<br/>hash / age / lock state"]
B["Klass pointer - 4 byte<br/>tro toi Klass trong Metaspace"]
C["Instance fields<br/>reorder: long -> int -> short -> byte -> ref"]
D["Padding<br/>bu tong ve boi so 8"]
end
A --> B --> C --> D
subgraph META["Klass trong Metaspace"]
E["vtable / itable"]
F["Constant pool runtime"]
G["Field layout info"]
end
B -. "klass pointer" .-> META8. Đo bằng JOL — đừng đoán, hãy đo
JOL (Java Object Layout) là công cụ chính thức của OpenJDK để inspect layout chính xác của bất kỳ object nào. Trước khi xem output, hãy tự tính đã:
Cho class Point có ba field: int x, int y, và String label (một reference). Với HotSpot 64-bit, compressed OOP bật: object này chiếm bao nhiêu byte? Viết ra con số bạn tin chắc trước khi đọc tiếp.
import org.openjdk.jol.info.ClassLayout;
class Point {
int x;
int y;
String label; // reference
}
System.out.println(ClassLayout.parseClass(Point.class).toPrintable());
Output (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
Đọc output: header chiếm offset 0-11 (mark 8 + klass 4), x ở 12, y ở 16, label ở 20 (reference 4 byte nhờ compressed OOP). Tổng 24 byte — đã align bội 8. Nếu bạn đoán 20, bạn quên rằng 20 không phải bội 8. JOL còn báo Gap losses và Alignment losses — hai con số cho biết object có lãng phí padding không. Bản CLI (java -jar jol-cli-full.jar internals java.lang.Integer) soi được class có sẵn mà không cần viết code.
9. Cơ chế bên dưới — Compact Object Headers (JDK 24/25)
Header 12 byte là chuẩn nhiều năm, nhưng Compact Object Headers đang đổi con số đó — thử nghiệm ở JDK 24 (JEP 450, Experimental), thành product ở JDK 25 (JEP 519), bật bằng -XX:+UseCompactObjectHeaders. Nó nén klass pointer (32 bit xuống 22 bit) và gộp vào mark word, đưa header từ 12 byte xuống 8 byte.
Hệ quả phụ thuộc alignment: một java.lang.Object trần giảm từ 16 xuống 8 byte, nhưng Integer vẫn 16 byte — 8 byte header cộng int 4 byte thành 12, rồi padding kéo lại lên 16 (4 byte tiết kiệm bị padding "nuốt"). Lợi ích thật xuất hiện ở object mà 4 byte đó đủ để tụt một bậc alignment; đo thực tế heap giảm 10-22% trên workload nhiều object nhỏ.
Nguồn: JEP 519 · Shipilev — Objects Inside Out · JOL. Xa hơn, Project Valhalla hướng tới value class không header, inline thẳng vào array/field.
Liên hệ các bài khác
- Bài 05 — Memory: heap & metaspace: object header nằm trong Eden khi vừa allocate; GC age trong mark word điều khiển việc promote qua các generation.
- Bài 09b — Compressed OOP & 32GB cliff: vì sao klass pointer và reference chỉ 4 byte, cơ chế toán học behind, và cái bẫy khi heap chạm 32 GB.
- Bài 11 — Safepoint & STW: revoke biased lock và cập nhật forwarding pointer trong mark word đều cần STW.
10. Tóm tắt
- Object header = mark word (8) + klass pointer (4 compressed) = 12 byte; array thêm 4 byte length nên 16 byte.
- Mark word là union 8 byte: identity hash (31 bit), GC age (4 bit), hoặc con trỏ lock/monitor/forwarding tuỳ trạng thái; 2 tag bit cho biết đọc kiểu nào. Biased locking đã bỏ (deprecated Java 15, removed Java 18).
- Klass pointer trỏ tới
Klasstrong metaspace — nền của dynamic dispatch vàinstanceof. - Field layout: HotSpot reorder theo nhóm kích thước và lấp khe sau header để giảm padding; field parent luôn đặt trước subclass.
- Alignment 8 byte khiến mọi object là bội 8; padding bù phần thiếu, nên
Integervới mộtintvẫn tốn 16 byte. Đo bằng JOL, đừng đoán.
11. Tự kiểm tra
Q1Mark word chứa những gì, và vì sao gọi nó là một union?▸
Ở trạng thái unlocked, mark word (8 byte) chứa identity hash (31 bit, tính lazy rồi cache) và GC age (4 bit, max 15). Khi object bị lock, cùng vùng đó thành con trỏ tới lock record (lightweight) hoặc monitor (heavyweight); khi GC relocate, nó thành forwarding pointer.
Gọi là union vì cùng 8 byte tái sử dụng cho nhiều mục đích loại trừ nhau — không có field riêng cho mỗi thông tin. Hai tag bit thấp cho biết đang đọc kiểu nào, nhờ vậy header mới nhỏ gọn.
Q2Vì sao HotSpot reorder field thay vì giữ thứ tự khai báo trong source?▸
JVM specification không mandate thứ tự field trong bộ nhớ — chỉ yêu cầu đọc/ghi đúng. HotSpot dùng tự do đó để giảm padding: xếp field theo nhóm kích thước giảm dần và lấp field nhỏ vào khe trống sau header. Ví dụ byte, long, byte, long giữ source order tốn 40 byte, sau reorder chỉ 32 byte. Ngoại lệ: field parent luôn đặt trước subclass, nên ranh giới đó có thể còn padding không tránh được.
Q3Klass pointer trỏ tới cấu trúc nào, và nó nằm ở đâu?▸
Trỏ tới Klass struct (C++ struct trong HotSpot, không phải Java Class object) trong metaspace — native memory ngoài heap. Klass chứa vtable, itable, constant pool runtime, field layout info, và chain superclass cho instanceof. Vì Klass nằm ngoài heap, nhiều instance cùng class chia sẻ một Klass — object chỉ mang con trỏ 4 byte.
Q4Vì sao array object có header 16 byte trong khi object thường chỉ 12 byte (compressed OOP)?▸
Object thường: mark word (8) + klass pointer (4) = 12 byte. Array thêm một trường length 4 byte = 16 byte. Array cần length riêng vì mỗi lần truy cập array[i] JVM phải check bound; length không nhét được vào mark word (đã dùng hết) và klass chỉ biết element type chứ không biết instance dài bao nhiêu. Ví dụ int[0] vẫn tốn 16 byte chỉ riêng header.
Q5Cho class chỉ có hai int field. Object tốn bao nhiêu byte? Viết code JOL để kiểm chứng.▸
Header 12 + hai int (4 + 4) = 20 byte, chưa phải bội 8 nên padding thêm 4 -> tổng 24 byte. Đừng dừng ở 20 — luôn nhớ bước align cuối.
class TwoInts { int x; int y; }
System.out.println(
ClassLayout.parseInstance(new TwoInts()).toPrintable());
// Instance size: 24 bytesBài tiếp theo: Compressed OOP và 32GB cliff — vì sao heap 31GB chứa nhiều object hơn 33GB
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên