Class loading: nạp, liên kết, khởi tạo
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 đề
- javac chỉ cần class có trên compile classpath. Dependency
scope=providedhoặc bịexclusionvẫn compile được nhưng không có trong jar. - JVM chỉ nạp class khi dùng lần đầu, nên lỗi xuất hiện muộn, đúng lúc gọi đế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:
| Loader | Nạp gì |
|---|---|
| Bootstrap (native) | java.base: java.lang.*, java.util.* |
| Platform | Cá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
| ClassNotFoundException | NoClassDefFoundError | |
|---|---|---|
| Loại | Checked exception | Error (LinkageError) |
| Khi nào | Nạp động theo tên: Class.forName, loadClass, reflection | Class có lúc compile, lúc chạy JVM không nạp/khởi tạo được |
| Nguyên nhân hay gặp | Thiếu driver JDBC, sai tên class | Thiếu jar runtime, static init lỗi trước đó, xung đột version |
| Cách xử lý | Kiểm tra dependency / tên | Xem "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
- Nên: plugin system, hot reload (Spring DevTools dùng restart class loader), app server cô lập webapp.
- Không nên: app Spring Boot thông thường. Tạo class loader/class động liên tục mà không giải phóng là nguồn leak Metaspace (bài 2.5).
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:
- Không còn instance nào của class,
- Không còn ai giữ object
Classcủa nó, - 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
}
- JDK đã module hóa → bạn dùng
jlinktạo runtime nhỏ chỉ có module cần. - Đa số app Spring Boot vẫn chạy trên classpath (unnamed module), không viết
module-info. Spring Modulith là chuyện khác: kiểm tra ranh giới module ở mức package, không phải JPMS. - Lỗi hay gặp khi nâng Java:
InaccessibleObjectException ... does not "opens" java.langdo thư viện cũ reflection vào JDK. Sửa bằng nâng thư viện;--add-openschỉ là tạm thời.
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
- Load → Link (verify, prepare, resolve) → Init; nạp lười.
- Bootstrap → Platform → App → custom; hỏi cha trước.
- Class = tên + loader.
- CNFE: nạp theo tên. NCDFE: có lúc compile, mất lúc chạy / static init hỏng.
- Không I/O trong static init.
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.