ClassLoader & JIT Compilation
Bài trước: Bên trong Collections — Bạn đã biết cấu trúc nội bộ của ArrayList, HashMap. Bài này giải thích cách JVM load classes và tại sao Java "khởi động chậm nhưng chạy nhanh".
ClassLoader là gì?
ClassLoader chịu trách nhiệm tìm, load, và liên kết .class files vào JVM runtime. Mỗi khi bạn dùng một class, ClassLoader đảm bảo nó sẵn sàng.
// Khi JVM gặp dòng này lần đầu:
ArrayList<String> list = new ArrayList<>();
// ClassLoader phải:
// 1. Tìm java/util/ArrayList.class
// 2. Load bytecode vào memory
// 3. Verify, prepare, resolve
// 4. Initialize (chạy static initializers)
ClassLoader Hierarchy
JVM có 3 ClassLoaders mặc định, hoạt động theo delegation model (ủy quyền từ dưới lên):
┌─────────────────────────────┐
│ Bootstrap ClassLoader │ ← Load core Java: java.lang.*, java.util.*
│ (Native code, no parent) │ Từ: $JAVA_HOME/lib/modules
├─────────────────────────────┤
│ Platform ClassLoader │ ← Load platform modules
│ (parent: Bootstrap) │ Từ: Java platform modules
├─────────────────────────────┤
│ Application ClassLoader │ ← Load application classes
│ (parent: Platform) │ Từ: classpath (-cp, -classpath)
└─────────────────────────────┘
Delegation Model (Parent-First)
Khi cần load một class, ClassLoader ủy quyền lên parent trước:
Request: Load "com.myapp.User"
Application CL: "Tôi cần com.myapp.User"
│
▼ delegate to parent
Platform CL: "Tôi không có, hỏi parent"
│
▼ delegate to parent
Bootstrap CL: "Tôi không có" → trả về null
│
▼ back to child
Platform CL: "Tôi cũng không có" → trả về null
│
▼ back to child
Application CL: "Tôi tìm trong classpath... → FOUND! Load it."
Bảo mật: Ngăn application code ghi đè core classes. Bạn không thể tạo class java.lang.String riêng để thay thế String gốc — Bootstrap CL luôn load String gốc trước.
Ví dụ thực tế
public class ClassLoaderDemo {
public static void main(String[] args) {
// String → Bootstrap ClassLoader (null vì native)
System.out.println(String.class.getClassLoader());
// Output: null
// Application class → Application ClassLoader
System.out.println(ClassLoaderDemo.class.getClassLoader());
// Output: jdk.internal.loader.ClassLoaders$AppClassLoader@...
// Parent chain
ClassLoader cl = ClassLoaderDemo.class.getClassLoader();
while (cl != null) {
System.out.println(cl);
cl = cl.getParent();
}
// Output:
// jdk.internal.loader.ClassLoaders$AppClassLoader@...
// jdk.internal.loader.ClassLoaders$PlatformClassLoader@...
// (null = Bootstrap CL)
}
}
Class Lifecycle
Từ .class file đến sử dụng, class trải qua 5 phases:
1. Loading
ClassLoader tìm và đọc .class file → tạo Class object trong Method Area:
// JVM tạo Class object cho mỗi class được load
Class<?> userClass = Class.forName("com.myapp.User");
// Hoặc implicit khi dùng lần đầu:
User user = new User(); // Trigger loading nếu chưa load
2. Linking
| Phase | Mục đích |
|---|---|
| Verify | Kiểm tra bytecode hợp lệ (format, type safety, stack consistency) |
| Prepare | Cấp memory cho static fields, set default values (0, null, false) |
| Resolve | Chuyển symbolic references → direct references (lazy hoặc eager) |
public class Example {
static int count; // Prepare: count = 0 (default)
static String name; // Prepare: name = null (default)
}
3. Initialization
Chạy static initializers và static field assignments — đúng 1 lần, thread-safe:
public class Config {
// Bước Prepare: DB_URL = null
// Bước Initialize: DB_URL = "jdbc:mysql://..."
static String DB_URL = "jdbc:mysql://localhost:3306/mydb";
// Static initializer block
static {
System.out.println("Config class initialized!");
// Chạy đúng 1 lần, thread-safe (JVM đảm bảo)
}
}
Class chỉ được initialize khi thực sự cần (first active use):
new ClassName()- Truy cập static field (không phải final constant)
- Gọi static method
- Reflection:
Class.forName("...") - Subclass initialization trigger parent initialization
Không trigger: Chỉ khai báo reference type, truy cập final static compile-time constant.
ClassNotFoundException vs NoClassDefFoundError
Hai lỗi khác nhau, từ hai phases khác nhau:
| ClassNotFoundException | NoClassDefFoundError | |
|---|---|---|
| Type | Checked Exception | Error |
| Khi nào | Loading — class không tìm thấy | Linking/Initialization — class tìm thấy nhưng không load được |
| Nguyên nhân | Thiếu JAR trong classpath | Class tìm thấy nhưng dependency thiếu, hoặc static initializer throw exception |
| Fix | Thêm JAR vào classpath | Kiểm tra dependencies và static initializers |
// ClassNotFoundException — class không tồn tại
try {
Class.forName("com.nonexistent.MyClass");
} catch (ClassNotFoundException e) {
// "com.nonexistent.MyClass" không có trong classpath
}
// NoClassDefFoundError — class có nhưng initialization fail
public class Broken {
static {
if (true) throw new RuntimeException("Init failed!");
}
}
// Lần đầu dùng Broken → ExceptionInInitializerError
// Lần sau dùng Broken → NoClassDefFoundError
JIT Compilation
Tại sao Java "khởi động chậm nhưng chạy nhanh"?
Khởi động: Interpreter thực thi bytecode → CHẬM (mỗi instruction phải interpret)
Sau warmup: JIT compile hot code → native machine code → NHANH (gần C/C++)
Tiered Compilation (Mặc định từ JDK 8)
JVM dùng 5 tiers optimization:
// Method này ban đầu interpreted
public int compute(int x) {
return x * x + x;
}
// Sau ~10,000 calls → C1 compile
// Sau ~100,000 calls → C2 compile → gần tốc độ native
- Compile tốn thời gian — C2 compilation có thể mất seconds
- Không phải method nào cũng "hot" — compile code chỉ chạy 1 lần = lãng phí
- Interpreter thu thập profiling data — giúp C2 optimize tốt hơn (biết branch nào thường true)
Method Inlining
JIT thay thế method call bằng method body — giảm call overhead:
// Trước inlining:
public int calculate(int x) {
return square(x) + x;
}
private int square(int n) {
return n * n;
}
// Sau JIT inlining:
public int calculate(int x) {
return x * x + x; // square() body được inline
// Không còn method call overhead
// Tiếp tục tối ưu: x*x + x có thể optimize thêm
}
Escape Analysis
JVM phân tích xem object có "thoát" (escape) khỏi method không:
public int computeLength(String s) {
// StringBuilder KHÔNG escape khỏi method
StringBuilder sb = new StringBuilder();
sb.append(s);
sb.append(s.length());
return sb.toString().length();
}
Nếu object không escape:
- Stack Allocation: Allocate trên stack thay heap → không cần GC
- Scalar Replacement: Thay object bằng individual fields trên stack
- Lock Elimination: Bỏ synchronization nếu object chỉ 1 thread dùng
Không có Escape Analysis:
new StringBuilder() → Heap → GC later
Với Escape Analysis:
StringBuilder fields → Stack → tự mất khi method return
→ Không tạo garbage, không cần GC
Dead Code Elimination
JIT loại bỏ code không bao giờ chạy:
public void process(boolean debug) {
// Nếu JIT thấy debug LUÔN false:
if (debug) {
System.out.println("Debug info..."); // Loại bỏ hoàn toàn
}
doWork();
}
// Sau JIT: process() chỉ còn doWork()
Loop Unrolling
Giảm loop overhead bằng cách "mở" nhiều iterations:
// Trước:
for (int i = 0; i < 4; i++) {
sum += array[i];
}
// Sau JIT unrolling:
sum += array[0];
sum += array[1];
sum += array[2];
sum += array[3];
// Không còn loop counter, không branch prediction miss
Lỗi thường gặp
Lỗi 1: Nhầm ClassNotFoundException và NoClassDefFoundError
// ClassNotFoundException = class không tìm thấy (loading phase)
// NoClassDefFoundError = class tìm thấy nhưng initialization fail
// Fix: Đọc error message, check classpath VÀ static initializers
Lỗi 2: Benchmark sai vì không warmup JIT
// ❌ Đo thời gian lần chạy đầu tiên
long start = System.nanoTime();
compute(1000); // Interpreter, chưa JIT
long time = System.nanoTime() - start; // Kết quả chậm gấp 10-100x!
// ✅ Warmup trước khi đo
for (int i = 0; i < 100_000; i++) compute(i); // JIT warmup
long start = System.nanoTime();
compute(1000); // Đã JIT compiled
long time = System.nanoTime() - start;
Lỗi 3: Nghĩ static initializer chạy khi class load
// Static initializer chạy khi class INITIALIZE, không phải load
// Class có thể load mà chưa initialize (lazy initialization)
Bài tập
Bài 1: ClassLoader Chain [Cơ bản]
Viết chương trình in ra ClassLoader chain cho: String, ArrayList, và class bạn viết. Giải thích output.
Xem lời giải
public class CLDemo {
public static void main(String[] args) {
printLoaderChain("String", String.class.getClassLoader());
printLoaderChain("ArrayList", java.util.ArrayList.class.getClassLoader());
printLoaderChain("CLDemo", CLDemo.class.getClassLoader());
}
static void printLoaderChain(String name, ClassLoader cl) {
System.out.println(name + " loader chain:");
while (cl != null) {
System.out.println(" → " + cl);
cl = cl.getParent();
}
System.out.println(" → Bootstrap (null)");
System.out.println();
}
}
Output:
String loader chain:
→ Bootstrap (null) // Core class, loaded by Bootstrap
ArrayList loader chain:
→ Bootstrap (null) // Core class
CLDemo loader chain:
→ AppClassLoader@... // Application classpath
→ PlatformClassLoader@... // Platform
→ Bootstrap (null) // Root
Bài 2: Static Initialization Order [Trung bình]
Dự đoán output:
class Parent {
static { System.out.println("Parent static init"); }
{ System.out.println("Parent instance init"); }
Parent() { System.out.println("Parent constructor"); }
}
class Child extends Parent {
static { System.out.println("Child static init"); }
{ System.out.println("Child instance init"); }
Child() { System.out.println("Child constructor"); }
}
public class Main {
public static void main(String[] args) {
System.out.println("Creating first Child:");
new Child();
System.out.println("\nCreating second Child:");
new Child();
}
}
Xem lời giải
Creating first Child:
Parent static init ← Parent class initialized first (1 lần duy nhất)
Child static init ← Child class initialized (1 lần duy nhất)
Parent instance init ← Parent instance initializer
Parent constructor ← Parent constructor
Child instance init ← Child instance initializer
Child constructor ← Child constructor
Creating second Child:
Parent instance init ← Static init KHÔNG chạy lại
Parent constructor
Child instance init
Child constructor
Static initializers chạy đúng 1 lần khi class initialize. Instance initializers chạy mỗi lần new.
Bài 3: JIT Warmup Benchmark [Thách thức]
Viết benchmark so sánh thời gian của cùng một method: (a) cold run (lần đầu), (b) sau 1000 warmup calls, (c) sau 100,000 warmup calls. Giải thích sự khác biệt bằng tiered compilation.
Gợi ý
Dùng một method tính toán phức tạp vừa (sort array, string processing). Đo System.nanoTime(). Chạy nhiều rounds, lấy trung bình. Kỳ vọng: (c) nhanh nhất vì C2 đã optimize.
Tóm tắt
| Khái niệm | Điểm chính |
|---|---|
| ClassLoader | 3 tầng: Bootstrap → Platform → Application |
| Delegation | Child delegate lên parent trước, load cuối cùng |
| Class Lifecycle | Loading → Verify → Prepare → Resolve → Initialize |
| Lazy Loading | Class initialize khi first active use |
| JIT Tiered | Interpreter → C1 (basic) → C2 (aggressive) |
| Method Inlining | Thay method call bằng body, giảm overhead |
| Escape Analysis | Object không escape → stack allocation, no GC |
| CNFE vs NCDFE | ClassNotFound = missing class; NoClassDef = init failed |