Java — Từ Zero đến Senior/Class loader — load, link, init và parent delegation
~25 phútJVM InternalsMiễn phí

Class loader — load, link, init và parent delegation

Class JVM nạp khi nào, qua tầng nào, và vì sao Tomcat / Spring Boot fat jar có ClassLoader riêng. Hiểu 3 phase load/link/init, hierarchy Bootstrap → Platform → App, parent delegation và 2 lỗi anh em ClassNotFoundException vs NoClassDefFoundError.

Bug điển hình production:

Caused by: java.lang.NoClassDefFoundError: com/google/common/collect/ImmutableList
    at com.myapp.Service.<init>(Service.java:12)

Bạn check pom.xml — Guava có. Build OK, test local OK. Deploy WAR vào Tomcat — NoClassDefFoundError. Cùng class file, cùng JVM, khác môi trường — vì sao?

Câu trả lời nằm ở ClassLoader. JVM không có 1 nguồn duy nhất nạp class. Mỗi ClassLoader có classpath riêng, hierarchy phân tầng, có thể isolate class cùng tên. Tomcat có ClassLoader riêng cho mỗi WAR. Spring Boot fat jar có LaunchedURLClassLoader đọc từ nested JAR. Kotlin Gradle plugin có ClassLoader riêng cho Kotlin compiler.

Hiểu sai ClassLoader → debug NoClassDefFoundError mò mẫm hàng giờ. Hiểu đúng → 5 phút locate vấn đề: "class này được load qua loader nào, classpath loader đó có gì".

Bài này đi qua 3 phase loading / linking / initialization, hierarchy Bootstrap → Platform → App, parent delegation model, khác biệt giữa ClassNotFoundExceptionNoClassDefFoundError, và pattern isolation production thường gặp (Tomcat, Spring Boot fat jar, OSGi).

1. Analogy — Thư viện công ty nhiều tầng

Một công ty 3 tầng. Mỗi tầng có thư viện riêng:

  • Tầng 1 — Bootstrap: thư viện trung tâm. Sách JDK chuẩn (String, Object, ArrayList). Tất cả nhân viên đọc được.
  • Tầng 2 — Platform: thư viện kỹ thuật. Sách standard extension (XML, crypto, scripting). Đọc được nhưng tầng 1 không thấy.
  • Tầng 3 — App: thư viện riêng nhóm bạn. Sách dự án (Guava, Spring, Jackson). Chỉ tầng 3 thấy.

Khi bạn cần 1 cuốn sách:

  1. Hỏi tầng trên trước (tầng 1) — có không? Nếu có, mượn về.
  2. Tầng 1 không có → hỏi tầng 2.
  3. Tầng 2 không có → hỏi tầng 3.
  4. Tầng 3 không có → "không tìm thấy" (ClassNotFoundException).

Quy trình "hỏi parent trước, tự tìm sau" gọi là parent delegation. Bảo đảm String trong app bạn = String trong JDK (cùng class object), không bị app override class system bằng version riêng.

Đời thườngJVM
Thư viện tầng 1Bootstrap ClassLoader
Thư viện tầng 2Platform ClassLoader
Thư viện tầng 3Application ClassLoader
Sách = class file.class file
Mượn sáchLoad class
Hỏi tầng trên trướcParent delegation
💡 Cách nhớ

3 tầng Bootstrap → Platform → App, mỗi tầng có classpath riêng, child luôn hỏi parent trước. Thấy NoClassDefFoundError → hỏi: "class đó nằm trong classpath của loader nào".

2. ClassLoader hierarchy

Java 9+ có 3 ClassLoader chuẩn:

flowchart TD
    A[Bootstrap ClassLoader<br/>native, no Java reference] --> B[Platform ClassLoader<br/>java.* extensions]
    B --> C[Application ClassLoader<br/>your app classpath]
    C --> D[Custom ClassLoader<br/>Tomcat WebappClassLoader, Spring Boot LaunchedURLClassLoader, ...]

Bootstrap ClassLoader

Native code C++, không có Java reference. Nạp class core JDK từ $JAVA_HOME/lib/modules:

  • java.lang.* (String, Object, Thread)
  • java.util.* (ArrayList, HashMap)
  • java.io.*, java.nio.*, java.net.*

Trong code, String.class.getClassLoader() trả null — Java convention biểu thị Bootstrap.

System.out.println(String.class.getClassLoader());      // null (Bootstrap)
System.out.println(ArrayList.class.getClassLoader());   // null (Bootstrap)

Platform ClassLoader (Java 9+, trước là "Extension")

Nạp module standard nhưng không phải core: java.sql, java.xml, java.scripting, jdk.crypto.*.

System.out.println(java.sql.Connection.class.getClassLoader());
// jdk.internal.loader.ClassLoaders$PlatformClassLoader@...

Trước Java 9 gọi là Extension ClassLoader, đọc từ $JAVA_HOME/lib/ext. Java 9 module system thay bằng Platform.

Application ClassLoader (System ClassLoader)

Nạp class từ -cp / --class-path argument. Class app của bạn, dependency Maven/Gradle, đều qua loader này.

System.out.println(MyApp.class.getClassLoader());
// jdk.internal.loader.ClassLoaders$AppClassLoader@...

Lấy reference: ClassLoader.getSystemClassLoader().

Custom ClassLoader

Framework / container thường có loader riêng:

  • Tomcat: mỗi WAR = 1 WebappClassLoader. Isolation giữa WAR — com.myapp.Foo của WAR A khác com.myapp.Foo của WAR B (cùng tên, khác Class object).
  • Spring Boot fat jar: LaunchedURLClassLoader đọc nested JAR trong BOOT-INF/lib/*.jar (JAR trong JAR).
  • OSGi (Equinox, Felix): mỗi bundle 1 ClassLoader, dependency declared explicit.
  • Hot reload (JRebel, Spring DevTools): ClassLoader mới mỗi reload, drop loader cũ.

Custom loader thường extend URLClassLoader hoặc SecureClassLoader.

JVM xử lý class qua 3 phase tuần tự (JLS §12, JVMS §5):

flowchart LR
    A[1. Loading<br/>Read .class file<br/>Parse bytecode] --> B[2. Linking]
    B --> B1[2a. Verification<br/>Bytecode safe?]
    B1 --> B2[2b. Preparation<br/>Alloc static fields default]
    B2 --> B3[2c. Resolution<br/>Resolve symbolic references]
    B3 --> C[3. Initialization<br/>Run static initializer<br/>Run static field assign]
    C --> D[Class ready to use]

3.1 Loading

JVM tìm .class file (qua ClassLoader), parse bytecode, tạo Class<?> object trong memory.

Trigger:

  • new MyClass() — instance đầu tiên.
  • MyClass.staticMethod() — gọi static method.
  • MyClass.staticField — đọc static field non-final.
  • Class.forName("MyClass") — explicit load.
  • Subclass init → load superclass trước.

Lazy: class chỉ load khi cần thực sự. App có 1000 class trong JAR, chạy chỉ cần 50 — JVM không load 950 class còn lại.

3.2 Linking

3 step con:

Verification: kiểm bytecode hợp lệ. Stack frame consistent, type safe, không jump nhảy lung tung. Bảo vệ JVM khỏi class file độc hại (vd hacker viết tay bytecode ăn cắp memory).

Preparation: cấp phát memory cho static field, set giá trị default (0 cho int, null cho reference, false cho boolean). Chưa chạy assignment trong code.

public static int count = 100;
// Sau Preparation: count = 0 (default int)
// Sau Initialization: count = 100 (assignment)

Resolution: chuyển symbolic reference ("java/lang/String" text) thành direct reference (con trỏ Class object trong memory). Có thể lazy — JVM trì hoãn resolution đến khi reference thực sự dùng.

3.3 Initialization

Chạy <clinit> — method JVM tự sinh chứa:

  • Assignment cho static field (static int count = 100;).
  • static { ... } block.

Chạy theo thứ tự xuất hiện trong source.

class Config {
    public static final int VERSION = 1;             // 1
    public static String name = loadName();          // 2
    static {                                          // 3
        System.out.println("Config init");
    }
    public static List<String> tags = new ArrayList<>();  // 4
}

<clinit> chạy 1 lần trên mỗi loader, thread-safe (JVM lock). Nếu init throw → class state là erroneous, mọi truy cập sau throw NoClassDefFoundError (không phải ExceptionInInitializerError như lần đầu — chỉ lần đầu).

class Bad {
    static int x = 1 / 0;   // ArithmeticException
}

try { Bad.x = 5; } catch (Throwable t) { System.out.println(t); }
// java.lang.ExceptionInInitializerError
//   Caused by: java.lang.ArithmeticException: / by zero

try { Bad.x = 5; } catch (Throwable t) { System.out.println(t); }
// java.lang.NoClassDefFoundError: Could not initialize class Bad
//   (KHONG phai ExceptionInInitializerError nua)

Lần 1 throw ExceptionInInitializerError, lần 2+ throw NoClassDefFoundError. Hay gây nhầm lẫn — bug debug khó vì stack trace lần 2 không show ArithmeticException gốc.

⚠️ Static init throw → class chết vĩnh viễn

Nếu <clinit> throw, class bị mark erroneous. Mọi access sau throw NoClassDefFoundError không kèm cause gốc. Để tìm bug thật, xem log lần đầu app start — lần đó mới có ExceptionInInitializerError với cause. Restart app ghi log đầy đủ trước khi tin NoClassDefFoundError.

4. Parent delegation model

Algorithm

Khi app gọi loader.loadClass("com.myapp.Foo"):

// Pseudo code, da don gian hoa tu java.lang.ClassLoader
protected Class<?> loadClass(String name) {
    // 1. Da load truoc do?
    Class<?> c = findLoadedClass(name);
    if (c != null) return c;

    // 2. Hoi parent truoc
    try {
        if (parent != null) return parent.loadClass(name);
        else return findBootstrapClass(name);
    } catch (ClassNotFoundException ignored) {
        // Parent khong tim duoc, tu tim
    }

    // 3. Tu tim trong classpath cua minh
    return findClass(name);
}

Quy trình:

  1. Check cache — đã load chưa? Có → trả luôn (mỗi class load đúng 1 lần per loader).
  2. Hỏi parent trước. Parent thử tìm. Parent của parent thử tìm. Cứ thế lên Bootstrap.
  3. Parent không có → tự tìm trong classpath của loader này.

Vì sao thiết kế này?

Đảm bảo class system không bị override.

Tưởng tượng app bạn ship 1 class tên java.lang.String mục đích override standard. Không có parent delegation → app loader có thể load nó → tất cả code trong app dùng class đó thay String chuẩn → vỡ JDK.

Với parent delegation: app loader hỏi parent (Bootstrap) trước. Bootstrap có java.lang.String chuẩn → trả về. App version không bao giờ load.

JVM còn hard-block — package java.* không được load qua ClassLoader khác Bootstrap. Cố tình → SecurityException.

Hệ quả: 2 class cùng tên có thể cùng tồn tại

Nếu 2 ClassLoader không có quan hệ parent-child, mỗi loader load class cùng tên → 2 Class<?> object khác nhau. JVM coi là 2 class khác nhau.

ClassLoader loader1 = new URLClassLoader(...);
ClassLoader loader2 = new URLClassLoader(...);
Class<?> c1 = loader1.loadClass("com.foo.Bar");
Class<?> c2 = loader2.loadClass("com.foo.Bar");
System.out.println(c1 == c2);   // false

Cast giữa 2 phía → ClassCastException. Lỗi ám ảnh:

java.lang.ClassCastException: com.foo.Bar cannot be cast to com.foo.Bar

Cùng tên, khác Class object — cast fail. Lỗi này sạch sẽ là dấu hiệu 2 ClassLoader load cùng class. Thường gặp khi:

  • Tomcat: shared lib trong tomcat/lib và webapp WEB-INF/lib đều có cùng JAR. Class load 2 lần.
  • Spring DevTools hot reload: object trước reload (loader cũ) vs sau reload (loader mới).

Fix: bảo đảm class load qua 1 loader duy nhất — di chuyển JAR ra shared lib (1 loader chung), hoặc dùng Class.forName(name, true, ourLoader) explicit.

5. ClassNotFoundException vs NoClassDefFoundError

2 lỗi anh em, hay gây nhầm. Khác biệt cốt lõi: lúc nào throw.

ClassNotFoundException

JVM chủ động tìm class theo name string, không thấy. Throw từ:

  • Class.forName("com.foo.Bar")
  • ClassLoader.loadClass("com.foo.Bar")
  • Class.forName(name, ...) với name không tồn tại trong classpath

Checked exception — phải catch hoặc declare throws.

try {
    Class<?> c = Class.forName("com.foo.NonExistent");
} catch (ClassNotFoundException e) {
    e.printStackTrace();
}

Nguyên nhân thường:

  • JAR thiếu trong runtime classpath.
  • Tên class typo.
  • Class load qua loader sai (vd reflection từ thread context loader nhưng class chỉ trong app loader).

NoClassDefFoundError

JVM cố link / init class đã từng compile được với class A reference, nhưng A vắng mặt lúc runtime. Throw từ:

  • Static field access của class A vắng.
  • new A() lần đầu — link fail.
  • Class A lần đầu init throw → các lần sau access throw NoClassDefFoundError.

Error (không phải Exception) — không nên catch.

Nguyên nhân thường:

  • Compile time có JAR (compile classpath), runtime không có (runtime classpath khác). Maven scope provided mà runtime không cung cấp là case kinh điển.
  • Class load OK nhưng static init throw → class chết → access sau ra NoClassDefFoundError không kèm cause (mục 3.3).
  • Build script load JAR sai version → class A có nhưng method/field A reference đã đổi → NoSuchMethodError (anh em với NoClassDefFoundError).

Phân biệt nhanh

SymptomLoạiHành động debug
Stack có Class.forName(...)ClassNotFoundExceptionTìm JAR cung cấp class đó, kiểm classpath
Stack có <init> hoặc field access trực tiếpNoClassDefFoundErrorCheck compile vs runtime classpath khớp
Could not initialize class XNoClassDefFoundError (init fail)Tìm log lần đầu — ExceptionInInitializerError có cause gốc
NoSuchMethodErrorVariant — version mismatchmvn dependency:tree tìm version conflict

Trong production Spring Boot, NoClassDefFoundError 90% là dependency conflict: 2 thư viện kéo 2 version khác nhau, Maven chọn 1 version, version đó thiếu method version kia cần.

6. Pattern isolation thực tế

Tomcat / servlet container

Tomcat layout:

tomcat/
  lib/                      <- Shared ClassLoader (cap cao)
    servlet-api.jar
    catalina.jar
  webapps/
    app1.war
      WEB-INF/lib/          <- WebappClassLoader cua app1
        guava-30.jar
    app2.war
      WEB-INF/lib/          <- WebappClassLoader cua app2
        guava-31.jar

App1 và App2 cùng host trên 1 Tomcat, cùng dùng Guava nhưng khác version. Mỗi WAR có WebappClassLoader riêng — Guava 30 trong app1, Guava 31 trong app2 không xung đột.

Đặc biệt — Tomcat ngược parent delegation cho app classpath: WebappClassLoader load class từ WEB-INF/classesWEB-INF/lib trước, mới hỏi parent. Quy ước này cho phép app override class trong shared lib (vd app cần Hibernate version mới hơn shared).

Servlet API (javax.servlet.* / jakarta.servlet.*) thì luôn load từ shared lib — không cho app override (sẽ break container).

Spring Boot fat jar

Spring Boot package app thành 1 JAR khổng lồ:

myapp.jar
  META-INF/MANIFEST.MF (Main-Class: org.springframework.boot.loader.JarLauncher)
  org/springframework/boot/loader/...   <- Loader code
  BOOT-INF/
    classes/                  <- App class
    lib/
      spring-core-6.x.jar     <- Dependency JAR (nested)
      jackson-databind-2.x.jar
      ...

JAR chuẩn không hỗ trợ JAR-trong-JAR. Spring Boot dùng LaunchedURLClassLoader custom đọc nested JAR. Khi app chạy java -jar myapp.jar:

  1. JVM load JarLauncher.
  2. JarLauncher tạo LaunchedURLClassLoader với URL trỏ vào mỗi nested JAR.
  3. LaunchedURLClassLoader load class từ BOOT-INF/classesBOOT-INF/lib/*.jar.
  4. App class load qua loader này.

Hệ quả: app class load qua LaunchedURLClassLoader, không phải App ClassLoader chuẩn. Code dùng getClass().getClassLoader() trả về LaunchedURLClassLoader. Reflection vào nested JAR cần thread context loader đúng — nhiều framework set tự động.

Spring DevTools hot reload

Khi save file, DevTools:

  1. Drop RestartClassLoader cũ (chứa user code).
  2. Tạo RestartClassLoader mới load lại user code.
  3. Giữ nguyên LaunchedURLClassLoader cha (chứa dependency, không cần reload).

Lý do tách 2 loader: dependency hàng trăm MB — reload mất giây. User code vài MB — reload < 1s. Hot reload nhanh.

Side effect: object tạo trước reload là instance class loader cũ. Reference cũ trong session/cache cast sang class mới → ClassCastException "X cannot be cast to X". Không lỗi code, chỉ là 2 loader.

7. Pitfall tổng hợp

Nhầm 1: Static init throw, debug lần thứ N không thấy cause.

class Config {
    static { Files.readAllBytes(Path.of("/missing")); }
}

✅ Tìm log lần đầu app start — có ExceptionInInitializerError với cause gốc. Restart và ghi log.

Nhầm 2: Catch NoClassDefFoundError.

try { useGuava(); } catch (NoClassDefFoundError e) { ... }

Error không nên catch. Fix classpath thay vì swallow.

Nhầm 3: ClassCastException cùng tên cùng package.

((com.foo.Bar) obj).doSomething();
// ClassCastException: com.foo.Bar cannot be cast to com.foo.Bar

✅ Tìm 2 loader load cùng JAR. Di chuyển JAR ra 1 loader chung hoặc remove duplicate.

Nhầm 4: Reflection Class.forName không chỉ định loader.

Class.forName("com.foo.Bar");   // Default: caller class loader

✅ Trong framework, dùng thread context loader:

Thread.currentThread().getContextClassLoader().loadClass("com.foo.Bar");

Nhầm 5: Đặt class vào package java.*.

package java.lang.evil;
public class MyHack { }

✅ JVM hard-block. Không load được. Dùng package tên app thực.

Nhầm 6: Eager init bằng Class.forName cho side effect.

Class.forName("com.foo.MyService");   // Mong static block chay

Class.forName(name) mặc định init (chạy <clinit>). Nếu chỉ muốn load không init: Class.forName(name, false, loader). Thường code mong init thì version 1-arg là đúng — nhưng đảm bảo không phụ thuộc side effect tinh vi (anti-pattern).

8. 📚 Deep Dive Oracle

📚 Deep Dive Oracle

Spec / reference chính thức:

Ghi chú: JLS §12.4.1 mô tả khi nào class bị "actively used" → trigger init — đáng đọc khi debug "vì sao static block không chạy". JVMS §5.5 chi tiết verification — hiểu vì sao class file lạ bị reject. Spring Boot doc giải thích thiết kế nested JAR — lý do LaunchedURLClassLoader cần thay vì dùng URLClassLoader chuẩn (URLClassLoader không đọc nested entries).

9. Tóm tắt

  • 3 ClassLoader chuẩn: Bootstrap (core JDK, native), Platform (extension module), Application (app classpath).
  • 3 phase: Loading (đọc .class), Linking (verify + prepare static + resolve reference), Initialization (chạy <clinit>).
  • Lazy: class load khi cần thực sự. App 1000 class chạy 50 → JVM load 50.
  • Parent delegation: child loader hỏi parent trước, mới tự tìm. Bảo vệ class system khỏi override.
  • Class load qua 2 loader khác nhau → 2 Class<?> object khác nhau → ClassCastException cùng tên cùng package.
  • ClassNotFoundException: chủ động tìm theo name string, fail. Checked exception.
  • NoClassDefFoundError: link / init class compile-time có nhưng runtime vắng. Error.
  • Static init throw → class erroneous vĩnh viễn. Lần 1 ra ExceptionInInitializerError (có cause), lần 2+ ra NoClassDefFoundError (không cause). Tìm log lần đầu để debug.
  • Tomcat: mỗi WAR 1 WebappClassLoader — isolation giữa app, ngược parent delegation cho app classpath.
  • Spring Boot fat jar: LaunchedURLClassLoader đọc nested JAR trong BOOT-INF/lib.
  • Hot reload (DevTools): tách RestartClassLoader (user code) khỏi LaunchedURLClassLoader (dep) để reload nhanh.
  • package java.* JVM hard-block — không thể tự định nghĩa class trong namespace JDK.

10. Tự kiểm tra

Tự kiểm tra
Q1
Vì sao parent delegation tồn tại, và điều gì xảy ra nếu không có?

Parent delegation đảm bảo class system core (như java.lang.String) chỉ load 1 lần qua Bootstrap, không bị app override.

Cơ chế: child loader luôn hỏi parent trước. String hỏi App → Platform → Bootstrap. Bootstrap tìm thấy → trả về luôn. App không bao giờ load String version riêng.

Nếu không có delegation:

  • App ship class java.lang.String riêng — load qua App loader → app dùng String version đó.
  • JDK code (chạy qua Bootstrap loader) dùng String chuẩn → 2 String khác nhau cùng tên → ClassCastException khi truyền String giữa app và JDK.
  • Lỗ hổng bảo mật — class độc hại có thể impersonate class trusted.

JVM còn hard-block package java.* không cho load qua loader khác Bootstrap — defense in depth.

Tomcat đảo quy ước cho app classpath (load WEB-INF/lib trước parent) để cho phép override library version, nhưng java.* vẫn delegate Bootstrap — không bypass được.

Q2
Khác biệt giữa ClassNotFoundExceptionNoClassDefFoundError?

ClassNotFoundException — checked exception, throw khi code chủ động tìm class theo name string mà không thấy:

  • Class.forName("com.foo.Bar")
  • loader.loadClass("com.foo.Bar")

Stack trace có method forName hoặc loadClass. Nguyên nhân: typo tên, JAR thiếu, loader sai.

NoClassDefFoundError — Error, throw khi JVM cố link / init class A đã compile-time reference, nhưng A vắng runtime. Stack trace ở chỗ:

  • new Foo() — link Foo fail.
  • Truy cập static field Foo.X.
  • Init class chính fail trước đó → access lần sau ra NoClassDefFoundError.

Nguyên nhân: compile classpath khớp, runtime classpath khác. Maven scope provided mà runtime không cung cấp là case điển hình. Hoặc 2 thư viện kéo 2 version khác nhau, version chọn thiếu method bên kia cần (NoSuchMethodError — anh em).

Quy tắc: Exception catch được; Error không nên catch — fix classpath thay vì swallow.

Q3
Đoạn sau xảy ra gì? class C { static int x = 1/0; } try { C.x = 5; } catch (Throwable t) { print(t); } try { C.x = 5; } catch (Throwable t) { print(t); }

Lần 1: ExceptionInInitializerError với cause là ArithmeticException: / by zero. JVM cố init class C, chạy <clinit> trong đó assignment x = 1/0 throw → wrap thành ExceptionInInitializerError.

Lần 2: NoClassDefFoundError: Could not initialize class Ckhông có cause. Class C đã bị mark erroneous lần 1, JVM từ chối init lại, throw NoClassDefFoundError báo "class chết".

Bug debug khó: trong production, log lần thứ N (sau khi đã restart 1 lần) chỉ thấy NoClassDefFoundError không kèm cause. Dev nhìn message "Could not initialize class C" mò mẫm.

Cách debug: tìm log app start lần đầu — có ExceptionInInitializerError kèm cause gốc. Hoặc clear log, restart app, theo dõi log đầu tiên.

Pattern phòng ngừa: tránh logic phức tạp trong static init. Nếu cần init expensive, dùng holder pattern (lazy):

class Config {
  private static class Holder {
      static final Config INSTANCE = loadFromFile();
  }
  public static Config get() { return Holder.INSTANCE; }
}

Init lazy + thread-safe miễn phí qua JVM holder idiom (mục 6 module 5).

Q4
Vì sao 2 ClassLoader load cùng JAR có thể gây ClassCastException "X cannot be cast to X"?

JVM định danh class qua cặp (loader, full-name), không chỉ name. com.foo.Bar load qua loader L1 và load qua loader L2 — hai Class<?> object khác nhau trong memory.

Cast giữa 2 instance:

Object obj = loader1.loadClass("com.foo.Bar").getConstructor().newInstance();
com.foo.Bar bar = (com.foo.Bar) obj;
// ClassCastException: com.foo.Bar cannot be cast to com.foo.Bar

Type expression com.foo.Bar ở vế cast được resolve qua loader của caller (ví dụ App loader). obj là instance qua L1. Hai Class object khác → cast fail.

Nguyên nhân thường gặp:

  • Tomcat shared lib + WEB-INF/lib trùng JAR: cùng class load qua 2 loader.
  • Spring DevTools hot reload: object tạo trước reload (loader cũ) còn ref trong session/cache, code mới (loader mới) cast → fail.
  • Reflection load class qua loader khác mainstream: vd URLClassLoader ad-hoc, rồi cast về app type.

Fix: bảo đảm class load qua 1 loader duy nhất. Tomcat → di chuyển JAR ra shared lib hoặc remove khỏi WEB-INF/lib. DevTools → restart full thay vì hot reload khi đụng object cached. Reflection → load qua loader của instance: obj.getClass().getClassLoader().

Q5
Spring Boot fat jar dùng LaunchedURLClassLoader thay URLClassLoader — vì sao cần custom loader?

JAR format chuẩn không hỗ trợ JAR-trong-JAR. URLClassLoader chuẩn đọc class từ:

  • Directory (file:///path/to/classes/)
  • JAR (file:///path/to/lib.jar) — đọc entry top-level trong JAR.

Spring Boot fat jar có dependency dạng nested JAR trong BOOT-INF/lib/spring-core.jar. URLClassLoader chuẩn không thấy class trong nested JAR — chỉ thấy entry binary BOOT-INF/lib/spring-core.jar như 1 file.

LaunchedURLClassLoader custom hiểu nested JAR:

  1. Khi resolve class, nó tìm trong BOOT-INF/classes/ (app code).
  2. Không thấy → tìm trong từng nested JAR BOOT-INF/lib/*.jar.
  3. Đọc class entry trong nested JAR qua custom URLStreamHandler hiểu URL jar:nested:....

Lợi ích design này:

  • 1 file deploy: java -jar myapp.jar chạy được, không cần extract dep ra.
  • Không xung đột với system loader: app dependency isolated trong fat jar, không pollute parent.
  • Reproducible: cùng JAR = cùng dependency exact version, không phụ thuộc classpath external.

Trade-off: startup chậm hơn 1 ít (custom resolution), debugger / IDE tooling cần hiểu nested JAR. Spring Boot 2.x build extract layout tối ưu Docker — base layer dependency ít đổi, top layer app code đổi nhiều.

Q6
Static field assignment chạy lúc nào — Loading, Linking, hay Initialization? Khác biệt với default value?

2 bước riêng biệt:

  1. Linking → Preparation: JVM cấp phát memory cho static field, set default value theo type:
    • int, long, byte, short: 0
    • float, double: 0.0
    • boolean: false
    • char: '\u0000'
    • Reference: null
  2. Initialization: JVM chạy <clinit> — method tự sinh chứa assignment thực tế (static int x = 100;) và static block (static { ... }). Chạy theo thứ tự source.

Hệ quả tinh tế:

class A {
  public static int x = B.y + 1;   // 1
}
class B {
  public static int y = A.x + 1;   // 2
}
// Truy cap A.x dau tien:
// 1. A vao Preparation: x = 0 (default)
// 2. A vao Init: chay x = B.y + 1
// 3. Reference B -> B vao Preparation: y = 0
// 4. B vao Init: chay y = A.x + 1
// 5. A.x luc nay van la 0 (chua xong init A) -> y = 1
// 6. Quay ve A: x = B.y + 1 = 2
// Ket qua: A.x = 2, B.y = 1

Circular static reference → giá trị phụ thuộc thứ tự load. Anti-pattern. Tránh.

Field final với compile-time constant (literal hoặc expression chỉ chứa literal + final reference) là exception: JVM inline trực tiếp vào bytecode caller, không trigger init class chứa nó.

class Config {
  public static final int VERSION = 42;     // Compile-time constant
  static { System.out.println("Init"); }
}

System.out.println(Config.VERSION);   // KHONG print "Init" - VERSION inline
System.out.println(Config.class);     // print "Init" - access class trigger init

Detail này quan trọng debug: vì sao static block không chạy mặc dù truy cập field. Trả lời: field là compile-time constant.

Bài tiếp theo: Bytecode và javap — đọc instruction JVM

Bài này có giúp bạn hiểu bản chất không?

Bình luận (0)

Đang tải...