1.classloader是干什么的?
根據(jù)字面意思讲仰,也可以清楚慕趴,classloader是用來加載class的。其功能即為叮盘,將class的字節(jié)碼形式轉換成內存形式的class對象秩贰。字節(jié)碼可以來自jar包中的.class,可以是磁盤中的.class,甚至是來自遠程服務器的字節(jié)流數(shù)組柔吼。因為字節(jié)碼的本質就是一個byte[]毒费,它有特定的內部格式而已。
class對象的內部有一個classloader字段來標識自己是被哪個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.特點:延遲加載
JVM 運行并不是一次性加載所需要的全部類的,它是按需加載溪厘,也就是延遲加載胡本。程序在運行的過程中會逐漸遇到很多不認識的新類,這時候就會調用 ClassLoader 來加載這些類畸悬。加載完成后就會將 Class 對象存在 ClassLoader 里面侧甫,下次就不需要重新加載了。
比如你在調用某個類的靜態(tài)方法時蹋宦,首先這個類肯定是需要被加載的披粟,但是并不會觸及這個類的實例字段,那么實例字段的類別 Class 就可以暫時不必去加載冷冗,但是它可能會加載靜態(tài)字段相關的類別守屉,因為靜態(tài)方法會訪問靜態(tài)字段。而實例字段的類別需要等到你實例化對象的時候才可能會加載蒿辙。
3.三種類加載器
JVM 運行實例中會存在多個 ClassLoader拇泛,不同的 ClassLoader 會從不同的地方加載字節(jié)碼文件。它可以從不同的文件目錄加載思灌,也可以從不同的 jar 文件中加載俺叭,也可以從網(wǎng)絡上不同的服務地址來加載。
JVM 中內置了三個重要的 ClassLoader习瑰,分別是 BootstrapClassLoader绪颖、ExtensionClassLoader 和 AppClassLoader。
BootstrapClassLoader 負責加載 JVM 運行時核心類甜奄,這些類位于 JAVA_HOME/lib/rt.jar 文件中,我們常用內置庫 java.xxx.* 都在里面窃款,比如 java.util.课兄、java.io.、java.nio.晨继、java.lang. 等等烟阐。這個 ClassLoader 比較特殊,它是由 C 代碼實現(xiàn)的紊扬,我們將它稱之為「根加載器」蜒茄。
ExtensionClassLoader 負責加載 JVM 擴展類,比如 swing 系列餐屎、內置的 js 引擎檀葛、xml 解析器 等等,這些庫名通常以 javax 開頭腹缩,它們的 jar 包位于 JAVA_HOME/lib/ext/*.jar 中屿聋,有很多 jar 包空扎。
AppClassLoader 才是直接面向我們用戶的加載器,它會加載 Classpath 環(huán)境變量里定義的路徑中的 jar 包和目錄润讥。我們自己編寫的代碼以及使用的第三方 jar 包通常都是由它來加載的转锈。
那些位于網(wǎng)絡上靜態(tài)文件服務器提供的 jar 包和 class文件,jdk 內置了一個 URLClassLoader楚殿,用戶只需要傳遞規(guī)范的網(wǎng)絡路徑給構造器撮慨,就可以使用 URLClassLoader 來加載遠程類庫了。URLClassLoader 不但可以加載遠程類庫脆粥,還可以加載本地路徑的類庫甫煞,取決于構造器中不同的地址形式。ExtensionClassLoader 和 AppClassLoader 都是 URLClassLoader 的子類冠绢,它們都是從本地文件系統(tǒng)里加載類庫抚吠。
AppClassLoader 可以由 ClassLoader 類提供的靜態(tài)方法 getSystemClassLoader() 得到,它就是我們所說的「系統(tǒng)類加載器」弟胀,我們用戶平時編寫的類代碼通常都是由它加載的楷力。當我們的 main 方法執(zhí)行的時候,這第一個用戶類的加載器就是 AppClassLoader孵户。
4.ClassLoader 傳遞性
程序在運行過程中萧朝,遇到了一個未知的類,它會選擇哪個 ClassLoader 來加載它呢夏哭?虛擬機的策略是使用調用者 Class 對象的 ClassLoader 來加載當前未知的類检柬。何為調用者 Class 對象?就是在遇到這個未知的類時竖配,虛擬機肯定正在運行一個方法調用(靜態(tài)方法或者實例方法)何址,這個方法掛在哪個類上面,那這個類就是調用者 Class 對象进胯。前面我們提到每個 Class 對象里面都有一個 classLoader 屬性記錄了當前的類是由誰來加載的用爪。
因為 ClassLoader 的傳遞性,所有延遲加載的類都會由初始調用 main 方法的這個 ClassLoader 全全負責胁镐,它就是 AppClassLoader偎血。
5.雙親委派原則
前面我們提到 AppClassLoader 只負責加載 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)的父子關系,每個 ClassLoader 都很懶棺蛛,盡量把工作交給父親做怔蚌,父親干不了了自己才會干。每個 ClassLoader 對象內部都會有一個 parent 屬性指向它的父加載器旁赊。
class ClassLoader {
...
private final ClassLoader parent;
...
}
值得注意的是圖中的 ExtensionClassLoader 的 parent 指針畫了虛線桦踊,這是因為它的 parent 的值是 null,當 parent 字段是 null 時就表示它的父加載器是「根加載器」终畅。如果某個 Class 對象的 classLoader 屬性值是 null籍胯,那么就表示這個類也是「根加載器」加載的。
6.Class.forName方法
當我們在使用 jdbc 驅動時离福,經(jīng)常會使用 Class.forName 方法來動態(tài)加載驅動類杖狼。
Class.forName("com.mysql.cj.jdbc.Driver");
其原理是 mysql 驅動的 Driver 類里有一個靜態(tài)代碼塊,它會在 Driver 類被加載的時候執(zhí)行妖爷。這個靜態(tài)代碼塊會將 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 還提供了多參數(shù)版本絮识,可以指定使用哪個 ClassLoader 來加載
Class<?> forName(String name, boolean initialize, ClassLoader cl)
通過這種形式的 forName 方法可以突破內置加載器的限制绿聘,通過使用自定類加載器允許我們自由加載其它任意來源的類庫。根據(jù) ClassLoader 的傳遞性笋除,目標類庫傳遞引用到的其它類庫也將會使用自定義加載器加載斜友。
7.可以自定義加載器嗎
ClassLoader 里面有三個重要的方法 loadClass()、findClass() 和 defineClass()垃它。
loadClass() 方法是加載目標類的入口,它首先會查找當前 ClassLoader 以及它的雙親里面是否已經(jīng)加載了目標類烹看,如果沒有找到就會讓雙親嘗試加載国拇,如果雙親都加載不了,就會調用 findClass() 讓自定義加載器自己來加載目標類惯殊。ClassLoader 的 findClass() 方法是需要子類來覆蓋的酱吝,不同的加載器將使用不同的邏輯來獲取目標類的字節(jié)碼。拿到這個字節(jié)碼之后再調用 defineClass() 方法將字節(jié)碼轉換成 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;
}
// 交給子類自己去實現(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 方法崎岂。否則可能會導致自定義加載器無法加載內置的核心類庫捆毫。在使用自定義加載器時,要明確好它的父加載器是誰冲甘,將父加載器通過子類的構造器傳入绩卤。如果父類加載器是 null,那就表示父加載器是「根加載器」江醇。
// ClassLoader 構造器
protected ClassLoader(String name, ClassLoader parent);
雙親委派規(guī)則可能會變成三親委派濒憋,四親委派,取決于你使用的父加載器是誰陶夜,它會一直遞歸委派到根加載器凛驮。
8.classLoader意義:分工與合作
這里我們重新理解一下 ClassLoader 的意義,它相當于類的命名空間条辟,起到了類隔離的作用黔夭。位于同一個 ClassLoader 里面的類名是唯一的,不同的 ClassLoader 可以持有同名的類捂贿。ClassLoader 是類名稱的容器纠修,是類的沙箱。
不同的 ClassLoader 之間也會有合作厂僧,它們之間的合作是通過 parent 屬性和雙親委派機制來完成的扣草。parent 具有更高的加載優(yōu)先級。除此之外颜屠,parent 還表達了一種共享關系辰妙,當多個子 ClassLoader 共享同一個 parent 時,那么這個 parent 里面包含的類可以認為是所有子 ClassLoader 共享的甫窟。這也是為什么 BootstrapClassLoader 被所有的類加載器視為祖先加載器密浑,JVM 核心類庫自然應該被共享。
9粗井,實例:Thread.contextClassLoader
如果你稍微閱讀過 Thread 的源代碼尔破,你會在它的實例字段中發(fā)現(xiàn)有一個字段非常特別
class Thread {
...
private ClassLoader contextClassLoader;
public ClassLoader getContextClassLoader() {
return contextClassLoader;
}
public void setContextClassLoader(ClassLoader cl) {
this.contextClassLoader = cl;
}
...
}
contextClassLoader「線程上下文類加載器」,這究竟是什么東西浇衬?
首先 contextClassLoader 是那種需要顯示使用的類加載器懒构,如果你沒有顯示使用它,也就永遠不會在任何地方用到它耘擂。你可以使用下面這種方式來顯示使用它
Thread.currentThread().getContextClassLoader().loadClass(name);
這意味著如果你使用 forName(string name) 方法加載目標類胆剧,它不會自動使用 contextClassLoader。那些因為代碼上的依賴關系而懶惰加載的類也不會自動使用 contextClassLoader來加載醉冤。
其次線程的 contextClassLoader 是從父線程那里繼承過來的秩霍,所謂父線程就是創(chuàng)建了當前線程的線程篙悯。程序啟動時的 main 線程的 contextClassLoader 就是 AppClassLoader。這意味著如果沒有人工去設置铃绒,那么所有的線程的 contextClassLoader 都是 AppClassLoader鸽照。
那這個 contextClassLoader 究竟是做什么用的?我們要使用前面提到了類加載器分工與合作的原理來解釋它的用途匿垄。
它可以做到跨線程共享類移宅,只要它們共享同一個 contextClassLoader。父子線程之間會自動傳遞 contextClassLoader椿疗,所以共享起來將是自動化的漏峰。
如果不同的線程使用不同的 contextClassLoader,那么不同的線程使用的類就可以隔離開來届榄。
如果我們對業(yè)務進行劃分浅乔,不同的業(yè)務使用不同的線程池,線程池內部共享同一個 contextClassLoader铝条,線程池之間使用不同的 contextClassLoader靖苇,就可以很好的起到隔離保護的作用,避免類版本沖突班缰。
如果我們不去定制 contextClassLoader贤壁,那么所有的線程將會默認使用 AppClassLoader,所有的類都將會是共享的埠忘。
線程的 contextClassLoader 使用場合比較罕見脾拆,如果上面的邏輯晦澀難懂也不必過于計較。
JDK9 增加了模塊功能之后對類加載器的結構設計做了一定程度的修改莹妒,不過類加載器的原理還是類似的名船,作為類的容器,它起到類隔離的作用旨怠,同時還需要依靠雙親委派機制來建立不同的類加載器之間的合作關系渠驼。