Object Memory & String Pool
Bài trước: Stack vs Heap chi tiết — Bạn đã biết objects nằm trên Heap. Bài này đi sâu vào cấu trúc bên trong một object trên Heap và cơ chế String Pool.
Object Layout trong Memory
Mỗi object trên Heap gồm 3 phần:
┌──────────────────────────────────────────────┐
│ Object trên Heap │
├──────────────────────────────────────────────┤
│ Object Header │
│ ┌────────────────────────────────────┐ │
│ │ Mark Word (8 bytes) │ │
│ │ → hash code, GC age, lock state │ │
│ ├────────────────────────────────────┤ │
│ │ Class Pointer (4 bytes compressed) │ │
│ │ → trỏ đến Class metadata │ │
│ └────────────────────────────────────┘ │
├──────────────────────────────────────────────┤
│ Instance Data │
│ → Giá trị các fields (thực sự) │
├──────────────────────────────────────────────┤
│ Padding │
│ → Đệm cho object size chia hết cho 8 │
└──────────────────────────────────────────────┘
Mark Word (8 bytes trên 64-bit JVM)
Mark Word chứa metadata runtime của object:
| Bit field | Nội dung |
|---|---|
| Identity hash code | Kết quả System.identityHashCode() |
| GC age | Số lần object sống sót qua GC (dùng để promote sang Old Gen) |
| Lock state | Biased lock, thin lock, hoặc heavy lock (cho synchronization) |
| GC flags | Đánh dấu cho Garbage Collector |
Class Pointer (4 bytes với Compressed Oops)
Trỏ đến Class metadata trong Method Area — cho JVM biết object thuộc class nào:
Object obj = new ArrayList<>();
obj.getClass(); // JVM đọc Class Pointer → trả về ArrayList.class
Mặc định trên 64-bit JVM với heap ≤ 32GB, JVM dùng compressed pointers (4 bytes thay vì 8 bytes). Tiết kiệm ~30-40% memory. Flag: -XX:+UseCompressedOops (default on).
Instance Data
Chứa giá trị thực sự của các fields:
public class User {
private int age; // 4 bytes
private boolean active; // 1 byte
private String name; // 4 bytes (compressed reference)
}
JVM không lưu fields theo thứ tự khai báo. Nó sắp xếp lại để giảm padding:
Thứ tự khai báo: int(4) → boolean(1) → String(4) = 4+1+3(pad)+4 = 12 bytes
JVM reorder: int(4) → String(4) → boolean(1) → 3(pad) = 12 bytes
Cả hai trường hợp có thể cùng 12 bytes, nhưng JVM chọn layout tối ưu nhất.
Padding
Object size phải chia hết cho 8 bytes (object alignment). JVM thêm padding nếu cần:
User object (64-bit JVM, Compressed Oops):
┌───────────────────────────┐
│ Mark Word 8 bytes│
│ Class Pointer 4 bytes│
│ age (int) 4 bytes│
│ active (boolean) 1 byte │
│ name (reference) 4 bytes│
│ Padding 3 bytes│ ← Đệm cho tổng = 24 (chia hết 8)
└───────────────────────────┘
Total: 24 bytes
Tại sao Object Size quan trọng?
Overhead so với Primitive
int primitive = 42; // 4 bytes trên Stack
Integer wrapper = 42; // 16 bytes trên Heap (header 12 + int 4)
Wrapper overhead = 4x — đây là lý do dùng primitive thay wrapper khi có thể:
| Type | Primitive size | Wrapper size | Overhead |
|---|---|---|---|
int / Integer | 4 bytes | 16 bytes | 4x |
long / Long | 8 bytes | 24 bytes | 3x |
boolean / Boolean | 1 byte | 16 bytes | 16x |
class Empty { }
Empty e = new Empty(); // 16 bytes (12 header + 4 padding)
Mỗi object có overhead ít nhất 16 bytes chỉ cho header. Đây là lý do collections of primitives (như int[]) tiết kiệm memory hơn collections of wrappers (như List<Integer>).
Integer Cache giải thích bằng Object Layout
Integer a = 127; // → IntegerCache.cache[255] (object đã tạo sẵn)
Integer b = 128; // → new Integer(128) = 16 bytes mới trên Heap
JVM pre-create 256 Integer objects (-128 đến 127) khi khởi động → tiết kiệm memory cho giá trị phổ biến. Tổng cache: 256 × 16 bytes = 4KB — đáng để đầu tư.
String Pool (String Interning)
String Pool là gì?
String Pool là vùng đặc biệt trên Heap nơi JVM lưu trữ String literals để tái sử dụng:
String s1 = "Hello"; // → String Pool: tạo hoặc reuse "Hello"
String s2 = "Hello"; // → String Pool: reuse "Hello" (cùng object)
String s3 = new String("Hello"); // → Heap mới: luôn tạo object MỚI
System.out.println(s1 == s2); // true — cùng reference trong Pool
System.out.println(s1 == s3); // false — khác reference (Pool vs Heap)
System.out.println(s1.equals(s3)); // true — cùng nội dung
Heap:
┌────────────────────────────────────────────┐
│ │
│ String Pool: │
│ ┌──────────┐ │
│ │ "Hello" │ ← s1 và s2 cùng trỏ đến │
│ └──────────┘ │
│ │
│ Regular Heap: │
│ ┌──────────┐ │
│ │ "Hello" │ ← s3 trỏ đến (object khác) │
│ └──────────┘ │
└────────────────────────────────────────────┘
String.intern()
Method intern() thêm String vào Pool thủ công:
String s1 = new String("Hello"); // Trên Heap (không trong Pool)
String s2 = s1.intern(); // Thêm vào Pool, trả về Pool reference
String s3 = "Hello"; // Từ Pool
System.out.println(s1 == s3); // false — s1 trên Heap, s3 trong Pool
System.out.println(s2 == s3); // true — cả hai từ Pool
- Khi có rất nhiều String trùng lặp (ví dụ: đọc CSV với cột giá trị lặp)
- Tiết kiệm memory đáng kể khi duplicate ratio cao
- Cẩn thận: intern() có chi phí (lookup trong pool), không dùng cho mọi String
String Pool History
| Java version | String Pool location |
|---|---|
| Java 6 trở về trước | PermGen (cố định, dễ OOM) |
| Java 7+ | Heap (GC quản lý, mở rộng được) |
Compact Strings (Java 9+)
Trước Java 9, mỗi char trong String chiếm 2 bytes (UTF-16). Nhưng hầu hết String chỉ chứa Latin-1 characters (ASCII):
Java 8: "Hello" = char[] {H, e, l, l, o} = 5 × 2 bytes = 10 bytes data
Java 9+: "Hello" = byte[] {H, e, l, l, o} = 5 × 1 byte = 5 bytes data
| Java version | Internal storage | "Hello" data size |
|---|---|---|
| ≤ Java 8 | char[] (always UTF-16) | 10 bytes |
| ≥ Java 9 | byte[] + coder flag | 5 bytes (Latin-1) hoặc 10 bytes (UTF-16) |
Bạn không cần thay đổi code. JVM tự động chọn encoding tối ưu:
- String chỉ chứa Latin-1 → LATIN1 encoding (1 byte/char)
- String chứa non-Latin-1 → UTF16 encoding (2 bytes/char)
Tiết kiệm ~30-40% memory cho applications dùng nhiều String Latin-1.
Ví dụ thực tế: Đo Object Size
public class ObjectSizeDemo {
public static void main(String[] args) {
// Cách ước tính memory impact
Runtime runtime = Runtime.getRuntime();
runtime.gc(); // Gợi ý GC trước khi đo
long before = runtime.freeMemory();
// Tạo 100,000 Integer objects
Integer[] wrappers = new Integer[100_000];
for (int i = 0; i < 100_000; i++) {
wrappers[i] = new Integer(i + 1000); // Ngoài cache range
}
long after = runtime.freeMemory();
long used = before - after;
System.out.printf("100K Integer objects: ~%.1f MB%n", used / 1_000_000.0);
System.out.printf("Per object: ~%d bytes%n", used / 100_000);
// Kết quả: ~16 bytes per object (12 header + 4 int value)
// So sánh với int[]
runtime.gc();
before = runtime.freeMemory();
int[] primitives = new int[100_000]; // 1 array object trên Heap
after = runtime.freeMemory();
used = before - after;
System.out.printf("int[100K]: ~%.1f MB%n", used / 1_000_000.0);
// Kết quả: ~0.4 MB (16 header + 100K × 4 bytes)
}
}
Lỗi thường gặp
Lỗi 1: Dùng == cho String tạo bằng new
String a = "Hello";
String b = new String("Hello");
System.out.println(a == b); // ❌ false — khác reference
// ✅ Luôn dùng .equals() cho String
System.out.println(a.equals(b)); // true
Lỗi 2: Nghĩ mọi String đều trong Pool
String s = new String("Hello"); // KHÔNG trong Pool
// Chỉ String literals ("Hello") mới tự động vào Pool
// new String() luôn tạo object mới trên Heap
Lỗi 3: Lạm dụng intern()
// ❌ Intern mọi String — tốn CPU cho lookup
for (String line : millionsOfLines) {
processedLines.add(line.intern()); // Chậm nếu ít duplicate
}
// ✅ Chỉ intern khi biết duplicate ratio cao
// Ví dụ: country codes, status values
Bài tập
Bài 1: Tính Object Size [Cơ bản]
Tính kích thước tối thiểu (bytes) của object sau trên 64-bit JVM với Compressed Oops:
public class Product {
private long id; // ? bytes
private String name; // ? bytes (reference)
private double price; // ? bytes
private boolean active; // ? bytes
}
Xem lời giải
Mark Word: 8 bytes
Class Pointer: 4 bytes (compressed)
id (long): 8 bytes
name (ref): 4 bytes (compressed)
price (double): 8 bytes
active (boolean): 1 byte
Subtotal: 33 bytes
Padding: 7 bytes (để tổng = 40, chia hết cho 8)
Total: 40 bytes
Lưu ý: JVM có thể reorder fields để tối ưu padding.
Bài 2: String Pool Prediction [Trung bình]
Dự đoán output của chương trình sau:
String a = "Java";
String b = "Java";
String c = new String("Java");
String d = c.intern();
String e = "Ja" + "va"; // Compile-time constant
String f = "Ja";
String g = f + "va"; // Runtime concatenation
System.out.println(a == b);
System.out.println(a == c);
System.out.println(a == d);
System.out.println(a == e);
System.out.println(a == g);
Xem lời giải
a == b → true (cả hai từ String Pool)
a == c → false (c tạo bằng new → Heap riêng)
a == d → true (intern() trả về Pool reference = a)
a == e → true ("Ja" + "va" → compiler tối ưu thành "Java" → Pool)
a == g → false (f + "va" → runtime concatenation → new String)
Giải thích e: Compiler biết "Ja" + "va" là constant expression → tối ưu thành "Java" lúc compile → dùng String Pool.
Giải thích g: f là variable (không phải constant) → compiler không biết giá trị lúc compile → dùng StringBuilder lúc runtime → tạo String mới.
Bài 3: Memory Optimization [Thách thức]
Bạn có 1 triệu User objects với field country (chỉ có ~200 giá trị unique). Hiện tại tốn ~100MB cho country strings. Đề xuất và implement giải pháp giảm memory. Ước tính memory tiết kiệm được.
Gợi ý
Sử dụng String.intern() hoặc tạo country constants. Với intern: 1M duplicate strings → 200 unique strings trong Pool.
Memory tiết kiệm: ~99.98% cho country field (200 strings thay vì 1M strings).
Tóm tắt
| Khái niệm | Điểm chính |
|---|---|
| Object Header | Mark Word (8B) + Class Pointer (4B compressed) = 12 bytes minimum |
| Padding | Object size phải chia hết cho 8 bytes |
| Minimum object size | 16 bytes (12 header + 4 padding) |
| Wrapper overhead | Integer: 16B cho 4B data (4x overhead) |
| String Pool | Vùng Heap lưu String literals, tái sử dụng |
| intern() | Thêm String vào Pool thủ công |
| Compact Strings | Java 9+: byte[] thay char[] cho Latin-1 strings |
| Compressed Oops | 4-byte references trên 64-bit JVM (heap ≤ 32GB) |