* `ClassLoader`做什么的?顧名思義,它是用來加載`Class`的。它負責將`Class`的字節碼形式轉換成內存形式的`Class`對象。字節碼可以來自于磁盤文件`*.class`,
也可以是`jar`包里面的`*.class`,也可以是來自遠程服務器提供的字節流,字節碼的本質就是一個字節數組`byte[]`,它有特定的復雜的內部格式。
有很多字節碼加密技術就是依靠`ClassLoader`來實現的。先使用工具對字節碼文件進行加密,運行時使用定制的`ClassLoader`先解密文件內容再
加載這些解密后的字節碼。每個`Class`對象的內部都有一個`classLoader`字段來標識自己是由哪個`ClassLoader`加載的。`ClassLoader`就像一個容器,里面裝了很多已經加
載的`Class`對象。
```java
class Class<T> {
...
private final ClassLoader classLoader;
...
}
```
* 延遲加載
`JVM`運行并不是一次性加載所需要的全部類的,它是按需加載,也就是延遲加載。程序在運行的過程中會逐漸遇到很多不認識的新類,這時候就會
調用`ClassLoader`來加載這些類。加載完成后就會將`Class`對象存在`ClassLoader`里面,下次就不需要重新加載了。比如你在調用某個類的靜態方法時,首先這個類是肯定需要被加載的,但是并不會觸及這個類的實例字段,那么實例字段的類別`Class`就可以暫時
不必去加載,但是它可能會加載靜態字段相關的類別,因為靜態方法會訪問靜態字段。而實例字段的類別需要等到你實例化對象的時候可能會加載。
* 各司其職
`JVM`運行實例中存在多個`ClassLoader`,不同的`ClassLoader`會從不同的地方加載字節碼文件。它可以從不同的文件目錄加載,也可以從不
同的`jar`文件中加載,也可以從網絡上不同的服務地址來加載。`JVM`中內置了三個重要的`ClassLoader`,分別是`BootstrapClassLoader`、`ExtensionClassLoader`和`APPClassLoader`。`BootStrapClassLoader`負責加載`JVM`運行時核心類,這些類位于`JAVA_HOME/lib/rt.jar`文件中,我們常用的內置庫`java.xxx.*`都在
里面,比如`java.util.*`、`java.io.*`、`java.lang.*`等等。這個`ClassLoader`比較特殊,它是由`C`代碼實現的,我們將它稱之為\[根加載器\]。`ExtensionClassLoader`負責加載JVM擴展類,比如swing系列、內置的js引擎、xml解析器等等,這些庫名通常以`javax`開頭,它們的`jar`包位于
`JAVA_HOME/lib/ext/*.jar`中,有很多`jar`包。`ApplicationClassLoader`才是直接面向我們用戶的加載器,它會加載`Classpath`環境變量里定義的路徑中的`jar`包和目錄。我們自己編寫的代碼
以及使用的第三方`jar`包通常都是由它來加載的。那些位于網絡上靜態文件服務器提供的 `jar` 包和 `class`文件,`jdk` 內置了一個 `URLClassLoader`,用戶只需要傳遞規范的網絡路徑給構造器,
就可以使用 `URLClassLoader` 來加載遠程類庫了。`URLClassLoader` 不但可以加載遠程類庫,還可以加載本地路徑的類庫,取決于構造器中不
同的地址形式。`ExtensionClassLoader` 和 `AppClassLoader` 都是 `URLClassLoader` 的子類,它們都是從本地文件系統里加載類庫。`AppClassLoader` 可以由 `ClassLoader` 類提供的靜態方法 `getSystemClassLoader()` 得到,它就是我們所說的「系統類加載器」,
我們用戶平時編寫的類代碼通常都是由它加載的。當我們的 `main` 方法執行的時候,這第一個用戶類的加載器就是 `AppClassLoader`。
* `ClassLoader`傳遞性
程序在運行過程中,遇到了一個位置的類,它會選擇哪個`ClassLoader`來加載它呢?虛擬機的策略是使用調用者`Class`對象的`ClassLoader`
來加載當前未知的類。何為調用者`Class`對象?就是在遇到這個未知的類時,虛擬機肯定在運行一個調用方法(靜態方法或者實例方法),這個方法
掛在哪個類上面,那這個類就是調用者`Class`對象。前面我們提到每個`Class`對象里面都有一個`classLoader`屬性記錄了當前的類是由誰來加載的。因為`ClassLoader`的傳遞性,所有延遲加載的類都會由初始調用`main`方法的這個`ClassLoader`全權負責,它就是`AppClassLoader`。
* 雙親委派
前面我們提到`APPClassLoader`只負責加載 `Classpath` 下面的類庫,如果遇到沒有加載的系統類庫怎么辦,`AppClassLoader` 必須將系統類庫
的加載工作交給 `BootstrapClassLoader` 和 `ExtensionClassLoader` 來做,這就是我們常說的「雙親委派」。
`AppClassLoader` 在加載一個未知的類名時,它并不是立即去搜尋 `Classpath`,它會首先將這個類名稱交給 `ExtensionClassLoader` 來加載,
如果 `ExtensionClassLoader` 可以加載,那么 `AppClassLoader` 就不用麻煩了。否則它就會搜索 `Classpath` 。而 `ExtensionClassLoader` 在加載一個未知的類名時,它也并不是立即搜尋 `ext` 路徑,它會首先將類名稱交給 `BootstrapClassLoader` 來加載,
如果 `BootstrapClassLoader` 可以加載,那么 `ExtensionClassLoader` 也就不用麻煩了。否則它就會搜索 `ext` 路徑下的 `jar` 包。這三個 `ClassLoader` 之間形成了級聯的父子關系,每個 `ClassLoader` 都很懶,盡量把工作交給父親做,父親干不了了自己才會干。每個
`ClassLoader` 對象內部都會有一個 `parent` 屬性指向它的父加載器。
```java
class ClassLoader {
...
private final ClassLoader parent;
...
}
```
值得注意的是圖中的 `ExtensionClassLoader` 的 `parent` 指針畫了虛線,這是因為它的 `parent` 的值是 `null`,當 `parent` 字段是 `null`
時就表示它的父加載器是「根加載器」。如果某個 `Class` 對象的 `classLoader` 屬性值是 `null`,那么就表示這個類也是「根加載器」加載的。
因為`Bootstrap Loader`是用`C\C++`語言寫的,依`java`的觀點來看,邏輯上并不存在`Bootstrap Loader`的類實體,所以在`java`程序代碼里
試圖打印出其內容時,我們就會看到輸出為`null`。
* Class.forName
當我們在使用 `jdbc` 驅動時,經常會使用 `Class.forName` 方法來動態加載驅動類。
```java
Class.forName("com.mysql.cj.jdbc.Driver");
```
其原理是 `mysql` 驅動的 `Driver` 類里有一個靜態代碼塊,它會在 `Driver` 類被加載的時候執行。這個靜態代碼塊會將 `mysql` 驅動實例注冊到全局的
`jdbc` 驅動管理器里。
```java
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` 來加載。
```java
Class<?> forName(String name, boolean initialize, ClassLoader cl);
```
通過這種形式的 `forName` 方法可以突破內置加載器的限制,通過使用自定類加載器允許我們自由加載其它任意來源的類庫。根據 `ClassLoader`
的傳遞性,目標類庫傳遞引用到的其它類庫也將會使用自定義加載器加載。
* 自定義加載器
`ClassLoader` 里面有三個重要的方法 `loadClass()`、`findClass()` 和 `defineClass()`。`loadClass()` 方法是加載目標類的入口,它首先會查找當前 `ClassLoader` 以及它的雙親里面是否已經加載了目標類,如果沒有找到就會讓雙親嘗試加載,
如果雙親都加載不了,就會調用 `findClass()` 讓自定義加載器自己來加載目標類。`ClassLoader` 的 `findClass()` 方法是需要子類來覆蓋的,
不同的加載器將使用不同的邏輯來獲取目標類的字節碼。拿到這個字節碼之后再調用 `defineClass()` 方法將字節碼轉換成 `Class` 對象。
下面我使用偽代碼表示一下基本過程:
```java
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);
}
}
```
自定義類加載器不易破壞雙親委派規則,不要輕易覆蓋 `loadClass` 方法。否則可能會導致自定義加載器無法加載內置的核心類庫。在使用自定義加載器時,
要明確好它的父加載器是誰,將父加載器通過子類的構造器傳入。如果父類加載器是 `null`,那就表示父加載器是「根加載器」。
```java
// ClassLoader 構造器
protected ClassLoader(String name, ClassLoader parent);
```
雙親委派規則可能會變成三親委派,四親委派,取決于你使用的父加載器是誰,它會一直遞歸委派到根加載器。
* Class.forName vs ClassLoader.loadClass
這兩個方法都可以用來加載目標類,它們之間有一個小小的區別,那就是 `Class.forName()` 方法可以獲取原生類型的 `Class`,
而 `ClassLoader.loadClass()` 則會報錯。
```java
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` 這種形式叫「扁平化」依賴管理。使用 `ClassLoader` 可以解決鉆石依賴問題。不同版本的軟件包使用不同的 `ClassLoader` 來加載,位于不同 `ClassLoader` 中名稱一樣的類實際上是不同的類。
下面讓我們使用 `URLClassLoader` 來嘗試一個簡單的例子,它默認的父加載器是 `AppClassLoader`
```java
//$ 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`
加載出來的類都不能算同一個類。
```java
depv1Class.equals(depv2Class);
```
我們還可以讓兩個不同版本的 `Dep` 類實現同一個接口,這樣可以避免使用反射的方式來調用 `Dep` 類里面的方法。
```java
Class<?> depv1Class = v1.loadClass("Dep");
IPrint depv1 = (IPrint)depv1Class.getConstructor().newInstance();
depv1.print()
```
`ClassLoader` 固然可以解決依賴沖突問題,不過它也限制了不同軟件包的操作界面必須使用反射或接口的方式進行動態調用。
`Maven` 沒有這種限制,它依賴于虛擬機的默認懶惰加載策略,運行過程中如果沒有顯式使用定制的 `ClassLoader`,那么從頭到尾都是在使用
`AppClassLoader`,而不同版本的同名類必須使用不同的 `ClassLoader` 加載,所以 `Maven` 不能完美解決鉆石依賴。
如果你想知道有沒有開源的包管理工具可以解決鉆石依賴的,我推薦你了解一下 `sofa-ark`,它是螞蟻金服開源的輕量級類隔離框架。
* 分工與合作
這里我們重新理解一下 `ClassLoader` 的意義,它相當于類的命名空間,起到了類隔離的作用。位于同一個 `ClassLoader` 里面的類名是唯一的,
不同的 `ClassLoader` 可以持有同名的類。`ClassLoader` 是類名稱的容器,是類的沙箱。

