Stack vs Heap chi tiết
Bài trước: Kiến trúc JVM — Bạn đã biết JVM có 5 Runtime Data Areas. Bài này đi sâu vào Stack và Heap — hai vùng memory quan trọng nhất mà mọi dòng code Java đều tương tác.
Stack Frame Anatomy
Mỗi method call tạo một frame trên JVM Stack. Frame gồm 3 phần:
┌─────────────────────────────────┐
│ Stack Frame │
├─────────────────────────────────┤
│ 1. Local Variables Array │ ← Chứa this, parameters, local vars
├─────────────────────────────────┤
│ 2. Operand Stack │ ← Nơi phép tính diễn ra
├─────────────────────────────────┤
│ 3. Frame Data │ ← Constant pool ref, exception table
└─────────────────────────────────┘
1. Local Variables Array
Mảng chứa tất cả local variables của method, đánh số từ slot 0:
public class Calculator {
// Instance method: slot 0 = this
public int add(int a, int b) {
int sum = a + b;
return sum;
}
}
Local Variables Array cho add(int, int):
┌──────┬──────────┬───────────────┐
│ Slot │ Tên │ Giá trị │
├──────┼──────────┼───────────────┤
│ 0 │ this │ Calculator@ │ ← Instance method → slot 0 = this
│ 1 │ a │ 10 │
│ 2 │ b │ 20 │
│ 3 │ sum │ 30 │
└──────┴──────────┴───────────────┘
this cho instance methods- Instance method: Slot 0 luôn là
this(reference đến object hiện tại) - Static method: Không có
this, parameters bắt đầu từ slot 0 longvàdoublechiếm 2 slots (vì 64-bit), các kiểu khác chiếm 1 slot
// Static method: không có this
public static long multiply(int x, double y) {
long result = (long)(x * y);
return result;
}
Local Variables Array cho static multiply(int, double):
┌──────┬──────────┬───────────────┐
│ Slot │ Tên │ Giá trị │
├──────┼──────────┼───────────────┤
│ 0 │ x │ 5 │ ← Không có this (static)
│ 1-2 │ y │ 3.14 │ ← double chiếm 2 slots
│ 3-4 │ result │ 15 │ ← long chiếm 2 slots
└──────┴──────────┴───────────────┘
2. Operand Stack
Operand Stack là nơi phép tính diễn ra — JVM là stack-based machine:
int result = 3 + 5 * 2;
Bytecode instructions: Operand Stack:
iconst_3 [3]
iconst_5 [3, 5]
iconst_2 [3, 5, 2]
imul (5 * 2) [3, 10]
iadd (3 + 10) [13]
istore_1 (result = 13) []
Hiểu operand stack giải thích tại sao tất cả phép tính trong Java đều diễn ra trên stack — kể cả so sánh, method call, type casting. Đây cũng là lý do bytecode trông khác xa source code.
3. Frame Data
Chứa thông tin hỗ trợ:
- Reference to Constant Pool: Tham chiếu đến constant pool của class
- Exception Table: Ánh xạ try-catch blocks
- Return address: Nơi trả về khi method kết thúc
Biến lưu trữ ở đâu?
Đây là câu hỏi quan trọng nhất — mỗi loại biến nằm ở vùng memory khác nhau:
public class MemoryLocation {
// Static field → Method Area (Metaspace)
static int globalCount = 0;
// Instance field → Heap (bên trong object)
private String name;
private int age;
public void process() {
// Primitive local → Stack (Local Variables Array)
int temp = 42;
// Object reference local → Stack (reference)
// Actual object → Heap
String message = new String("Hello");
// Array reference → Stack
// Array data → Heap
int[] numbers = {1, 2, 3};
}
}
| Loại biến | Memory location | Ví dụ |
|---|---|---|
| Primitive local variable | Stack (Local Variables Array) | int temp = 42; |
| Object reference (local) | Stack (reference) + Heap (object) | String s = "hello"; |
| Instance field | Heap (bên trong object) | this.name |
| Static field | Method Area (Metaspace) | static int count |
| Array | Stack (reference) + Heap (data) | int[] arr = new int[10]; |
| Method parameter | Stack (copy vào Local Variables Array) | void foo(int x) |
Pass-by-Value giải thích bằng Stack Frame
Java luôn pass-by-value — hiểu Stack Frame giải thích tại sao:
public class PassByValue {
public static void main(String[] args) {
int num = 10;
changeValue(num);
System.out.println(num); // Vẫn 10!
int[] arr = {1, 2, 3};
changeArray(arr);
System.out.println(arr[0]); // 99! Tại sao?
}
static void changeValue(int x) {
x = 99; // Thay đổi copy trên frame mới, không ảnh hưởng num
}
static void changeArray(int[] a) {
a[0] = 99; // a và arr cùng trỏ đến array trên Heap → thay đổi thấy được
}
}
main() frame: changeValue() frame:
┌────────────────┐ ┌────────────────┐
│ num = 10 │──── copy ───▶│ x = 10 → 99 │
│ arr → [1,2,3]@ │ └────────────────┘
└────────────────┘
changeArray() frame:
┌────────────────┐
copy │ a → [1,2,3]@ │ ← Cùng reference!
──────▶│ │
└────────────────┘
Heap: [1,2,3] → [99,2,3] ← Cả arr và a trỏ đến cùng object
Java KHÔNG có pass-by-reference. Khi truyền object vào method, reference được copy (không phải object). Method nhận bản copy của reference, trỏ đến cùng object trên Heap. Nên thay đổi nội dung object thì thấy được, nhưng gán reference mới thì không:
static void reassign(int[] a) {
a = new int[]{99, 99, 99}; // Gán reference mới cho COPY
// Không ảnh hưởng reference gốc ở main
}
TLAB — Thread-Local Allocation Buffers
Heap là shared memory → nhiều threads allocate cùng lúc → cần synchronization → chậm.
JVM giải quyết bằng TLAB — cấp vùng Heap riêng cho mỗi thread:
Heap:
┌─────────────────────────────────────────────────┐
│ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
│ │ TLAB T1 │ │ TLAB T2 │ │ Shared area │ │
│ │(Thread 1)│ │(Thread 2)│ │ (large objs) │ │
│ └──────────┘ └──────────┘ └──────────────┘ │
└─────────────────────────────────────────────────┘
| Đặc điểm | Giải thích |
|---|---|
| Không cần lock | Thread allocate trong TLAB riêng → không cần synchronize |
| Nhanh | Allocation = tăng pointer, O(1) — gần như nhanh bằng stack allocation |
| Tự động | JVM quản lý TLAB transparently |
| Small objects | TLAB cho objects nhỏ; objects lớn allocate trực tiếp trên shared heap |
new trong Java nhanhNhiều người nghĩ "tạo object luôn chậm". Thực tế, nhờ TLAB + JIT escape analysis, object allocation trong Java rất nhanh — chỉ ~10 nanoseconds cho small objects.
StackOverflowError vs OutOfMemoryError
Hai lỗi memory phổ biến nhất — từ hai vùng khác nhau:
| StackOverflowError | OutOfMemoryError | |
|---|---|---|
| Vùng memory | JVM Stack (per-thread) | Heap (shared) |
| Nguyên nhân chính | Recursion quá sâu | Tạo quá nhiều objects / memory leak |
| Scope | Chỉ ảnh hưởng 1 thread | Ảnh hưởng toàn bộ JVM |
| Fix | Thêm base case, giảm recursion depth | Tăng -Xmx, fix memory leak |
| JVM flag | -Xss (stack size per thread) | -Xmx (max heap size) |
StackOverflowError — Demo
public class StackOverflowDemo {
static int depth = 0;
public static void recursive() {
depth++;
recursive(); // Mỗi call = 1 frame → stack đầy
}
public static void main(String[] args) {
try {
recursive();
} catch (StackOverflowError e) {
System.out.println("Stack overflow at depth: " + depth);
// Thường ~ 5000-20000 tuỳ frame size và -Xss
}
}
}
OutOfMemoryError — Demo
public class OOMDemo {
public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
try {
while (true) {
list.add(new byte[1024 * 1024]); // 1MB mỗi lần
System.out.println("Allocated: " + list.size() + " MB");
}
} catch (OutOfMemoryError e) {
System.out.println("OOM after: " + list.size() + " MB");
}
}
}
Method Call Flow — Từ góc nhìn Memory
public class MethodCallFlow {
public static void main(String[] args) { // Step 1
User user = new User("Java"); // Step 2
String greeting = greet(user); // Step 3
System.out.println(greeting); // Step 6
}
static String greet(User u) { // Step 4
return "Hello, " + u.getName(); // Step 5
}
}
Step 1: main() frame pushed to Stack
Step 2: User object created on Heap, reference stored in main frame
Step 3: greet() frame pushed to Stack, reference COPIED to parameter 'u'
Step 4: greet() executing — u points to same User on Heap
Step 5: New String "Hello, Java" created on Heap, returned
Step 6: greet() frame POPPED, local var 'u' gone
Returned String reference stored in main frame
Lỗi thường gặp
Lỗi 1: Nghĩ Java có pass-by-reference
void swap(Integer a, Integer b) {
Integer temp = a;
a = b; // Chỉ thay đổi local copies
b = temp; // Không ảnh hưởng caller
}
// ❌ KHÔNG hoạt động — Java luôn pass-by-value
Lỗi 2: Recursion thiếu base case
// ❌ StackOverflowError
int fibonacci(int n) {
return fibonacci(n-1) + fibonacci(n-2); // Không có base case
}
// ✅ Có base case
int fibonacci(int n) {
if (n <= 1) return n;
return fibonacci(n-1) + fibonacci(n-2);
}
Lỗi 3: Nhầm lẫn Stack vs Heap cho local objects
void method() {
String s = "hello";
// s (reference) → Stack
// "hello" (String object) → Heap (String Pool)
// KHÔNG PHẢI: cả s và "hello" đều trên Stack
}
Bài tập
Bài 1: Xác định Memory Location [Cơ bản]
Cho đoạn code sau, xác định mỗi biến nằm ở Stack, Heap, hay Method Area:
public class Shop {
static String storeName = "JavaShop";
private List<String> items;
public void addItem(String item) {
int count = items.size();
items.add(item);
}
}
Xem lời giải
storeName→ Method Area (static field)"JavaShop"→ Heap (String Pool)items(field declaration) → Heap (inside Shop object)items(actual List object) → Heapitem(parameter, in addItem) → Stack (Local Variables Array, slot 1)count→ Stack (Local Variables Array, slot 2)this→ Stack (slot 0 of addItem frame, reference to Shop object on Heap)
Bài 2: Stack Frame Trace [Trung bình]
Vẽ trạng thái JVM Stack tại thời điểm method c() đang thực thi:
public class StackTrace {
public static void main(String[] args) {
a(5);
}
static void a(int x) { b(x * 2); }
static void b(int y) { c(y + 1); }
static void c(int z) { System.out.println(z); }
}
Xem lời giải
JVM Stack khi c() đang thực thi:
│ c(z=11) │ ← Top (đang in z)
├──────────────────────┤
│ b(y=10) │ ← y = 5*2 = 10
├──────────────────────┤
│ a(x=5) │ ← x = 5
├──────────────────────┤
│ main(args=[]) │ ← Bottom
└──────────────────────┘
Output: 11
Bài 3: Memory Leak Analysis [Thách thức]
Phân tích đoạn code sau: (a) Nó sẽ gây lỗi gì? (b) Tại sao GC không giải quyết được? (c) Cách fix?
public class Cache {
private static final Map<String, byte[]> cache = new HashMap<>();
public static void store(String key, byte[] data) {
cache.put(key, data); // Chỉ put, không bao giờ remove
}
// Được gọi liên tục với key mới
public static void processRequest(String requestId) {
byte[] data = new byte[1024 * 100]; // 100KB
store(requestId, data);
}
}
Xem lời giải
(a) OutOfMemoryError: Java heap space — Heap sẽ đầy sau đủ nhiều requests.
(b) GC không giải quyết được vì cache là static final → luôn reachable. Mọi entries trong map đều reachable qua cache → GC không thể collect chúng.
(c) Các cách fix:
// Fix 1: Giới hạn size
public static void store(String key, byte[] data) {
if (cache.size() > 10000) {
cache.clear(); // Hoặc evict oldest
}
cache.put(key, data);
}
// Fix 2: Dùng WeakHashMap — GC có thể collect entries khi key không còn reference
private static final Map<String, byte[]> cache = new WeakHashMap<>();
// Fix 3: Dùng cache library (Caffeine, Guava Cache) với eviction policy
Tóm tắt
| Khái niệm | Điểm chính |
|---|---|
| Stack Frame | 3 phần: Local Variables Array + Operand Stack + Frame Data |
| Slot 0 | this cho instance methods, parameter đầu tiên cho static methods |
| Primitive locals | Trên Stack (Local Variables Array) |
| Object locals | Reference trên Stack, object trên Heap |
| Static fields | Method Area (Metaspace) |
| TLAB | Thread-local allocation buffer — giúp allocation nhanh không cần lock |
| Pass-by-value | Java luôn copy value (primitive) hoặc reference (object) |
| StackOverflowError | Stack đầy — recursion quá sâu |
| OutOfMemoryError | Heap đầy — quá nhiều objects hoặc memory leak |