Chuyển tới nội dung chính

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 fieldNội dung
Identity hash codeKết quả System.identityHashCode()
GC ageSố lần object sống sót qua GC (dùng để promote sang Old Gen)
Lock stateBiased 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
Compressed Oops (Ordinary Object Pointers)

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 sắp xếp lại fields

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ể:

TypePrimitive sizeWrapper sizeOverhead
int / Integer4 bytes16 bytes4x
long / Long8 bytes24 bytes3x
boolean / Boolean1 byte16 bytes16x
Empty Object vẫn tốn 16 bytes
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 nào dùng intern()?
  • 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 versionString Pool location
Java 6 trở về trướcPermGen (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 versionInternal storage"Hello" data size
≤ Java 8char[] (always UTF-16)10 bytes
≥ Java 9byte[] + coder flag5 bytes (Latin-1) hoặc 10 bytes (UTF-16)
Compact Strings tự động

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

ObjectSizeDemo.java
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 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 HeaderMark Word (8B) + Class Pointer (4B compressed) = 12 bytes minimum
PaddingObject size phải chia hết cho 8 bytes
Minimum object size16 bytes (12 header + 4 padding)
Wrapper overheadInteger: 16B cho 4B data (4x overhead)
String PoolVùng Heap lưu String literals, tái sử dụng
intern()Thêm String vào Pool thủ công
Compact StringsJava 9+: byte[] thay char[] cho Latin-1 strings
Compressed Oops4-byte references trên 64-bit JVM (heap ≤ 32GB)

Đọc thêm