Java的神秘世界:為何說ClassLoader 是 Java最神秘的技術(shù)之一
ClassLoader 是 Java 屆最為神秘的技術(shù)之一,無數(shù)人被它傷透了腦筋,摸不清門道究竟在哪里。網(wǎng)上的文章也是一篇又一篇,經(jīng)過本人的親自鑒定,絕大部分內(nèi)容都是在誤導(dǎo)別人。本文我?guī)ёx者徹底吃透 ClassLoader,以后其它的相關(guān)文章你們可以不必再細(xì)看了。
ClassLoader 做什么的?
顧名思義,它是用來加載 Class 的。它負(fù)責(zé)將 Class 的字節(jié)碼形式轉(zhuǎn)換成內(nèi)存形式的 Class 對(duì)象。字節(jié)碼可以來自于磁盤文件 *.class,也可以是 jar 包里的 *.class,也可以來自遠(yuǎn)程服務(wù)器提供的字節(jié)流,字節(jié)碼的本質(zhì)就是一個(gè)字節(jié)數(shù)組 []byte,它有特定的復(fù)雜的內(nèi)部格式。

有很多字節(jié)碼加密技術(shù)就是依靠定制 ClassLoader 來實(shí)現(xiàn)的。先使用工具對(duì)字節(jié)碼文件進(jìn)行加密,運(yùn)行時(shí)使用定制的 ClassLoader 先解密文件內(nèi)容再加載這些解密后的字節(jié)碼。每個(gè) Class 對(duì)象的內(nèi)部都有一個(gè) classLoader 字段來標(biāo)識(shí)自己是由哪個(gè) ClassLoader 加載的。ClassLoader 就像一個(gè)容器,里面裝了很多已經(jīng)加載的 Class 對(duì)象。
- class Class<T> {
- ...
- private final ClassLoader classLoader;
- ...
- }
延遲加載
JVM 運(yùn)行并不是一次性加載所需要的全部類的,它是按需加載,也就是延遲加載。程序在運(yùn)行的過程中會(huì)逐漸遇到很多不認(rèn)識(shí)的新類,這時(shí)候就會(huì)調(diào)用 ClassLoader 來加載這些類。加載完成后就會(huì)將 Class 對(duì)象存在 ClassLoader 里面,下次就不需要重新加載了。
比如你在調(diào)用某個(gè)類的靜態(tài)方法時(shí),首先這個(gè)類肯定是需要被加載的,但是并不會(huì)觸及這個(gè)類的實(shí)例字段,那么實(shí)例字段的類別 Class 就可以暫時(shí)不必去加載,但是它可能會(huì)加載靜態(tài)字段相關(guān)的類別,因?yàn)殪o態(tài)方法會(huì)訪問靜態(tài)字段。而實(shí)例字段的類別需要等到你實(shí)例化對(duì)象的時(shí)候才可能會(huì)加載。
各司其職
JVM 運(yùn)行實(shí)例中會(huì)存在多個(gè) ClassLoader,不同的 ClassLoader 會(huì)從不同的地方加載字節(jié)碼文件。它可以從不同的文件目錄加載,也可以從不同的 jar 文件中加載,也可以從網(wǎng)絡(luò)上不同的靜態(tài)文件服務(wù)器來下載字節(jié)碼再加載。
JVM 中內(nèi)置了三個(gè)重要的 ClassLoader,分別是 BootstrapClassLoader、ExtensionClassLoader 和 AppClassLoader。
BootstrapClassLoader 負(fù)責(zé)加載 JVM 運(yùn)行時(shí)核心類,這些類位于 $JAVA_HOME/lib/rt.jar 文件中,我們常用內(nèi)置庫(kù) java.xxx.* 都在里面,比如 java.util.、java.io.、java.nio.、java.lang. 等等。這個(gè) ClassLoader 比較特殊,它是由 C 代碼實(shí)現(xiàn)的,我們將它稱之為「根加載器」。
ExtensionClassLoader 負(fù)責(zé)加載 JVM 擴(kuò)展類,比如 swing 系列、內(nèi)置的 js 引擎、xml 解析器 等等,這些庫(kù)名通常以 javax 開頭,它們的 jar 包位于 $JAVA_HOME/lib/ext/*.jar 中,有很多 jar 包。
AppClassLoader 才是直接面向我們用戶的加載器,它會(huì)加載 Classpath 環(huán)境變量里定義的路徑中的 jar 包和目錄。我們自己編寫的代碼以及使用的第三方 jar 包通常都是由它來加載的。
那些位于網(wǎng)絡(luò)上靜態(tài)文件服務(wù)器提供的 jar 包和 class文件,jdk 內(nèi)置了一個(gè) URLClassLoader,用戶只需要傳遞規(guī)范的網(wǎng)絡(luò)路徑給構(gòu)造器,就可以使用 URLClassLoader 來加載遠(yuǎn)程類庫(kù)了。URLClassLoader 不但可以加載遠(yuǎn)程類庫(kù),還可以加載本地路徑的類庫(kù),取決于構(gòu)造器中不同的地址形式。ExtensionClassLoader 和 AppClassLoader 都是 URLClassLoader 的子類,它們都是從本地文件系統(tǒng)里加載類庫(kù)。
AppClassLoader 可以由 ClassLoader 類提供的靜態(tài)方法 getSystemClassLoader() 得到,它就是我們所說的「系統(tǒng)類加載器」,我們用戶平時(shí)編寫的類代碼通常都是由它加載的。當(dāng)我們的 main 方法執(zhí)行的時(shí)候,這第一個(gè)用戶類的加載器就是 AppClassLoader。
ClassLoader 傳遞性
程序在運(yùn)行過程中,遇到了一個(gè)未知的類,它會(huì)選擇哪個(gè) ClassLoader 來加載它呢?虛擬機(jī)的策略是使用調(diào)用者 Class 對(duì)象的 ClassLoader 來加載當(dāng)前未知的類。何為調(diào)用者 Class 對(duì)象?就是在遇到這個(gè)未知的類時(shí),虛擬機(jī)肯定正在運(yùn)行一個(gè)方法調(diào)用(靜態(tài)方法或者實(shí)例方法),這個(gè)方法掛在哪個(gè)類上面,那這個(gè)類就是調(diào)用者 Class 對(duì)象。前面我們提到每個(gè) Class 對(duì)象里面都有一個(gè) classLoader 屬性記錄了當(dāng)前的類是由誰來加載的。
因?yàn)?ClassLoader 的傳遞性,所有延遲加載的類都會(huì)由初始調(diào)用 main 方法的這個(gè) ClassLoader 全全負(fù)責(zé),它就是 AppClassLoader。
雙親委派
前面我們提到 AppClassLoader 只負(fù)責(zé)加載 Classpath 下面的類庫(kù),如果遇到?jīng)]有加載的系統(tǒng)類庫(kù)怎么辦,AppClassLoader 必須將系統(tǒng)類庫(kù)的加載工作交給 BootstrapClassLoader 和 ExtensionClassLoader 來做,這就是我們常說的「雙親委派」。

AppClassLoader 在加載一個(gè)未知的類名時(shí),它并不是立即去搜尋 Classpath,它會(huì)首先將這個(gè)類名稱交給 ExtensionClassLoader 來加載,如果 ExtensionClassLoader 可以加載,那么 AppClassLoader 就不用麻煩了。否則它就會(huì)搜索 Classpath 。
而 ExtensionClassLoader 在加載一個(gè)未知的類名時(shí),它也并不是立即搜尋 ext 路徑,它會(huì)首先將類名稱交給 BootstrapClassLoader 來加載,如果 BootstrapClassLoader 可以加載,那么 ExtensionClassLoader 也就不用麻煩了。否則它就會(huì)搜索 ext 路徑下的 jar 包。
這三個(gè) ClassLoader 之間形成了級(jí)聯(lián)的父子關(guān)系,每個(gè) ClassLoader 都很懶,盡量把工作交給父親做,父親干不了了自己才會(huì)干。每個(gè) ClassLoader 對(duì)象內(nèi)部都會(huì)有一個(gè) parent 屬性指向它的父加載器。
- class ClassLoader {
- ...
- private final ClassLoader parent;
- ...
- }
值得注意的是圖中的 ExtensionClassLoader 的 parent 指針畫了虛線,這是因?yàn)樗?parent 的值是 null,當(dāng) parent 字段是 null 時(shí)就表示它的父加載器是「根加載器」。如果某個(gè) Class 對(duì)象的 classLoader 屬性值是 null,那么就表示這個(gè)類也是「根加載器」加載的。注意這里的 parent 不是 super 不是父類,只是 ClassLoader 內(nèi)部的字段。
Class.forName
當(dāng)我們?cè)谑褂?jdbc 驅(qū)動(dòng)時(shí),經(jīng)常會(huì)使用 Class.forName 方法來動(dòng)態(tài)加載驅(qū)動(dòng)類。
- Class.forName("com.mysql.cj.jdbc.Driver");
其原理是 mysql 驅(qū)動(dòng)的 Driver 類里有一個(gè)靜態(tài)代碼塊,它會(huì)在 Driver 類被加載的時(shí)候執(zhí)行。這個(gè)靜態(tài)代碼塊會(huì)將 mysql 驅(qū)動(dòng)實(shí)例注冊(cè)到全局的 jdbc 驅(qū)動(dòng)管理器里。
- class Driver {
- static {
- try {
- java.sql.DriverManager.registerDriver(new Driver());
- } catch (SQLException E) {
- throw new RuntimeException("Can't register driver!");
- }
- }
- ...
- }
forName 方法同樣也是使用調(diào)用者 Class 對(duì)象的 ClassLoader 來加載目標(biāo)類。不過 forName 還提供了多參數(shù)版本,可以指定使用哪個(gè) ClassLoader 來加載
- Class<?> forName(String name, boolean initialize, ClassLoader cl)
通過這種形式的 forName 方法可以突破內(nèi)置加載器的限制,通過使用自定類加載器允許我們自由加載其它任意來源的類庫(kù)。根據(jù) ClassLoader 的傳遞性,目標(biāo)類庫(kù)傳遞引用到的其它類庫(kù)也將會(huì)使用自定義加載器加載。
自定義加載器
ClassLoader 里面有三個(gè)重要的方法 loadClass()、findClass() 和 defineClass()。
loadClass() 方法是加載目標(biāo)類的入口,它首先會(huì)查找當(dāng)前 ClassLoader 以及它的雙親里面是否已經(jīng)加載了目標(biāo)類,如果沒有找到就會(huì)讓雙親嘗試加載,如果雙親都加載不了,就會(huì)調(diào)用 findClass() 讓自定義加載器自己來加載目標(biāo)類。ClassLoader 的 findClass() 方法是需要子類來覆蓋的,不同的加載器將使用不同的邏輯來獲取目標(biāo)類的字節(jié)碼。拿到這個(gè)字節(jié)碼之后再調(diào)用 defineClass() 方法將字節(jié)碼轉(zhuǎn)換成 Class 對(duì)象。下面我使用偽代碼表示一下基本過程
- class ClassLoader {
- // 加載入口,定義了雙親委派規(guī)則
- Class loadClass(String name) {
- // 是否已經(jīng)加載了
- Class t = this.findFromLoaded(name);
- if(t == null) {
- // 交給雙親
- t = this.parent.loadClass(name)
- }
- if(t == null) {
- // 雙親都不行,只能靠自己了
- t = this.findClass(name);
- }
- return t;
- }
- // 交給子類自己去實(shí)現(xiàn)
- Class findClass(String name) {
- throw ClassNotFoundException();
- }
- // 組裝Class對(duì)象
- Class defineClass(byte[] code, String name) {
- return buildClassFromCode(code, name);
- }
- }
- class CustomClassLoader extends ClassLoader {
- Class findClass(String name) {
- // 尋找字節(jié)碼
- byte[] code = findCodeFromSomewhere(name);
- // 組裝Class對(duì)象
- return this.defineClass(code, name);
- }
- }
自定義類加載器不易破壞雙親委派規(guī)則,不要輕易覆蓋 loadClass 方法。否則可能會(huì)導(dǎo)致自定義加載器無法加載內(nèi)置的核心類庫(kù)。在使用自定義加載器時(shí),要明確好它的父加載器是誰,將父加載器通過子類的構(gòu)造器傳入。如果父類加載器是 null,那就表示父加載器是「根加載器」。
- // ClassLoader 構(gòu)造器
- protected ClassLoader(String name, ClassLoader parent);
雙親委派規(guī)則可能會(huì)變成三親委派,四親委派,取決于你使用的父加載器是誰,它會(huì)一直遞歸委派到根加載器。
Class.forName vs ClassLoader.loadClass
這兩個(gè)方法都可以用來加載目標(biāo)類,它們之間有一個(gè)小小的區(qū)別,那就是 Class.forName() 方法可以獲取原生類型的 Class,而 ClassLoader.loadClass() 則會(huì)報(bào)錯(cuò)。
- 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
- ...
項(xiàng)目管理上有一個(gè)著名的概念叫著「鉆石依賴」,是指軟件依賴導(dǎo)致同一個(gè)軟件包的兩個(gè)版本需要共存而不能沖突。

我們平時(shí)使用的 maven 是這樣解決鉆石依賴的,它會(huì)從多個(gè)沖突的版本中選擇一個(gè)來使用,如果不同的版本之間兼容性很糟糕,那么程序?qū)o法正常編譯運(yùn)行。Maven 這種形式叫「扁平化」依賴管理。使用 ClassLoader 可以解決鉆石依賴問題。不同版本的軟件包使用不同的 ClassLoader 來加載,位于不同 ClassLoader 中名稱一樣的類實(shí)際上是不同的類。下面讓我們使用 URLClassLoader 來嘗試一個(gè)簡(jiǎn)單的例子,它默認(rèn)的父加載器是 AppClassLoader
- $ 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));
- }
- }
在運(yùn)行之前,我們需要對(duì)依賴的類庫(kù)進(jìn)行編譯
- $ 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
在這個(gè)例子中如果兩個(gè) URLClassLoader 指向的路徑是一樣的,下面這個(gè)表達(dá)式還是 false,因?yàn)榧词故峭瑯拥淖止?jié)碼用不同的 ClassLoader 加載出來的類都不能算同一個(gè)類
- depv1Class.equals(depv2Class)
我們還可以讓兩個(gè)不同版本的 Dep 類實(shí)現(xiàn)同一個(gè)接口,這樣可以避免使用反射的方式來調(diào)用 Dep 類里面的方法。
- Class<?> depv1Class = v1.loadClass("Dep");
- IPrint depv1 = (IPrint)depv1Class.getConstructor().newInstance();
- depv1.print()
ClassLoader 固然可以解決依賴沖突問題,不過它也限制了不同軟件包的操作界面必須使用反射或接口的方式進(jìn)行動(dòng)態(tài)調(diào)用。Maven 沒有這種限制,它依賴于虛擬機(jī)的默認(rèn)懶惰加載策略,運(yùn)行過程中如果沒有顯示使用定制的 ClassLoader,那么從頭到尾都是在使用 AppClassLoader,而不同版本的同名類必須使用不同的 ClassLoader 加載,所以 Maven 不能完美解決鉆石依賴。 如果你想知道有沒有開源的包管理工具可以解決鉆石依賴的,我推薦你了解一下 sofa-ark,它是螞蟻金服開源的輕量級(jí)類隔離框架。
分工與合作
這里我們重新理解一下 ClassLoader 的意義,它相當(dāng)于類的命名空間,起到了類隔離的作用。位于同一個(gè) ClassLoader 里面的類名是唯一的,不同的 ClassLoader 可以持有同名的類。ClassLoader 是類名稱的容器,是類的沙箱。

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