1.classloader是干什么的?
根據(jù)字面意思,也可以清楚,classloader是用來加載class的。其功能即為,將class的字節(jié)碼形式轉(zhuǎn)換成內(nèi)存形式的class對象。字節(jié)碼可以來自jar包中的.class,可以是磁盤中的.class,甚至是來自遠(yuǎn)程服務(wù)器的字節(jié)流數(shù)組。因?yàn)樽止?jié)碼的本質(zhì)就是一個byte[],它有特定的內(nèi)部格式而已。

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

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

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