ClassLoader hierarchy — ba tầng và parent delegation
JVM không có một nguồn duy nhất nạp class. Hiểu hierarchy Bootstrap → Platform → Application và parent delegation để biết class nào nạp qua loader nào.
TL;DR: JVM không nạp class từ một nguồn duy nhất. Từ Java 9, có ba ClassLoader chuẩn xếp thành hierarchy: Bootstrap (core JDK như String, Object — viết bằng native, không có Java reference nên getClassLoader() trả null), Platform (module chuẩn ngoài core như java.sql), và Application (class app của bạn và dependency Maven/Gradle). Khi cần một class, loader con luôn hỏi parent trước, tự tìm sau — gọi là parent delegation. Cơ chế này đảm bảo java.lang.String luôn nạp qua Bootstrap, không bị app ghi đè bằng version giả. Hệ quả cốt lõi: JVM định danh một class bằng cặp (loader, tên đầy đủ), nên cùng một class nạp qua hai loader độc lập là hai class khác nhau.
Bạn gọi String.class.getClassLoader() và nhận về null. Gọi lại với một class trong app của bạn thì lại ra AppClassLoader. Cùng một JVM, cùng một cách hỏi — vì sao hai class lại nạp qua hai loader khác nhau, và null nghĩa là gì?
Câu trả lời nằm ở kiến trúc ClassLoader. JVM chia việc nạp class cho nhiều loader xếp tầng, mỗi loader có classpath riêng và một quy tắc phối hợp chặt chẽ. Hiểu đúng kiến trúc này là nền để về sau đọc được lỗi NoClassDefFoundError hay ClassCastException trong container — những bug mà nếu không nắm loader thì debug mò mẫm hàng giờ.
Bài này đi qua hierarchy ba tầng, parent delegation model, và hệ quả quan trọng nhất: vì sao hai loader load cùng một class lại cho ra hai class khác nhau.
1. Analogy — Thư viện công ty nhiều tầng
Hình dung một công ty ba tầng, mỗi tầng có một thư viện riêng:
- Tầng 1 — Bootstrap: thư viện trung tâm, chứa sách JDK chuẩn (
String,Object,ArrayList). Ai cũng đọc được. - Tầng 2 — Platform: thư viện kỹ thuật, chứa module chuẩn nhưng không thuộc core (SQL, scripting, crypto).
- Tầng 3 — App: thư viện riêng của nhóm bạn, chứa sách dự án (Guava, Spring, Jackson).
Khi cần một cuốn sách, quy tắc là hỏi tầng trên trước: bạn hỏi tầng 1, không có thì tầng 2, không có thì tầng 3 mới tự tìm. Quy trình "hỏi parent trước, tự tìm sau" chính là parent delegation. Nó bảo đảm cuốn String bạn mượn luôn là bản chuẩn ở tầng 1, không phải một bản tự chế lẫn vào tầng 3.
| Đời thường | JVM |
|---|---|
| Thư viện tầng 1 | Bootstrap ClassLoader |
| Thư viện tầng 2 | Platform ClassLoader |
| Thư viện tầng 3 | Application ClassLoader |
| Sách = class file | File .class |
| Mượn sách | Load class |
| Hỏi tầng trên trước | Parent delegation |
Ba tầng Bootstrap → Platform → App, mỗi tầng một classpath riêng, con luôn hỏi parent trước. Gặp lỗi class → hỏi ngay: "class đó nằm trong classpath của loader nào?".
2. Hierarchy ba tầng
Từ Java 9 (JEP 261 — module system), JVM có ba ClassLoader chuẩn, xếp thành một chuỗi parent-child:
Chuỗi parent-child đó thường được vẽ từ Bootstrap xuống App, và chính chiều mũi tên ấy dễ gây hiểu nhầm: nó là chiều quan hệ parent, không phải chiều đi của một yêu cầu nạp class. Yêu cầu đi ngược lại — leo lên hết parent trước, rồi mới quay xuống, mỗi tầng tự tìm trên đường về:

Nhìn theo chiều đó mới thấy parent delegation bảo vệ được cái gì: java.lang.String luôn do Bootstrap nạp vì Bootstrap được hỏi trước, nên không ai chèn được một bản String tự chế vào -cp.
Bootstrap ClassLoader
Viết bằng native C++, không có Java reference. Nó nạp core JDK từ $JAVA_HOME/lib/modules: java.lang.* (String, Object, Thread), java.util.* (ArrayList, HashMap), java.io.*, java.net.*.
Vì Bootstrap không phải một đối tượng Java, getClassLoader() của class do nó nạp trả về null — đó là convention của Java để 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 các module chuẩn không thuộc core: java.sql, java.scripting, java.xml.crypto, jdk.crypto.*.
System.out.println(java.sql.Connection.class.getClassLoader());
// jdk.internal.loader.ClassLoaders$PlatformClassLoader@...
Trước Java 9, tầng này gọi là Extension ClassLoader, đọc JAR từ $JAVA_HOME/lib/ext. Java 9 thay bằng Platform ClassLoader gắn với module system — nên đừng dùng tên "Extension" cho JDK hiện đại.
Trực giác "ngoài core thì thuộc Platform" không phải lúc nào cũng đúng. Module java.xml (chứa javax.xml.*) nằm trong danh sách boot module, nên getClassLoader() của nó trả null. Việc phân module nào cho loader nào được định nghĩa cứng trong jdk.internal.module.ModuleLoaderMap — khi nghi ngờ, in getClassLoader() ra kiểm, đừng đoán.
Application ClassLoader (System ClassLoader)
Nạp class từ argument -cp / --class-path: class app của bạn và mọi dependency Maven/Gradle đều qua loader này. Lấy reference bằng ClassLoader.getSystemClassLoader().
System.out.println(MyApp.class.getClassLoader());
// jdk.internal.loader.ClassLoaders$AppClassLoader@...
Custom ClassLoader
Framework và container thường thêm loader con của riêng chúng, đặt bên dưới Application trong hierarchy:
- Tomcat: mỗi WAR có một
WebappClassLoaderriêng — isolation giữa các app cùng host. - Spring Boot executable jar:
LaunchedClassLoaderđọc class từ nested JAR trongBOOT-INF/lib. - Hot reload (Spring DevTools, JRebel): loader mới mỗi lần reload, drop loader cũ.
Cơ chế và các bug của những loader này là chủ đề của bài 01c — classloader isolation. Ở đây chỉ cần nhớ: chúng nằm dưới App loader và vẫn tuân parent delegation (trừ vài ngoại lệ có chủ đích).
3. Parent delegation model
Thuật toán
Khi có ai gọi loader.loadClass("com.myapp.Foo"), loader không tự tìm ngay:
// Rut gon tu java.lang.ClassLoader
protected Class<?> loadClass(String name) {
// 1. Da load truoc do chua? (cache theo tung loader)
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 o buoc 3
}
// 3. Tu tim trong classpath cua chinh minh
return findClass(name);
}
Ba bước: (1) kiểm cache — mỗi class chỉ load đúng một lần trên mỗi loader; (2) đẩy yêu cầu lên parent, parent lại đẩy lên parent của nó, cứ thế tới Bootstrap; (3) chỉ khi cả chuỗi parent đều không có, loader này mới tự tìm trong classpath riêng.
Vì sao thiết kế như vậy?
Để không class hệ thống nào bị ghi đè. Giả sử app của bạn ship một class tên java.lang.String với ý đồ thay bản chuẩn. Nếu không có delegation, App loader có thể nạp nó, và mọi code trong app sẽ dùng String giả đó — vỡ toàn bộ JDK.
Với parent delegation, App loader hỏi parent trước. Bootstrap có sẵn java.lang.String chuẩn nên trả về ngay; bản giả trong app không bao giờ được nạp.
JVM còn một tầng phòng thủ cứng nữa: package java.* bị cấm nạp qua loader khác Bootstrap — cố tình sẽ nhận SecurityException. Đây là defense in depth: delegation là phòng thủ chính, hard-block là lưới an toàn.
4. Hai loader nạp cùng một class thì sao?
Đây là kết luận quan trọng nhất của bài, và là nền cho mọi lỗi isolation về sau.
JVM không định danh class chỉ bằng tên đầy đủ. Nó định danh bằng cặp (loader, tên đầy đủ). Vì thế nếu hai ClassLoader không có quan hệ parent-child (không ai delegate cho ai), mỗi loader tự nạp class cùng tên sẽ tạo ra hai Class<?> object khác nhau:
ClassLoader loader1 = new URLClassLoader(urls, null); // parent = null
ClassLoader loader2 = new URLClassLoader(urls, null);
Class<?> c1 = loader1.loadClass("com.foo.Bar");
Class<?> c2 = loader2.loadClass("com.foo.Bar");
System.out.println(c1 == c2); // false -- cung ten, khac Class object
Với hierarchy chuẩn (App delegate lên Platform lên Bootstrap), điều này không xảy ra: mọi class core chỉ nạp một lần ở Bootstrap rồi chia sẻ xuống dưới. Nhưng ngay khi có hai loader anh em (cùng một parent, hoặc parent null) cùng nạp một class, bạn có hai bản độc lập. Ép kiểu một đối tượng của bản này sang kiểu của bản kia sẽ ném ClassCastException với thông báo trông vô lý: com.foo.Bar cannot be cast to com.foo.Bar.
Đây chính xác là chuyện xảy ra trong Tomcat (shared lib trùng JAR với WEB-INF/lib) và Spring DevTools (object trước reload so với sau reload) — bài 01c mổ từng ca và cách diagnose.
5. Pitfall tổng hợp
❌ Nhầm 1: Coi getClassLoader() == null là lỗi.
if (obj.getClass().getClassLoader() == null) throw new IllegalStateException();
✅ null là hợp lệ — nó nghĩa là class do Bootstrap nạp (mọi class core JDK). Đừng coi null là "chưa có loader".
❌ Nhầm 2: Đặt class vào package java.* để "gần" JDK.
package java.lang.util; // JVM hard-block, SecurityException khi load
✅ JVM cấm nạp java.* qua loader khác Bootstrap. Dùng package tên thật của app.
❌ Nhầm 3: Class.forName trong framework mà không chỉ định loader.
Class.forName("com.foo.Plugin"); // dung caller class loader — co the sai
✅ Trong container/framework, class cần tìm thường nằm ở loader khác caller. Dùng thread context loader để nạp đúng chỗ:
Thread.currentThread().getContextClassLoader().loadClass("com.foo.Plugin");
6. 📚 Deep Dive Oracle
Spec / reference chính thức:
- java.lang.ClassLoader (Java 21 API) — mô tả
loadClass, parent delegation, và conventionnullcho Bootstrap. - JEP 261: Module System — nguồn của thay đổi Java 9: Bootstrap/Platform/Application thay cho Bootstrap/Ext/App.
- ClassLoader.getPlatformClassLoader() — API lấy Platform loader, xác nhận tên chính thức.
Ghi chú: Đọc phần "Parent Delegation Model" ngay đầu Javadoc của ClassLoader — nó ghi rõ thứ tự hỏi và lý do. JEP 261 giải thích vì sao Extension ClassLoader biến mất: module system thay cơ chế lib/ext cũ, nên mọi tài liệu gọi tầng giữa là "Extension" đều lỗi thời với Java 9+.
7. Liên hệ các bài khác
- Bài 01b — Vòng đời class: load, link, initialize — sau khi loader tìm được file
.class, JVM còn ba phase xử lý; bài đó cũng phân biệtClassNotFoundExceptionvớiNoClassDefFoundError. - Bài 01c — Classloader isolation trong thực tế — hệ quả "hai loader, hai class" ở §4 hiện hình thành lỗi thật trong Tomcat và Spring Boot.
- Bài 02 — Class file và javap — sau khi class được nạp, cấu trúc bên trong file
.classmà loader đọc là gì.
8. Tóm tắt
- Ba tầng (Java 9+): Bootstrap (core JDK, native, object Java = null) → Platform (module chuẩn ngoài core, thay Extension ClassLoader bị bỏ từ Java 9) → Application (app + classpath).
- Parent delegation: loader con hỏi parent trước, tự tìm sau; JVM hard-block
java.*khỏi mọi loader không phải Bootstrap — bảo vệ class hệ thống. - JVM định danh class bằng cặp (loader, tên đầy đủ): hai loader độc lập nạp cùng JAR → hai
Class<?>khác nhau → cast giữa chúng némClassCastException: Bar cannot be cast to Bar. - Debug tip:
getClassLoader()trảnull= Bootstrap nạp; ingetClassLoader()của cả hai phía khi thấyX cannot be cast to Xđể xác định loader nào gây isolation.
9. Tự kiểm tra
- Q1Vì sao parent delegation tồn tại, và điều gì xảy ra nếu không có nó?
- Q2
String.class.getClassLoader()trảnull, cònjava.sql.Connection.class.getClassLoader()ra Platform loader. Giải thích vì sao khác nhau. - Q3Hai
URLClassLoadertạo vớiparent = nullcùng nạpcom.foo.Bar. So sánhc1 == c2ra gì, và vì sao? - Q4Vì sao JVM hard-block package
java.*, trong khi đã có parent delegation rồi? - Q5Khi nào cần đến một custom ClassLoader, và vì sao hierarchy cho phép nhiều loader cùng tồn tại mà không xung đột?
Bài tiếp theo: Vòng đời class — load, link, initialize
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên