首先說一說什么是類和類加載器
1.類(Class)
我們在編寫代碼時,創(chuàng)建的每個“*.java”文件都可以認為是一個類博脑,我們使用“class”去定義一個類墩邀,例如String.java搁料。
2.類加載器(Class Loader)
(1)將“通過類的全限定名獲取描述類的二進制字節(jié)流”這件事放在虛擬機外部凉驻,由應用程序自己決定如何實現(xiàn)。
(2)我們定義的類艳丛,如果我們要在編碼中用到這個類匣掸,首先就是要先把“*.java”這個文件編譯成class文件,然后由對應的“類加載器”加載到JVM中氮双,我們才能夠使用這個“類對象”碰酝。
在我們日常使用中,類加載器默認有下面3種:
啟動類加載器屬于虛擬機的一部分戴差,它是用C++寫的送爸,看不到源碼;其他類加載器是用Java寫的暖释,說白了就是一些Java類袭厂,一會兒就可以看到了,比如擴展類加載器球匕、應用類加載器纹磺。
(1)啟動類加載器(Bootstrap Class Loader):
JDK自帶的一款類加載器,用于加載JDK內部的類亮曹。Bootstrap類加載器用于加載JDK中$JAVA_HOME/jre/lib下面的那些類橄杨,比如rt.jar包里面的類秘症。
(2)擴展類加載器(Extension Class Loader)
主要用于加載JDK擴展包里的類。一般$JAVA_HOME/lib/ext下面的包都是通過這個類加載器加載的讥珍,這個包下面的類基本上是以javax開頭的。
(3)應用程序類加載器(Application Class Loader)
用來加載開發(fā)人員自己平時寫的應用代碼的類的窄瘟,加載存放在classpath路徑下的那些應用程序級別的類的衷佃。
既然只是把class文件裝進虛擬機,為什么要用多種加載器呢蹄葱?因為Java虛擬機啟動的時候氏义,并不會一次性加載所有的class文件(內存會爆),而是根據(jù)需要去動態(tài)加載图云。
雙親委派模型
雙親委派模式是在Java 1.2后引入的惯悠,其工作原理的是,如果一個類加載器收到了類加載請求竣况,它并不會自己先去加載克婶,而是把這個請求委托給父類的加載器去執(zhí)行,如果父類加載器還存在其父類加載器丹泉,則進一步向上委托情萤,依次遞歸,請求最終將到達頂層的啟動類加載器摹恨,如果父類加載器可以完成類加載任務筋岛,就成功返回,倘若父類加載器無法完成此加載任務晒哄,子加載器才會嘗試自己去加載睁宰,這就是雙親委派模式
為什么叫雙親委派模型而不是父委派模型?
在閱讀相關虛擬機書籍以及大多數(shù)人的交流中寝凌,這種模型叫做雙親委派模型柒傻。但是我們在上述概念中得知,這里的請求委托僅僅給了父類加載器去執(zhí)行较木,而并不是雙親诅愚。那為什么叫做雙親委派模型呢?
對于這個問題處于好奇劫映,所以去網(wǎng)絡中去搜素了一番违孝,這樣寫道:
這是個很蛋疼的翻譯問題,實際上在oracle官方文檔上泳赋,人家是這樣描述的:
The Java platform uses a delegation model for loading classes. The basic idea is that every class loader has a “parent” class loader. When loading a class, a class loader first “delegates” the search for the class to its parent class loader before attempting to find the class itself.
用谷歌進行的機翻是這樣的:
Java平臺使用委托模型來加載類雌桑。 基本思想是每個類加載器都有一個“父”類加載器。 加載類時祖今,類加載器首先將對類的搜索“委派”給其父類加載器校坑,然后再嘗試查找類本身拣技。
所以很多博客是以父委派模型去描述
雙親委派模型提及的父類并不是繼承關系
上面提及了父類加載器,以Java的特性可能會有人誤認為為繼承(extend)
這里類加載器之間的父子關系一般不會以繼承.(Inheritance)的關系來實現(xiàn);而是都使用組合(Composition)關系來復用父加載器的代碼耍目。
以下代碼取自書中:
{
// First, check if the class has already been loaded 先判斷class是否已經(jīng)被加載過了
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false); //去委托它的父類去加載
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
一膏斤、它們分別加載了什么?
轉載自:https://zhuanlan.zhihu.com/p/73359363
類加載器是通過類的全限定名(或者說絕對路徑)來找到一個class文件的邪驮∧妫可以直接打印啟動類加載器BootstrapClassLoader的加載路徑看看:
URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (URL url : urls) {
System.out.println(url);
}
輸出結果(%20是空格):
file:/C:/Program%20Files/Java/jre1.8.0_131/lib/resources.jar
file:/C:/Program%20Files/Java/jre1.8.0_131/lib/rt.jar
file:/C:/Program%20Files/Java/jre1.8.0_131/lib/sunrsasign.jar
file:/C:/Program%20Files/Java/jre1.8.0_131/lib/jsse.jar
file:/C:/Program%20Files/Java/jre1.8.0_131/lib/jce.jar
file:/C:/Program%20Files/Java/jre1.8.0_131/lib/charsets.jar
file:/C:/Program%20Files/Java/jre1.8.0_131/lib/jfr.jar
file:/C:/Program%20Files/Java/jre1.8.0_131/classes
可以看到,啟動類加載器加載的是jre和jre/lib目錄下的核心庫毅访,具體路徑要看你的jre安裝在哪里沮榜。再打印一下擴展類加載器ExtentionClassLoader的加載路徑看看:
URL[] urls = ((URLClassLoader) ClassLoader.getSystemClassLoader().getParent()).getURLs();
for (URL url : urls) {
System.out.println(url);
}
輸出結果:
file:/C:/Program%20Files/Java/jre1.8.0_131/lib/ext/access-bridge-64.jar
file:/C:/Program%20Files/Java/jre1.8.0_131/lib/ext/cldrdata.jar
file:/C:/Program%20Files/Java/jre1.8.0_131/lib/ext/dnsns.jar
file:/C:/Program%20Files/Java/jre1.8.0_131/lib/ext/dns_sd.jar
file:/C:/Program%20Files/Java/jre1.8.0_131/lib/ext/jaccess.jar
file:/C:/Program%20Files/Java/jre1.8.0_131/lib/ext/jfxrt.jar
file:/C:/Program%20Files/Java/jre1.8.0_131/lib/ext/localedata.jar
file:/C:/Program%20Files/Java/jre1.8.0_131/lib/ext/nashorn.jar
file:/C:/Program%20Files/Java/jre1.8.0_131/lib/ext/sunec.jar
file:/C:/Program%20Files/Java/jre1.8.0_131/lib/ext/sunjce_provider.jar
file:/C:/Program%20Files/Java/jre1.8.0_131/lib/ext/sunmscapi.jar
file:/C:/Program%20Files/Java/jre1.8.0_131/lib/ext/sunpkcs11.jar
file:/C:/Program%20Files/Java/jre1.8.0_131/lib/ext/zipfs.jar
很明顯,擴展類加載器加載的是jre/lib/ext目錄下的擴展包喻粹。這些類庫具體是什么不重要蟆融,只需要知道不同的類庫可能是被不同的類加載器加載的。
JVM是怎么知道我們把JRE安裝到哪里了呢守呜?因為你安裝完JDK之后配置了環(huán)境變量靶退帧!那些 JAVA_HOME查乒、CLASSPATH 之類的就是干這個用的冕末。
最后是AppClassLoader:
URL[] urls = ((URLClassLoader) ClassLoader.getSystemClassLoader()).getURLs();
for (URL url : urls) {
System.out.println(url);
}
輸出結果:
file:/D:/JavaWorkSpace/PicklePee/bin/
這是當前java工程的bin目錄,也就是我們自己的Java代碼編譯成的class文件所在侣颂。
雙親委派模型的好處
使用雙親委派模型來組織類加載器之間的關系档桃,:有一個顯而易見的好處就是Java類隨著它的類加載器一起具備 了一種帶有優(yōu)先級的層次關系。例如類java.lang .Object,.它存放在:rt.jar之中憔晒,無論哪一個類加載器要加載這個類藻肄,最終都是委派給處于模型最頂端的啟動類加載器進行加載,因此Object類在程序的各種類加載器環(huán)境中都是同一個類拒担。相反嘹屯,如果沒有使用雙親委派模型,由各個類加載器自行去加載的話从撼,如果用戶自己編寫了一個稱為java.lang.Object的類州弟,并放在程序的ClassPath中;那系統(tǒng)中將會出現(xiàn)多個不同的Object類, Java類型體系中最基礎的行為也就無法保證,應用程序也將會變得一片混亂低零。如果讀者有興趣的話婆翔,可以嘗試去編寫-一個與rt.jar類庫中已有類重名的Java類,將會發(fā)現(xiàn)可以正常編譯掏婶,但永遠無法被加載運行啃奴。
如果你自己寫的一個類與核心類庫中的類重名,會發(fā)現(xiàn)這個類可以被正常編譯雄妥,但永遠無法被加載運行最蕾。因為你寫的這個類不會被應用類加載器加載依溯,而是被委托到頂層,被啟動類加載器在核心類庫中找到了瘟则。如果沒有雙親委托機制來確保類的全局唯一性黎炉,誰都可以編寫一個java.lang.Object類放在classpath下,那應用程序就亂套了醋拧。
從安全的角度講慷嗜,通過雙親委托機制,Java虛擬機總是先從最可信的Java核心API查找類型趁仙,可以防止不可信的類假扮被信任的類對系統(tǒng)造成危害洪添。
破壞雙親委派模型
第一次破壞:
由于雙親委派模型是在JDK1.2之后才被引入的垦页,而類加載器和抽象類java.lang.ClassLoader則在JDK1.0時代就已經(jīng)存在雀费,面對已經(jīng)存在的用戶自定義類加載器的實現(xiàn)代碼,Java設計者引入雙親委派模型時不得不做出一些妥協(xié)痊焊。在此之前盏袄,用戶去繼承java.lang.ClassLoader的唯一目的就是為了重寫loadClass()方法,因為虛擬機在進行類加載的時候會調用加載器的私有方法loadClassInternal()薄啥,而這個方法唯一邏輯就是去調用自己的loadClass()辕羽。
第二次破壞:
雙親委派模型的第二次“被破壞”是由這個模型自身的缺陷所導致的,雙親委派很好地解決了各個類加載器的基礎類的同一問題(越基礎的類由越上層的加載器進行加載)垄惧,基礎類之所以稱為“基礎”刁愿,是因為它們總是作為被用戶代碼調用的API,但世事往往沒有絕對的完美到逊。
如果基礎類又要調用回用戶的代碼铣口,那該么辦?
一個典型的例子就是JNDI服務觉壶,JNDI現(xiàn)在已經(jīng)是Java的標準服務脑题,
它的代碼由啟動類加載器去加載(在JDK1.3時放進去的rt.jar),但JNDI的目的就是對資源進行集中管理和查找铜靶,它需要調用由獨立廠商實現(xiàn)并部署在應用程序的ClassPath下的JNDI接口提供者的代碼叔遂,但啟動類加載器不可能“認識”這些代碼。
為了解決這個問題争剿,Java設計團隊只好引入了一個不太優(yōu)雅的設計:線程上下文類加載器(Thread Context ClassLoader)已艰。這個類加載器可以通過java.lang.Thread類的setContextClassLoader()方法進行設置,如果創(chuàng)建線程時還未設置蚕苇,他將會從父線程中繼承一個旗芬,如果在應用程序的全局范圍內都沒有設置過的話,那這個類加載器默認就是應用程序類加載器捆蜀。
有了線程上下文加載器疮丛,JNDI服務就可以使用它去加載所需要的SPI代碼幔嫂,也就是父類加載器請求子類加載器去完成類加載的動作,這種行為實際上就是打通了雙親委派模型層次結構來逆向使用類加載器誊薄,實際上已經(jīng)違背了雙親委派模型的一般性原則履恩,但這也是無可奈何的事情。Java中所有涉及SPI的加載動作基本上都采用這種方式呢蔫,例如JNDI切心、JDBC、JCE片吊、JAXB和JBI等绽昏。
第三次破壞:
雙親委派模型的第三次“被破壞”是由于用戶對程序動態(tài)性的追求導致的,這里所說的“動態(tài)性”指的是當前一些非城渭梗“熱門”的名詞:代碼熱替換全谤、模塊熱部署等,簡答的說就是機器不用重啟爷贫,只要部署上就能用认然。
OSGi實現(xiàn)模塊化熱部署的關鍵則是它自定義的類加載器機制的實現(xiàn)。每一個程序模塊(Bundle)都有一個自己的類加載器漫萄,當需要更換一個Bundle時卷员,就把Bundle連同類加載器一起換掉以實現(xiàn)代碼的熱替換。在OSGi幻境下腾务,類加載器不再是雙親委派模型中的樹狀結構毕骡,而是進一步發(fā)展為更加復雜的網(wǎng)狀結構,當受到類加載請求時岩瘦,OSGi將按照下面的順序進行類搜索:
1)將java.*開頭的類委派給父類加載器加載未巫。
2)否則,將委派列表名單內的類委派給父類加載器加載担钮。
3)否則橱赠,將Import列表中的類委派給Export這個類的Bundle的類加載器加載。
4)否則箫津,查找當前Bundle的ClassPath狭姨,使用自己的類加載器加載。
5)否則苏遥,查找類是否在自己的Fragment Bundle中饼拍,如果在,則委派給Fragment Bundle的類加載器加載田炭。
6)否則师抄,查找Dynamic Import列表的Bundle,委派給對應Bundle的類加載器加載教硫。
7)否則叨吮,類加載器失敗辆布。
JDBC為什么要破壞雙親委派模型
問題背景
在JDBC 4.0之后實際上我們不需要再調用Class.forName來加載驅動程序了,我們只需要把驅動的jar包放到工程的類加載路徑里茶鉴,那么驅動就會被自動加載锋玲。
這個自動加載采用的技術叫做SPI,數(shù)據(jù)庫驅動廠商也都做了更新涵叮〔氧澹可以看一下jar包里面的META-INF/services目錄,里面有一個java.sql.Driver的文件割粮,文件里面包含了驅動的全路徑名盾碗。
使用上,我們只需要通過下面一句就可以創(chuàng)建數(shù)據(jù)庫的連接:
Connection con =
DriverManager.getConnection(url , username , password ) ;
問題解答
因為類加載器受到加載范圍的限制舀瓢,在某些情況下父類加載器無法加載到需要的文件廷雅,這時候就需要委托子類加載器去加載class文件。
JDBC的Driver接口定義在JDK中氢伟,其實現(xiàn)由各個數(shù)據(jù)庫的服務商來提供榜轿,比如MySQL驅動包幽歼。DriverManager 類中要加載各個實現(xiàn)了Driver接口的類朵锣,然后進行管理,但是DriverManager位于 JAVA_HOME中jre/lib/rt.jar 包甸私,由BootStrap類加載器加載诚些,而其Driver接口的實現(xiàn)類是位于服務商提供的 Jar 包,根據(jù)類加載機制皇型,當被裝載的類引用了另外一個類的時候诬烹,虛擬機就會使用裝載第一個類的類裝載器裝載被引用的類。也就是說BootStrap類加載器還要去加載jar包中的Driver接口的實現(xiàn)類弃鸦。我們知道绞吁,BootStrap類加載器默認只負責加載 $JAVA_HOME中jre/lib/rt.jar 里所有的class,所以需要由子類加載器去加載Driver實現(xiàn)唬格,這就破壞了雙親委派模型家破。
查看DriverManager類的源碼,看到在使用DriverManager的時候會觸發(fā)其靜態(tài)代碼塊购岗,調用 loadInitialDrivers() 方法汰聋,并調用ServiceLoader.load(Driver.class) 加載所有在META-INF/services/java.sql.Driver 文件里邊的類到JVM內存,完成驅動的自動加載喊积。
每個Tomcat的webappClassLoader加載自己的目錄下的class文件烹困,不會傳遞給父類加載器。
事實上乾吻,tomcat之所以造了一堆自己的classloader髓梅,大致是出于下面三類目的:
對于各個 webapp中的 class和 lib拟蜻,需要相互隔離,不能出現(xiàn)一個應用中加載的類庫會影響另一個應用的情況枯饿,而對于許多應用瞭郑,需要有共享的lib以便不浪費資源。
與 jvm一樣的安全性問題鸭你。使用單獨的 classloader去裝載 tomcat自身的類庫屈张,以免其他惡意或無意的破壞;
熱部署袱巨。相信大家一定為 tomcat修改文件不用重啟就自動重新裝載類庫而驚嘆吧阁谆。
為什么要自定義加載類
我們需要的類不一定存放在已經(jīng)設置好的classPath下(有系統(tǒng)類加載器AppClassLoader加載的路徑),對于自定義路徑中的class類文件的加載愉老,我們需要自己的ClassLoader
有時我們不一定是從類文件中讀取類场绿,可能是從網(wǎng)絡的輸入流中讀取類,這就需要做一些加密和解密操作嫉入,這就需要自己實現(xiàn)加載類的邏輯焰盗,當然其他的特殊處理也同樣適用。
可以定義類的實現(xiàn)機制咒林,實現(xiàn)類的熱部署,如OSGi中的bundle模塊就是通過實現(xiàn)自己的ClassLoader實現(xiàn)的熬拒。