Class loading: nạp, liên kết, khởi tạo

4 câu hỏi~20 phútTừ khóa: loading, linking, verification, preparation, resolution, initialization, static init, bootstrap/platform/app class loader, parent delegation, ClassNotFoundException, NoClassDefFoundError, LaunchedClassLoader, fat jar, JPMS

Tiếp đi: class loading. order-service build thì xanh, chạy local ổn, lên server lại ném NoClassDefFoundError. Sao compile được mà chạy lại không thấy class?

Vì compile và chạy dùng hai classpath khác nhau, và JVM nạp class lười: chỉ nạp khi cần lần đầu. Hiểu quá trình class loading là đọc được lỗi này trong vài giây.

1. Bài toán

2026-10-14 09:12:03 ERROR o.a.c.c.C.[dispatcherServlet] Servlet.service() threw exception
java.lang.NoClassDefFoundError: com/fasterxml/jackson/datatype/jsr310/JavaTimeModule
    at com.shop.order.config.JsonConfig.mapper(JsonConfig.java:18)
Caused by: java.lang.ClassNotFoundException: com.fasterxml.jackson.datatype.jsr310.JavaTimeModule

App khởi động bình thường, chỉ lỗi khi request đầu tiên đi vào chỗ cần class đó.

2. Vấn đề

3. Giải pháp: hiểu 3 pha và class loader

Loading  ──►  Linking                       ──►  Initialization
(đọc .class   ├ Verification (bytecode hợp lệ?)   (chạy static field
 tạo Class)   ├ Preparation (static = giá trị       initializer + static {}
              │   mặc định 0/null)                  đúng 1 lần, thread-safe)
              └ Resolution (symbolic ref → thật, thường lười)

Class được initialize khi: tạo instance (new), gọi static method, đọc/ghi static field (không phải hằng compile-time), reflection Class.forName, hoặc subclass được init.

Class loader theo cây cha-con, dùng parent delegation:

LoaderNạp gì
Bootstrap (native)java.base: java.lang.*, java.util.*
PlatformCác module khác của JDK (java.sql, java.net.http...)
Application (System)Classpath / module path của app
Custom (Spring Boot LaunchedClassLoader, Tomcat WebappClassLoader)Jar lồng trong fat jar, webapp

4. Từng bước

Bước 1: thứ tự static init

public class OrderIdGenerator {
    static { System.out.println("1. static block A"); }
    private static final String PREFIX = loadPrefix();          // 2
    static { System.out.println("3. static block B"); }
    private final long createdAt;
    { System.out.println("instance block"); }                    // 4 (mỗi new)
    public OrderIdGenerator() { createdAt = System.nanoTime(); System.out.println("5. ctor"); }

    private static String loadPrefix() { System.out.println("2. PREFIX"); return "ORD"; }
}

Static chạy theo thứ tự viết trong file, một lần cho mỗi class loader. Có kế thừa thì: static cha → static con → instance init + ctor của cha → instance init + ctor của con.

Bước 2: static init ném exception → bẫy kinh điển

public class TaxTable {
    static final Map<String, BigDecimal> RATES = load(System.getenv("TAX_FILE")); // file thiếu → NPE
}
Lần gọi 1: java.lang.ExceptionInInitializerError  Caused by: NullPointerException
Lần gọi 2+: java.lang.NoClassDefFoundError: Could not initialize class com.shop.order.TaxTable

Class bị đánh dấu hỏng vĩnh viễn trong loader đó. Log lần 2 trở đi không còn nguyên nhân gốc, phải tìm log lần đầu. Bài học: đừng làm I/O trong static init; để Spring bean @PostConstruct hoặc config lo.

Bước 3: chẩn đoán

# Class nào được nạp, từ jar nào
$ java -Xlog:class+load=info -jar order-service.jar | grep JavaTimeModule

# Jar có chứa dependency không
$ jar tf target/order-service.jar | grep jackson-datatype-jsr310
BOOT-INF/lib/jackson-datatype-jsr310-2.17.2.jar

# Ai kéo version nào
$ ./mvnw dependency:tree -Dincludes=com.fasterxml.jackson.datatype

Bước 4: fat jar của Spring Boot