不同的 `ClassLoader` 之間也會有合作,它們之間的合作是通過 `parent` 屬性和雙親委派機制來完成的。`parent` 具有更高的加載優先級。除此之外,
`parent` 還表達了一種共享關系,當多個子 `ClassLoader` 共享同一個 `parent` 時,那么這個 `parent` 里面包含的類可以認為是所有子 `ClassLoader` 共享的。
這也是為什么 `BootstrapClassLoader` 被所有的類加載器視為祖先加載器,`JVM` 核心類庫自然應該被共享。
* Thread.contextClassLoader
如果你稍微閱讀過 `Thread` 的源代碼,你會在它的實例字段中發現有一個字段非常特別
```java
class Thread {
...
private ClassLoader contextClassLoader;
public ClassLoader getContextClassLoader() {
return contextClassLoader;
}
public void setContextClassLoader(ClassLoader cl) {
this.contextClassLoader = cl;
}
...
}
```
`contextClassLoader`「線程上下文類加載器」,這究竟是什么東西?首先 `contextClassLoader` 是那種需要顯式使用的類加載器,如果你沒有顯式使用它,也就永遠不會在任何地方用到它。你可以使用下面這種方式來顯式使用它
```java
Thread.currentThread().getContextClassLoader().loadClass(name);
```
這意味著如果你使用 `forName(string name)` 方法加載目標類,它不會自動使用 `contextClassLoader`。那些因為代碼上的依賴關系而懶惰加載的類也不會自動使用 `contextClassLoader`來加載。
其次線程的 `contextClassLoader` 是從父線程那里繼承過來的,所謂父線程就是創建了當前線程的線程。程序啟動時的 `main` 線程的 `contextClassLoader` 就是 `AppClassLoader`。這意味著如果沒有人工去設置,那么所有的線程的 contextClassLoader 都是 AppClassLoader。
那這個 `contextClassLoader` 究竟是做什么用的?我們要使用前面提到了類加載器分工與合作的原理來解釋它的用途。
它可以做到跨線程共享類,只要它們共享同一個 `contextClassLoader`。父子線程之間會自動傳遞 `contextClassLoader`,所以共享起來將是自動化的。
如果不同的線程使用不同的 `contextClassLoader`,那么不同的線程使用的類就可以隔離開來。
如果我們對業務進行劃分,不同的業務使用不同的線程池,線程池內部共享同一個 `contextClassLoader`,線程池之間使用不同的 `contextClassLoader`,就可以很好的起到隔離保護的作用,避免類版本沖突。
如果我們不去定制 `contextClassLoader`,那么所有的線程將會默認使用 `AppClassLoader`,所有的類都將會是共享的。
線程的 `contextClassLoader` 使用場合比較罕見,如果上面的邏輯晦澀難懂也不必過于計較。
`JDK9` 增加了模塊功能之后對類加載器的結構設計做了一定程度的修改,不過類加載器的原理還是類似的,作為類的容器,它起到類隔離的作用,同時還需要依靠雙親委派機制來建立不同的類加載器之間的合作關系。
[TOC]