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

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 classestạ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."
Tại sao Parent-First?

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

PhaseMục đích
VerifyKiểm tra bytecode hợp lệ (format, type safety, stack consistency)
PrepareCấp memory cho static fields, set default values (0, null, false)
ResolveChuyể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 initializersstatic 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)
}
}
Lazy Loading

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:

ClassNotFoundExceptionNoClassDefFoundError
TypeChecked ExceptionError
Khi nàoLoading — class không tìm thấyLinking/Initialization — class tìm thấy nhưng không load được
Nguyên nhânThiếu JAR trong classpathClass tìm thấy nhưng dependency thiếu, hoặc static initializer throw exception
FixThêm JAR vào classpathKiể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
Tại sao không compile tất cả từ đầu?
  • 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 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
ClassLoader3 tầng: Bootstrap → Platform → Application
DelegationChild delegate lên parent trước, load cuối cùng
Class LifecycleLoading → Verify → Prepare → Resolve → Initialize
Lazy LoadingClass initialize khi first active use
JIT TieredInterpreter → C1 (basic) → C2 (aggressive)
Method InliningThay method call bằng body, giảm overhead
Escape AnalysisObject không escape → stack allocation, no GC
CNFE vs NCDFEClassNotFound = missing class; NoClassDef = init failed

Đọc thêm