order-service.jar
├── META-INF/MANIFEST.MF
│     Main-Class: org.springframework.boot.loader.launch.JarLauncher
│     Start-Class: com.shop.order.OrderApplication
├── org/springframework/boot/loader/...   ← loader
├── BOOT-INF/classes/   ← code của bạn
└── BOOT-INF/lib/*.jar  ← jar lồng jar (JVM chuẩn không đọc được)

JarLauncher tạo LaunchedClassLoader biết đọc jar lồng, rồi gọi main của Start-Class. Trong Docker nên giải nén theo layer (java -Djarmode=tools -jar app.jar extract --layers, xem bài 16.2).

5. Cách hoạt động: parent delegation

loadClass("com.shop.order.OrderService")
   LaunchedClassLoader ── đã nạp? không ──► hỏi cha
     App loader ── hỏi cha
       Platform ── hỏi cha
         Bootstrap: không có ──► trả về
       Platform: không có ──► trả về
     App: không có trong classpath gốc
   LaunchedClassLoader: tìm trong BOOT-INF/classes ✓ define class

Lợi ích: không ai "đè" được java.lang.String bằng class giả;
         mỗi class chỉ nạp một lần cho một loader.
Danh tính class = (tên đầy đủ + class loader).
  Cùng tên, khác loader ⇒ khác class ⇒ ClassCastException "X cannot be cast to X"

6. So sánh hai lỗi hay hỏi

ClassNotFoundExceptionNoClassDefFoundError
LoạiChecked exceptionError (LinkageError)
Khi nàoNạp động theo tên: Class.forName, loadClass, reflectionClass có lúc compile, lúc chạy JVM không nạp/khởi tạo được
Nguyên nhân hay gặpThiếu driver JDBC, sai tên classThiếu jar runtime, static init lỗi trước đó, xung đột version
Cách xử lýKiểm tra dependency / tênXem "Caused by", log lần đầu, dependency:tree

Họ hàng: NoSuchMethodError = class có, nhưng version khác (method bị đổi chữ ký). Hay gặp khi hai thư viện kéo hai version Jackson/Guava khác nhau.

7. Khi nào cần custom class loader

Static field sống tới bao giờ? Có bao giờ một class bị gỡ ra khỏi JVM không?

Static field sống cùng class, và class sống cùng class loader của nó. Một class chỉ bị unload khi:

  1. Không còn instance nào của class,
  2. Không còn ai giữ object Class của nó,
  3. Class loader của nó không còn được tham chiếu.

Class do App loader nạp (đa số code của bạn) thì gần như sống tới khi JVM tắt. Nghĩa là static Map trong order-service không bao giờ được GC dọn cho tới khi bạn tự xóa phần tử. Đây là leak số 1 ở bài 2.5.

Còn module (JPMS) từ Java 9 thì sao, Spring Boot có dùng không?

JPMS thêm module-info.java: module khai báo mình requires gì và exports package nào. Package không export thì module khác không truy cập được, kể cả bằng reflection (trừ khi opens).

module com.shop.order {
    requires java.sql;
    requires spring.context;
    exports com.shop.order.api;
    opens com.shop.order.domain to org.hibernate.orm.core; // cho Hibernate reflection
}

Phỏng vấn hỏi class loading thì trả lời sao?

Q1. Mô tả quá trình class loading.

🇻🇳 Tiếng Việt:

Có ba pha: Loading đọc bytecode và tạo đối tượng Class; Linking gồm verify bytecode, prepare cấp bộ nhớ cho static với giá trị mặc định, và resolve tham chiếu; Initialization chạy static initializer đúng một lần, thread-safe. Class được nạp lười, lần đầu được dùng. Class loader theo mô hình parent delegation: hỏi cha trước, cha không có mới tự nạp, để class lõi của JDK không bị thay thế.

🇬🇧 English:

There are three phases: loading reads the bytecode and creates the Class object; linking verifies the bytecode, prepares static fields with default values and resolves references; initialization runs the static initializers exactly once, in a thread-safe way. Classes are loaded lazily on first use. Class loaders follow parent delegation: ask the parent first and only load it yourself if the parent cannot, so core JDK classes cannot be replaced.

Q2. ClassNotFoundException và NoClassDefFoundError khác nhau thế nào?

🇻🇳 Tiếng Việt:

ClassNotFoundException là checked exception khi nạp class động theo tên, ví dụ Class.forName, mà không tìm thấy. NoClassDefFoundError là Error khi class có lúc compile nhưng lúc chạy JVM không nạp được, do thiếu jar runtime hoặc static initializer đã lỗi trước đó. Với lỗi sau em luôn tìm ExceptionInInitializerError đầu tiên trong log và kiểm tra dependency tree.

🇬🇧 English:

ClassNotFoundException is a checked exception thrown when loading a class dynamically by name, such as Class.forName, fails. NoClassDefFoundError is an Error thrown when a class existed at compile time but cannot be loaded at runtime, because a jar is missing or its static initializer failed earlier. For the latter I look for the first ExceptionInInitializerError in the logs and check the dependency tree.

Q3. Spring Boot chạy fat jar thế nào?

🇻🇳 Tiếng Việt:

Main-Class trong manifest là JarLauncher của Spring Boot. Nó tạo một class loader riêng đọc được code trong BOOT-INF/classes và các jar lồng trong BOOT-INF/lib, rồi gọi main của Start-Class. JVM chuẩn không đọc được jar lồng jar nên cần loader này.

🇬🇧 English:

The manifest Main-Class is Spring Boot's JarLauncher. It creates a custom class loader that reads application classes from BOOT-INF/classes and nested jars from BOOT-INF/lib, then invokes the main method of the Start-Class. A standard JVM cannot read nested jars, which is why this loader exists.

Cách nhớ ngắn gọn


Tự kiểm tra

Pha nào gán static field về 0/null?

Preparation (trong Linking). Giá trị thật được gán ở Initialization.

Log có "Could not initialize class X" nghĩa là gì?

Static init của X đã lỗi trước đó; tìm ExceptionInInitializerError đầu tiên.

"com.shop.Order cannot be cast to com.shop.Order" do đâu?

Hai class cùng tên nhưng nạp bởi hai class loader khác nhau (hay gặp với DevTools restart loader).

Parent delegation bảo vệ điều gì?

Class lõi (java.lang.String...) luôn do Bootstrap nạp, app không thay thế được; tránh nạp trùng.

NoSuchMethodError thường do đâu?

Xung đột version thư viện: compile với version này, chạy với version khác.