Java hoạt động như thế nào?
Tài liệu này gom lại toàn bộ mạch học: JDK, JVM, bytecode, Constant Pool, Runtime Constant Pool, String Pool, Class Loading, ClassLoader, JVM Stack, Stack Frame, Heap, static/instance runtime model, Exceptions, GC, Interpreter và JIT.
Mục tiêu: không học thuộc định nghĩa rời rạc, mà hiểu cả journey từ source code đến lúc CPU thực thi.
1. Bức tranh tổng thể
Đúng: đây chính là phần lõi để hiểu cách Java chạy trên JVM.
Main.java
│
│ javac
▼
Main.class
│
├── Bytecode
└── Constant Pool
│
▼
java.exe
│
▼
JVM / HotSpot starts
│
▼
Class Loading
├── Loading
├── Linking
│ ├── Verification
│ ├── Preparation
│ └── Resolution
└── Initialization
│
▼
main() được gọi
│
▼
JVM Stack / Stack Frame
├── Local Variables
└── Operand Stack
│
▼
Bytecode Execution
├── Interpreter
└── JIT Compiler
│
▼
Native Machine Code
│
▼
CPU
Trong lúc chạy:
Heap chứa object
GC thu hồi object unreachable
String Pool quản lý interned String
2. JDK, java.exe và JVM
JDK là bộ công cụ để phát triển và chạy Java. Bạn đang dùng bản JDK 21 portable dạng ZIP là hoàn toàn đủ để học sâu.
JDK
├── bin
│ ├── javac.exe ← compile .java → .class
│ ├── java.exe ← launcher, khởi tạo JVM
│ ├── javap.exe ← disassemble / xem bytecode
│ ├── jcmd.exe
│ ├── jstack.exe
│ └── ...
├── lib
├── jmods
└── ...
java.exe không phải toàn bộ JVM. Nó là launcher. Trên Windows, phần native cốt lõi của HotSpot thường nằm trong file kiểu bin\server\jvm.dll.
Bạn chạy:
java Main
Windows tạo process java.exe
↓
java.exe khởi tạo JVM
↓
JVM load Main.class
↓
gọi Main.main(...)
java Main, JVM mới được khởi tạo trong process đó. Thông thường mỗi Java process có JVM instance riêng.3. Từ .java sang .class
Source Java là text; file .class là binary theo chuẩn JVM Class File Format.
Java source:
int a = 10;
int b = 20;
int c = a + b;
javac
↓
Bytecode (human-readable qua javap):
bipush 10
istore_1
bipush 20
istore_2
iload_1
iload_2
iadd
istore_3
javap không phải thứ JVM cần. JVM hiểu binary trực tiếp. javap chỉ giúp con người đọc dạng disassembly.
4. Constant Pool & Symbolic Reference
Mỗi class file có Constant Pool riêng. Nó chứa constants và symbolic references mà bytecode tham chiếu.
Constant pool:
#7 = String #8 // abc
#9 = Fieldref ... // java/lang/System.out
#15 = Methodref ... // PrintStream.println:(I)V
Bytecode có thể viết:
ldc #7
getstatic #9
invokevirtual #15
Ý tưởng là:
instruction
↓
tham chiếu Constant Pool
↓
biết constant / class / field / method nào đang được nói tới
Symbolic Reference
Thay vì hard-code địa chỉ memory, class file lưu thông tin kiểu:
java/io/PrintStream.println:(I)V
JVM sẽ resolve symbolic reference thành runtime representation có thể sử dụng để gọi method/field thực tế.
#7 trong Constant Pool không phải Java object, và biến Java không “trỏ vào #7”.5. String Pool
String Pool là runtime concept dành cho interned strings, đặc biệt là string literals.
String a = "abc";
String b = "abc";
Class file có thể chỉ cần một constant:
#7 = String #8 // abc
Runtime:
Class-file Constant Pool
#7 → "abc"
│
│ ldc #7
▼
Runtime resolution
│
▼
String Intern Pool
│
▼
canonical String object "abc"
▲ ▲
│ │
a b
Nhiều class khác nhau có Constant Pool riêng nhưng vẫn có thể cùng dùng một interned String:
A.class #7 → "abc" ──┐
├──► canonical String "abc"
B.class #15 → "abc" ──┘
new String("abc")
String a = "abc";
String b = new String("abc");
a tham chiếu canonical interned String. b tham chiếu một String object mới.
a ───────────────► String "abc" (interned)
b ───────────────► String "abc" (new object)
Do đó:
a == b // false
a.equals(b) // true
Nếu:
String c = b.intern();
thì c sẽ nhận canonical reference tương ứng.
new String(...) không tự động trở thành canonical pooled object.6. Primitive vs Object
Java có 8 primitive types, và chúng không phải Object:
| Primitive | Wrapper class |
|---|---|
| byte | Byte |
| short | Short |
| int | Integer |
| long | Long |
| float | Float |
| double | Double |
| char | Character |
| boolean | Boolean |
int a = 10; // primitive value
String s = "abc"; // reference tới String object
Integer x = 10; // reference tới Integer object
Bytecode cũng phản ánh khác biệt:
int:
iload / istore
object reference:
aload / astore
7. Class Loading Lifecycle
Khi JVM tìm thấy Main.class, nó không chạy ngay. Class sẽ đi qua lifecycle:
Loading
↓
Linking
├── Verification
├── Preparation
└── Resolution
↓
Initialization
Loading
Class Loader đọc binary và tạo runtime representation của class.
Verification
JVM kiểm tra class/bytecode có hợp lệ không: format, type consistency, operand stack, control flow...
Preparation
JVM chuẩn bị storage cho static fields và gán default values.
static int count = 100;
Preparation:
count = 0
Initialization:
count = 100
Resolution
Symbolic references được resolve khi cần.
"java/io/PrintStream.println:(I)V"
↓
runtime-resolved method reference
Initialization & <clinit>
Static initialization code được gom vào logic class initialization.
static int number = 100;
static {
System.out.println("Main initialized");
}
Cần phân biệt:
<init> | <clinit> |
|---|---|
| instance constructor | class initialization |
chạy khi new object | chạy khi class được initialize |
| có thể chạy nhiều lần | thường một lần / class / class loader |
8. JVM Stack & Stack Frame
Mỗi thread có JVM Stack riêng. Mỗi method invocation tạo một Stack Frame.
Thread main
↓
JVM Stack
┌────────────────────┐ ← top
│ methodB frame │
├────────────────────┤
│ methodA frame │
├────────────────────┤
│ main frame │
└────────────────────┘
Một Stack Frame quan trọng có:
Stack Frame
├── Local Variables
└── Operand Stack
Ví dụ
static int add(int a, int b) {
int result = a + b;
return result;
}
Local Variables:
slot 0 → a
slot 1 → b
slot 2 → result
Operand Stack:
[]
[a]
[a,b]
[a+b]
[]
Khi caller gọi method:
caller Operand Stack
↓ arguments
callee Local Variables
Khi method return:
callee return value
↓
caller Operand Stack
stack=2 nghĩa là gì?
Không phải số tham số lớn nhất của operation, mà là độ sâu tối đa của Operand Stack tại bất kỳ thời điểm nào trong method.
Recursion & StackOverflowError
hello()
↓
hello()
↓
hello()
↓
...
Mỗi call thêm một frame. Quá nhiều nested frames có thể gây StackOverflowError.
Instance method và this
instance method:
slot 0 → this
slot 1 → first parameter
...
static method:
không có this
9. Heap & Object Reference
Heap là vùng memory dùng chủ yếu cho object và array, chia sẻ giữa các threads.
Person p = new Person();
Stack Heap
p: ref ──────────────────► Person object
age = 0
name = null
Dòng Person p = new Person() nên tách thành hai ý:
new Person()
→ tạo object
p
→ chứa reference tới object đó
Bytecode của new
new
dup
invokespecial <init>
astore
dup duplicate reference trên Operand Stack vì JVM vừa cần reference để gọi constructor, vừa cần giữ reference lại để store vào local variable.
Primitive field vẫn có thể ở trong object
class Person {
int age;
}
age là primitive nhưng là field của object, nên nó là một phần dữ liệu của object trên Heap.
Hai biến có thể trỏ cùng một object
Person p1 = new Person();
Person p2 = p1;
p1 ───┐
├────► Person object
p2 ───┘
Java copy reference value, không copy toàn bộ object.
Java là pass-by-value
Khi truyền object vào method, Java vẫn copy một value — value đó là reference.
main frame:
p1 → ref XYZ
callee frame:
p → ref XYZ
Nếu callee sửa object qua reference thì caller thấy thay đổi. Nhưng nếu callee gán p = new Person(), caller không bị đổi reference.
10. Garbage Collection
GC quan tâm object còn reachable hay không.
GC Roots
GC bắt đầu từ những root như active stack references, static references, thread-related references, JNI/native references...
GC Roots
│
├────► A ─► B ─► C
│
└────► D
X ─► Y // không có đường từ root tới X/Y
A, B, C, D còn sống. X và Y có thể trở thành garbage.
Circular reference vẫn có thể bị GC
A ───► B
▲ │
└──────┘
Nếu không có đường từ GC Roots tới A/B, cả hai vẫn có thể bị thu hồi.
Young & Old Generation
Heap
├── Young Generation
│ ├── Eden
│ └── Survivor
└── Old Generation
Object mới thường bắt đầu ở Young. Object sống lâu có thể được promote sang Old.
Stop-The-World
Một số GC phase cần tạm dừng application threads để xử lý nhất quán. Collector hiện đại cố giảm pause và làm nhiều việc concurrently.
Memory leak vẫn có thể xảy ra trong Java
static List<Person> people = new ArrayList<>();
while (true) {
people.add(new Person());
}
Các Person vẫn reachable từ static list, nên GC không được xóa. Đây là memory leak theo nghĩa object không còn hữu ích nhưng vẫn reachable.
StackOverflow vs OutOfMemory
| Lỗi | Ý nghĩa thường gặp |
|---|---|
| StackOverflowError | quá nhiều nested stack frames |
| OutOfMemoryError | JVM không còn đủ memory cho một loại allocation nào đó; heap exhaustion là case phổ biến |
11. Interpreter & JIT Compiler
CPU không hiểu bytecode như iload, iadd, invokevirtual. JVM Execution Engine phải thực thi chúng.
Interpreter
Interpreter đọc và thực hiện bytecode instruction. Ưu điểm: có thể bắt đầu chạy nhanh, không cần compile mọi method trước.
JIT — Just-In-Time Compiler
Khi một method/code path chạy nhiều và trở thành “hot”, JVM có thể compile bytecode đó thành native machine code cho CPU hiện tại.
Bytecode
↓
Interpreter lúc đầu
↓
profiling
↓
hot code
↓
JIT compile
↓
Native code
↓
CPU
Vì sao không compile hết từ đầu?
Compile tất cả sẽ tăng startup cost và có thể phí công cho những method hầu như không chạy.
Warm-up
Ứng dụng JVM có thể chạy nhanh hơn sau một thời gian vì class đã load, references đã resolve, code hot đã được JIT compile và optimize.
JIT không chỉ “dịch”
Nó còn tối ưu dựa trên runtime profile, ví dụ method inlining, speculative optimization và deoptimization khi assumption không còn đúng.
HotSpot, C1, C2
HotSpot tập trung tối ưu các “hot spots”. Với tiered compilation, có thể có các mức compile khác nhau: C1 thiên về compile nhanh, C2 thiên về optimization sâu hơn.
Code Cache
Native code do JIT tạo được giữ trong vùng runtime như Code Cache. Vì vậy JVM memory không chỉ có Stack và Heap.
12. Runtime Constant Pool
Đây là mảnh ghép còn thiếu giữa Constant Pool trong file .class và lúc JVM thực thi class.
Main.class trên disk
└── Class-file Constant Pool
├── #7 String "abc"
├── #9 Fieldref System.out
└── #15 Methodref PrintStream.println
↓ class được load
JVM Runtime
└── Runtime Constant Pool của Main
├── runtime representation của constants
├── symbolic references
└── resolved references khi cần
Class-file Constant Pool là cấu trúc binary nằm trong file .class. Khi class được load, JVM tạo runtime representation tương ứng cho class đó.
Ví dụ bytecode:
ldc #7
getstatic #9
invokevirtual #15
Ở runtime, JVM không nhất thiết cứ quay lại đọc file .class trên disk cho mỗi instruction. Class đã được load và JVM có runtime structures để thực thi.
Class-file Constant Pool = dữ liệu trong file
.classRuntime Constant Pool = representation của constant pool khi class đã được load
String Intern Pool = nơi quản lý canonical interned String objects
Class-file Constant Pool
↓ loading
Runtime Constant Pool
↓ resolution / use
runtime entity
String literal constant
↓
String Intern Pool
↓
canonical String object
13. Metaspace, Code Cache và JVM Memory Areas
JVM memory không chỉ có Stack và Heap. Hai vùng đó rất quan trọng, nhưng HotSpot còn sử dụng nhiều vùng memory khác.
Java Process / HotSpot JVM
├── Heap
│ ├── Objects
│ └── Arrays
│
├── Thread Stacks
│ ├── main thread stack
│ ├── worker thread stack
│ └── ...
│
├── Metaspace
│ └── class metadata
│
├── Code Cache
│ └── native code do JIT compile
│
└── Native / JVM internal memory
└── runtime structures khác
Metaspace
Metaspace dùng cho metadata của các class đã được load. Ví dụ có thể nghĩ tới thông tin về class, method, field và runtime metadata liên quan.
Nó không nên được hiểu là “nơi chứa mọi static object”. Static fields vẫn là một phần của runtime state của class, còn object được reference bởi static field vẫn là object trên Heap.
Code Cache
Khi JIT compiler compile bytecode thành native machine code, phần native code đó cần được lưu ở đâu đó. HotSpot dùng Code Cache cho mục đích này.
Bytecode
↓
JIT
↓
Native machine code
↓
Code Cache
↓
CPU execute
14. ClassLoader Hierarchy
Trước đây ta nói “ClassLoader load Main.class”. Nhưng thực tế JVM có nhiều class loaders với trách nhiệm khác nhau.
Bootstrap ClassLoader
↓
Platform ClassLoader
↓
Application ClassLoader
Bootstrap ClassLoader
Load các class nền tảng của Java runtime, ví dụ những class core như java.lang.Object, java.lang.String và nhiều class hệ thống khác.
Platform ClassLoader
Load các platform modules/libraries của Java mà không thuộc phần bootstrap core nhỏ nhất.
Application ClassLoader
Thường là loader load code application của bạn từ classpath, ví dụ Main.class.
Parent Delegation
Mental model đơn giản:
Application ClassLoader muốn load "java.lang.String"
↓
hỏi parent trước
↓
Platform
↓
hỏi parent
↓
Bootstrap
↓
Bootstrap đã biết String
↓
trả class về
Ý tưởng quan trọng là class loader con thường ưu tiên hỏi parent trước thay vì tự load ngay.
Điều này giúp tránh việc application tùy tiện thay thế các core classes của Java.
15. static vs Instance ở Runtime
Đây là phần nối Class Loading với Heap.
class Counter {
static int total = 0;
int value = 0;
}
total thuộc về class-level state. value thuộc về từng object instance.
Class Counter
└── static total = 0
Heap
├── Counter object A
│ └── value = 10
│
└── Counter object B
└── value = 20
Nếu:
Counter a = new Counter();
Counter b = new Counter();
a.value = 10;
b.value = 20;
Counter.total = 2;
thì có 2 instance fields riêng nhưng chỉ một class-level static field:
Counter.total = 2
a ──► Counter object A
value = 10
b ──► Counter object B
value = 20
static object reference
static Person global = new Person();
Không nên nói “Person object nằm trong static area”. Mental model đúng hơn:
class-level static field
global: reference
│
▼
Heap
┌──────────────┐
│ Person object│
└──────────────┘
Static field giữ reference; object vẫn là object trên Heap.
Preparation vs Initialization
static int total = 100;
Preparation:
total = 0
Initialization:
total = 100
Đây là chỗ class loading lifecycle và static runtime state gặp nhau.
16. Exception và Stack Unwinding
Exception là một ví dụ rất đẹp để thấy Stack Frame thực sự được JVM xử lý thế nào.
static void methodB() {
throw new RuntimeException("boom");
}
static void methodA() {
methodB();
}
public static void main(String[] args) {
methodA();
}
Lúc methodB() throw exception:
JVM Stack
┌────────────────────┐ ← top
│ methodB frame │
├────────────────────┤
│ methodA frame │
├────────────────────┤
│ main frame │
└────────────────────┘
JVM tìm exception handler phù hợp trong method hiện tại. Nếu không có, frame bị unwound và JVM tìm ở caller.
methodB
↓ no handler
pop / unwind methodB frame
methodA
↓ no handler
pop / unwind methodA frame
main
↓ no handler
exception reaches top
↓
thread terminates with uncaught exception
try/catch
try {
methodA();
} catch (RuntimeException e) {
System.out.println("caught");
}
Compiler ghi exception handling metadata vào class file. javap -c -v có thể cho bạn thấy Exception table.
Exception table:
from to target type
... ... ... java/lang/RuntimeException
Khi exception xảy ra, JVM dùng bảng này để tìm handler phù hợp trong method.
Exception object cũng là object
throw new RuntimeException("boom");
RuntimeException là object. Nó có thể được cấp phát trên Heap giống các object Java khác.
17. Mental model cuối cùng
.java
│
│ javac
▼
.class
├── bytecode
└── constant pool
│
▼
java.exe
│
▼
JVM / HotSpot
│
├── Class Loading
│ ├── Loading
│ ├── Verification
│ ├── Preparation
│ ├── Resolution
│ └── Initialization
│
├── Thread Stacks
│ └── Stack Frames
│ ├── Local Variables
│ └── Operand Stack
│
├── Heap
│ ├── Objects
│ └── Arrays
│
├── String Intern Pool
│
├── GC
│ └── reclaim unreachable objects
│
└── Execution Engine
├── Interpreter
└── JIT
↓
Native Machine Code
↓
CPU
18. Roadmap sau phần “How Java Works”
Nếu đã hiểu tài liệu này, bạn đã có foundation khá đầy đủ về cách Java chạy trên JVM. Bước tiếp theo nên chuyển từ “JVM hoạt động thế nào” sang “Java language/runtime được xây trên JVM thế nào”.
Java Runtime Foundation
↓
static / final / object lifecycle
↓
inheritance / interface
↓
virtual dispatch
↓
exceptions
↓
collections / generics
↓
I/O / NIO
↓
threads / synchronized / volatile
↓
Java Memory Model
↓
reflection / annotations
↓
frameworks như Spring
Những phần JVM nâng cao có thể học sau
Object layout
Compressed references
TLAB
Escape analysis
Safepoints
Write barriers
G1 internals
ZGC internals
C1 / C2 deeper
Deoptimization
JNI / native methods
Java Memory Model details
19. Bài thực hành trên máy
Với JDK portable của bạn:
# compile
jdk-21_windows-x64_bin\jdk-21.0.12.1\bin\javac.exe .\src\l01\Main.java
# xem class signatures
jdk-21_windows-x64_bin\jdk-21.0.12.1\bin\javap.exe .\src\l01\Main.class
# xem bytecode
jdk-21_windows-x64_bin\jdk-21.0.12.1\bin\javap.exe -c .\src\l01\Main.class
# xem cả Constant Pool + metadata
jdk-21_windows-x64_bin\jdk-21.0.12.1\bin\javap.exe -c -v .\src\l01\Main.class
Nên thử các ví dụ sau rồi so sánh bytecode:
String a = "abc";
String b = "abc";
String c = new String("abc");
int x = 10;
int y = 20;
int z = x + y;
Person p = new Person();
static int add(int a, int b) {
return a + b;
}
Tài liệu học cá nhân — Java/JVM fundamentals.