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

Kiến trúc JVM & Runtime Data Areas

Hiểu kiến trúc JVM giúp bạn giải thích tại sao code Java chạy như vậy — từ StackOverflowError vs OutOfMemoryError, đến tại sao Java "khởi động chậm nhưng chạy nhanh". Bài này cung cấp bản đồ tổng thể trước khi đi sâu vào từng thành phần.

JVM là gì?

JVM (Java Virtual Machine) là máy ảo thực thi Java bytecode. Đây là lý do Java "Write Once, Run Anywhere" — code compile thành bytecode chung, JVM trên mỗi nền tảng (Windows, Linux, macOS) biết cách thực thi bytecode đó.

Quá trình từ .java đến thực thi

Main.java
public class Main {
public static void main(String[] args) {
String greeting = "Hello, JVM!";
System.out.println(greeting);
}
}

Kiến trúc JVM — 3 thành phần chính

┌──────────────────────────────────────────────────────┐
│ JVM Architecture │
├──────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ │
│ │ ClassLoader │ Load .class files │
│ │ Subsystem │ (Bootstrap → Platform → App) │
│ └──────┬───────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────┐ │
│ │ Runtime Data Areas │ │
│ │ │ │
│ │ Per-Thread: Shared: │ │
│ │ ┌──────────┐ ┌──────────────┐ │ │
│ │ │ PC Register│ │ Heap │ │ │
│ │ ├──────────┤ ├──────────────┤ │ │
│ │ │ JVM Stack │ │ Method Area │ │ │
│ │ ├──────────┤ │(Metaspace) │ │ │
│ │ │Native Meth│ └──────────────┘ │ │
│ │ │ Stack │ │ │
│ │ └──────────┘ │ │
│ └──────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ Execution │ Interpreter + JIT Compiler │
│ │ Engine │ + Garbage Collector │
│ └──────────────┘ │
│ │
└──────────────────────────────────────────────────────┘

1. ClassLoader Subsystem

ClassLoader chịu trách nhiệm tìm và load .class files vào JVM:

// Khi bạn viết:
import java.util.ArrayList;
List<String> list = new ArrayList<>();

// ClassLoader tìm và load:
// 1. Bootstrap CL: java.util.ArrayList (core Java)
// 2. Application CL: your classes (classpath)

Chi tiết ClassLoader sẽ được trình bày ở bài ClassLoader & JIT.

2. Runtime Data Areas — 5 vùng memory

Đây là phần quan trọng nhất — nơi mọi dữ liệu chương trình tồn tại:

Per-Thread (Mỗi thread có riêng)

VùngMục đíchLỗi liên quan
PC RegisterĐịa chỉ bytecode instruction đang thực thi
JVM StackStack frames cho mỗi method callStackOverflowError
Native Method StackCho native methods (JNI — gọi C/C++)StackOverflowError

Shared (Tất cả threads dùng chung)

VùngMục đíchLỗi liên quan
HeapTất cả objects và arraysOutOfMemoryError: Java heap space
Method AreaClass metadata, static fields, constant poolOutOfMemoryError: Metaspace

3. Execution Engine

Execution Engine thực thi bytecode:

ComponentVai trò
InterpreterĐọc bytecode → thực thi từng instruction (chậm nhưng bắt đầu ngay)
JIT CompilerCompile hot methods → native code (nhanh sau warmup)
Garbage CollectorTự động dọn dẹp objects không còn sử dụng trên Heap

PC Register (Program Counter)

Mỗi thread có PC Register riêng — lưu địa chỉ của bytecode instruction đang thực thi.

public void example() {
int a = 10; // PC → instruction #0
int b = 20; // PC → instruction #2
int sum = a + b; // PC → instruction #4
System.out.println(sum); // PC → instruction #7
}
Tại sao mỗi thread cần PC riêng?

Khi JVM chuyển giữa các threads (context switch), cần biết mỗi thread đang thực thi ở đâu để tiếp tục. PC Register lưu trữ vị trí này.

JVM Stack

Mỗi thread có 1 JVM Stack riêng, chứa các frames — mỗi method call tạo 1 frame mới:

public class StackDemo {
public static void main(String[] args) { // Frame 1: main
int result = calculate(10, 20); // Frame 2: calculate (push)
System.out.println(result); // Frame 2 popped, back to Frame 1
}

static int calculate(int a, int b) { // Frame 2
return add(a, b); // Frame 3: add (push)
}

static int add(int x, int y) { // Frame 3
return x + y; // Frame 3 popped
}
}
JVM Stack (Thread main):

│ Frame 3: add(x=10, y=20) │ ← Top (đang thực thi)
├────────────────────────────────┤
│ Frame 2: calculate(a=10, b=20)│
├────────────────────────────────┤
│ Frame 1: main(args=[]) │ ← Bottom
└────────────────────────────────┘

StackOverflowError

Khi stack đầy (quá nhiều frames) — thường do recursion không có base case:

// ❌ StackOverflowError — infinite recursion
public static int factorial(int n) {
return n * factorial(n - 1); // Không có base case!
// Mỗi call = 1 frame → stack đầy → StackOverflowError
}

// ✅ Có base case
public static int factorial(int n) {
if (n <= 1) return 1; // Base case — stop recursion
return n * factorial(n - 1);
}

Mặc định JVM stack size là 512KB–1MB (tuỳ JVM). Có thể điều chỉnh: java -Xss2m Main.

Heap

Heap là vùng memory lớn nhất, dùng chung bởi tất cả threads. Mọi objects và arrays đều nằm trên Heap:

public class HeapDemo {
// Instance field → trong object → trên Heap
private String name;

public void demo() {
// Object → Heap
HeapDemo obj = new HeapDemo(); // Object trên Heap

// Array → Heap
int[] numbers = new int[100]; // Array trên Heap

// String object → Heap (String Pool cũng trên Heap)
String text = new String("Hello"); // String object trên Heap

// Primitive local variable → Stack (KHÔNG phải Heap)
int count = 42; // Trên Stack frame
}
}

OutOfMemoryError: Java heap space

Khi Heap đầy và GC không thể giải phóng đủ memory:

// ❌ OutOfMemoryError — tạo quá nhiều objects
List<byte[]> leak = new ArrayList<>();
while (true) {
leak.add(new byte[1024 * 1024]); // 1MB mỗi iteration
// List giữ reference → GC không thể collect → Heap đầy
}

Heap size mặc định phụ thuộc vào JVM và hệ thống. Điều chỉnh:

  • -Xms256m: Initial heap size
  • -Xmx1g: Maximum heap size

Method Area (Metaspace)

Method Area lưu trữ metadata của class — thông tin về class, không phải objects:

Nội dungVí dụ
Class structureTên class, superclass, interfaces
Field infoTên, type, access modifiers
Method infoTên, parameters, bytecode
Constant PoolString literals, numeric constants
Static fieldsstatic int count = 0
public class User {
// Static field → Method Area
private static int totalUsers = 0;

// Instance field info → Method Area (metadata)
// Instance field value → Heap (trong object)
private String name;

// Method bytecode → Method Area
public String getName() {
return name;
}
}
Metaspace (từ Java 8)

Trước Java 8: Method Area = PermGen (fixed size, dễ bị OutOfMemoryError: PermGen space). Từ Java 8: Method Area = Metaspace (sử dụng native memory, tự động mở rộng). Điều chỉnh: -XX:MaxMetaspaceSize=256m

Ví dụ tổng hợp: Theo dõi Memory

public class MemoryDemo {
static int counter = 0; // Method Area (static field)

public static void main(String[] args) { // Frame trên JVM Stack
String name = "Java"; // Stack: reference, Heap: String Pool
User user = new User(name); // Stack: reference, Heap: User object
counter++; // Method Area: counter = 1

processUser(user); // New frame on Stack
}

static void processUser(User u) { // Frame mới trên Stack
String info = u.toString(); // Stack: reference, Heap: new String
System.out.println(info); // info eligible for GC after method returns
} // Frame popped, local vars gone
}
Method Area:                    Heap:
┌──────────────────┐ ┌──────────────────┐
│ MemoryDemo class │ │ User object │
│ counter = 1 │ │ name → "Java" │
│ main() bytecode │ ├──────────────────┤
│ processUser() │ │ "Java" (Pool) │
│ bytecode │ ├──────────────────┤
│ User class info │ │ String from │
└──────────────────┘ │ toString() │
└──────────────────┘
JVM Stack (main thread):
┌──────────────────┐
│ processUser() │
│ u → User@heap │
│ info → String@ │
├──────────────────┤
│ main() │
│ name → "Java"@ │
│ user → User@ │
└──────────────────┘

Tóm tắt

ComponentVai tròScope
ClassLoaderLoad .class files vào JVMSystem
PC RegisterĐịa chỉ instruction đang thực thiPer-thread
JVM StackMethod call frames (local vars, operand stack)Per-thread
Native Method StackCho native methods (JNI)Per-thread
HeapTất cả objects và arraysShared
Method AreaClass metadata, static fields, constant poolShared
InterpreterThực thi bytecode (chậm, start nhanh)Execution Engine
JIT CompilerCompile hot code → native (nhanh sau warmup)Execution Engine
GCDọn dẹp objects không dùng trên HeapExecution Engine

Đọc thêm