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

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 StackHeap — 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 │
└──────┴──────────┴───────────────┘
Slot 0 = 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
  • longdouble chiế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) []
Tại sao cần biết Operand Stack?

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ếnMemory locationVí dụ
Primitive local variableStack (Local Variables Array)int temp = 42;
Object reference (local)Stack (reference) + Heap (object)String s = "hello";
Instance fieldHeap (bên trong object)this.name
Static fieldMethod Area (Metaspace)static int count
ArrayStack (reference) + Heap (data)int[] arr = new int[10];
Method parameterStack (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
Pass-by-Value cho Objects

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ểmGiải thích
Không cần lockThread allocate trong TLAB riêng → không cần synchronize
NhanhAllocation = tăng pointer, O(1) — gần như nhanh bằng stack allocation
Tự độngJVM quản lý TLAB transparently
Small objectsTLAB cho objects nhỏ; objects lớn allocate trực tiếp trên shared heap
TLAB giải thích tại sao new trong Java nhanh

Nhiề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:

StackOverflowErrorOutOfMemoryError
Vùng memoryJVM Stack (per-thread)Heap (shared)
Nguyên nhân chínhRecursion quá sâuTạo quá nhiều objects / memory leak
ScopeChỉ ảnh hưởng 1 threadẢnh hưởng toàn bộ JVM
FixThêm base case, giảm recursion depthTă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 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
  • storeNameMethod Area (static field)
  • "JavaShop"Heap (String Pool)
  • items (field declaration) → Heap (inside Shop object)
  • items (actual List object) → Heap
  • item (parameter, in addItem) → Stack (Local Variables Array, slot 1)
  • countStack (Local Variables Array, slot 2)
  • thisStack (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ì cachestatic 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 Frame3 phần: Local Variables Array + Operand Stack + Frame Data
Slot 0this cho instance methods, parameter đầu tiên cho static methods
Primitive localsTrên Stack (Local Variables Array)
Object localsReference trên Stack, object trên Heap
Static fieldsMethod Area (Metaspace)
TLABThread-local allocation buffer — giúp allocation nhanh không cần lock
Pass-by-valueJava luôn copy value (primitive) hoặc reference (object)
StackOverflowErrorStack đầy — recursion quá sâu
OutOfMemoryErrorHeap đầy — quá nhiều objects hoặc memory leak

Đọc thêm