ClassLoader 做什么的?
class Class<T> {
...
private final ClassLoader classLoader;
...
}
延遲加載
各司其職
JVM 運行實例中會存在多個 ClassLoader,不同的 ClassLoader 會從不同的地方加載字節碼文件。它可以從不同的文件目錄加載,也可以從不同的 jar 文件中加載,也可以從網絡上不同的靜態文件服務器來下載字節碼再加載。
JVM 中內置了三個重要的 ClassLoader,分別是 BootstrapClassLoader、ExtensionClassLoader 和 AppClassLoader。
ClassLoader 傳遞性
雙親委派
這三個 ClassLoader 之間形成了級聯的父子關系,每個 ClassLoader 都很懶,盡量把工作交給父親做,父親干不了了自己才會干。每個 ClassLoader 對象內部都會有一個 parent 屬性指向它的父加載器。
class ClassLoader {
...
private final ClassLoader parent;
...
}
Class.forName
當我們在使用 jdbc 驅動時,經常會使用 Class.forName 方法來動態加載驅動類。
Class.forName("com.mysql.cj.jdbc.Driver");
其原理是 mysql 驅動的 Driver 類里有一個靜態代碼塊,它會在 Driver 類被加載的時候執行。這個靜態代碼塊會將 mysql 驅動實例注冊到全局的 jdbc 驅動管理器里。
class Driver {
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
...
}
forName 方法同樣也是使用調用者 Class 對象的 ClassLoader 來加載目標類。不過 forName 還提供了多參數版本,可以指定使用哪個 ClassLoader 來加載
Class<?> forName(String name, boolean initialize, ClassLoader cl)
通過這種形式的 forName 方法可以突破內置加載器的限制,通過使用自定類加載器允許我們自由加載其它任意來源的類庫。根據 ClassLoader 的傳遞性,目標類庫傳遞引用到的其它類庫也將會使用自定義加載器加載。
自定義加載器
ClassLoader 里面有三個重要的方法 loadClass()、findClass() 和 defineClass()。
class ClassLoader {
// 加載入口,定義了雙親委派規則
Class loadClass(String name) {
// 是否已經加載了
Class t = this.findFromLoaded(name);
if(t == null) {
// 交給雙親
t = this.parent.loadClass(name)
}
if(t == null) {
// 雙親都不行,只能靠自己了
t = this.findClass(name);
}
return t;
}
// 交給子類自己去實現
Class findClass(String name) {
throw ClassNotFoundException();
}
// 組裝Class對象
Class defineClass(byte[] code, String name) {
return buildClassFromCode(code, name);
}
}
class CustomClassLoader extends ClassLoader {
Class findClass(String name) {
// 尋找字節碼
byte[] code = findCodeFromSomewhere(name);
// 組裝Class對象
return this.defineClass(code, name);
}
}
// ClassLoader 構造器 protected ClassLoader(String name, ClassLoader parent);
雙親委派規則可能會變成三親委派,四親委派,取決於你使用的父加載器是誰,它會一直遞歸委派到根加載器。
Class.forName vs ClassLoader.loadClass
這兩個方法都可以用來加載目標類,它們之間有一個小小的區別,那就是 Class.forName() 方法可以獲取原生類型的 Class,而 ClassLoader.loadClass() 則會報錯。
Class<?> x = Class.forName("[I");
System.out.println(x);
x = ClassLoader.getSystemClassLoader().loadClass("[I");
System.out.println(x);
---------------------
class [I
Exception in thread "main" java.lang.ClassNotFoundException: [I
...
鑽石依賴
項目管理上有一個著名的概念叫着「鑽石依賴」,是指軟件依賴導致同一個軟件包的兩個版本需要共存而不能沖突。

我們平時使用的 maven 是這樣解決鑽石依賴的,它會從多個沖突的版本中選擇一個來使用,如果不同的版本之間兼容性很糟糕,那么程序將無法正常編譯運行。Maven 這種形式叫「扁平化」依賴管理。
$ cat ~/source/jcl/v1/Dep.java
public class Dep {
public void print() {
System.out.println("v1");
}
}
$ cat ~/source/jcl/v2/Dep.java
public class Dep {
public void print() {
System.out.println("v1");
}
}
$ cat ~/source/jcl/Test.java
public class Test {
public static void main(String[] args) throws Exception {
String v1dir = "file:///Users/qianwp/source/jcl/v1/";
String v2dir = "file:///Users/qianwp/source/jcl/v2/";
URLClassLoader v1 = new URLClassLoader(new URL[]{new URL(v1dir)});
URLClassLoader v2 = new URLClassLoader(new URL[]{new URL(v2dir)});
Class<?> depv1Class = v1.loadClass("Dep");
Object depv1 = depv1Class.getConstructor().newInstance();
depv1Class.getMethod("print").invoke(depv1);
Class<?> depv2Class = v2.loadClass("Dep");
Object depv2 = depv2Class.getConstructor().newInstance();
depv2Class.getMethod("print").invoke(depv2);
System.out.println(depv1Class.equals(depv2Class));
}
}
在運行之前,我們需要對依賴的類庫進行編譯
$ cd ~/source/jcl/v1 $ javac Dep.java $ cd ~/source/jcl/v2 $ javac Dep.java $ cd ~/source/jcl $ javac Test.java $ java Test v1 v2 false
在這個例子中如果兩個 URLClassLoader 指向的路徑是一樣的,下面這個表達式還是 false,因為即使是同樣的字節碼用不同的 ClassLoader 加載出來的類都不能算同一個類
depv1Class.equals(depv2Class)
我們還可以讓兩個不同版本的 Dep 類實現同一個接口,這樣可以避免使用反射的方式來調用 Dep 類里面的方法。
Class<?> depv1Class = v1.loadClass("Dep");
IPrint depv1 = (IPrint)depv1Class.getConstructor().newInstance();
depv1.print()
分工與合作
這里我們重新理解一下 ClassLoader 的意義,它相當於類的命名空間,起到了類隔離的作用。位於同一個 ClassLoader 里面的類名是唯一的,不同的 ClassLoader 可以持有同名的類。ClassLoader 是類名稱的容器,是類的沙箱。

Thread.contextClassLoader
如果你稍微閱讀過 Thread 的源代碼,你會在它的實例字段中發現有一個字段非常特別class Thread {
...
private ClassLoader contextClassLoader;
public ClassLoader getContextClassLoader() {
return contextClassLoader;
}
public void setContextClassLoader(ClassLoader cl) {
this.contextClassLoader = cl;
}
...
}
contextClassLoader「線程上下文類加載器」,這究竟是什么東西?
Thread.currentThread().getContextClassLoader().loadClass(name);
這意味着如果你使用 forName(string name) 方法加載目標類,它不會自動使用 contextClassLoader。那些因為代碼上的依賴關系而懶惰加載的類也不會自動使用 contextClassLoader來加載。
轉載鏈接:https://juejin.im/post/5c04892351882516e70dcc9b
