如何加載一個Class文件
在之前的文章中吏祸,筆者介紹了Java虛擬機(jī)--類加載機(jī)制对蒲,闡述了一個類加載到底做了哪些事情!
但是贡翘,關(guān)于類加載器的只是蹈矮,并沒有做任何介紹,只是說了下會在后面的文章中進(jìn)行單獨(dú)闡述含滴。那么丐巫,本篇的意義就是來告訴大家類加載器的實(shí)現(xiàn)。
首先碑韵,我們來簡單的回顧下類加載機(jī)制中的內(nèi)容缎脾。
類加載機(jī)制
虛擬機(jī)把類的數(shù)據(jù)從.class文件加載到內(nèi)存,并對class文件中的數(shù)據(jù)進(jìn)行校驗联喘、轉(zhuǎn)換、解析叭喜、初始化等操作后蓖谢,最終形成可以被虛擬機(jī)識別并使用的Class對象的過程就叫做“虛擬機(jī)的類加載”,主要包括為3大階段啥辨。
階段一:加載
加載溉知,類加載器通過類的全限定名來獲取類的二進(jìn)制字節(jié)流腊嗡,獲取的方式可以通過jar包、war包卡者、網(wǎng)絡(luò)客们、JSP文件中獲取,絕大部分情況下是通過jar包恒傻、war包中獲取建邓。
獲取到字節(jié)流后,會將字節(jié)流中的信息轉(zhuǎn)化為方法區(qū)中的運(yùn)行時數(shù)據(jù)結(jié)構(gòu)官边。在內(nèi)存中,生成代表該類的Class對象契吉,作為訪問該類的數(shù)據(jù)入口捐晶。
階段二:連接
連接比較復(fù)雜,分為3個小階段:
驗證:確保被加載類的正確性惑灵,即確保被加載的類符合javac編譯的規(guī)范,可編譯通過的代碼胶哲。
準(zhǔn)備:為類的靜態(tài)變量分配內(nèi)存潭辈,并初始化為默認(rèn)值(零值)把敢。
解析:將類中的符號引用轉(zhuǎn)化為直接引用谅辣。
階段三:初始化
為類的靜態(tài)變量賦值,與連接階段中的的準(zhǔn)備不同柏副。此階段蚣录,代碼可debug查看。
如int類型的靜態(tài)變量static int x = 3荔泳,連接階段賦零值即為0虐杯,而初始化階段賦值即為3。
以上就是類加載機(jī)制的三大階段支子,而我們今天要將的類加載器存在于階段一中--加載达舒。可以說吞歼,沒有類加載器也就沒有了后續(xù)的流程塔猾,類加載器在Java虛擬機(jī)中起到了至關(guān)重要的作用。
類加載器
類加載器(class loader)將Java類從本地磁盤加載到Java虛擬機(jī)中糯俗,并同時創(chuàng)建了該類的Class對象,實(shí)現(xiàn)了“通過一個類的全限定類名來獲取此類的二進(jìn)制字節(jié)流”功能得湘。
類加載器是Java語言的一項創(chuàng)新,也是Java語言流程的重要原因之一摆马,在類層次劃分囤采、OSGI惩淳、熱部署、代碼加密等領(lǐng)域有著重要的作用代虾,成為Java不可或缺的一部分激蹲。
首先,我們來寫一個測試類含蓉,來看下類加載器项郊,ClassLoaderTest測試類:
public class ClassLoaderTest {
public static void main(String[] args) {
ClassLoader loader = ClassLoaderTest.class.getClassLoader();
while (true) {
System.out.println(loader);
if(loader==null){
break;
}
loader = loader.getParent();
}
}
}
運(yùn)行結(jié)果:
sun.misc.Launcher$AppClassLoader@41dee0d7
sun.misc.Launcher$ExtClassLoader@f7b650a
null
首先獲取到的是AppClassLoader類加載器着降,緊接著又獲取的是ExtClassLoader類加載器,最后獲取的對象為null
為什么為null呢任洞,后續(xù)來解答交掏!
接下來,我們來看看在Java體系中到底有哪些類加載器钱骂。
類加載的分類
在Java中,類加載器可以分為兩大類见秽,一類是由Java系統(tǒng)提供的,另外一類是自定義的步责,由開發(fā)人員編寫提供的禀苦。
系統(tǒng)類加載器:
引導(dǎo)類加載器(bootstrap class loader):用來加載Java的核心庫,由于引導(dǎo)類加載器涉及到虛擬機(jī)本地實(shí)現(xiàn)細(xì)節(jié)省核,開發(fā)者無法直接獲取到啟動類加載器的引用,所以不允許直接通過引用進(jìn)行操作,不繼承自java.lang.ClassLoader(這就是上面例子中為什么最后取到的對象為null的原因)邻储。負(fù)責(zé)加載<Java_Runtime_Home>/lib下面的類庫到內(nèi)存中吨娜,或-Xbootclasspath選項指定的jar包裝入內(nèi)存;
擴(kuò)展類加載器(extensions class loader):用來加載Java的擴(kuò)展庫陪毡,由sun.misc.Launcher$ExtClassLoader來實(shí)現(xiàn)。負(fù)責(zé)加載
<Java_Runtime_Home>/lib/ext毡琉,或-Djava.ext.dirs選項指定目錄下的jar包裝入到內(nèi)存中桅滋;
系統(tǒng)類加載器(system class loader):用來加載Java應(yīng)用的類路徑(CLASSPATH)的Java類,由sun.misc.Launcher$AppClassLoader來實(shí)現(xiàn)丐谋。一般來說煌珊,Java應(yīng)用中的類都是由它來完成加載的,可以通過ClassLoader.getSystemClassLoader()來獲取吏饿。
自定義類加載器:
自定義類加載器(User Custom ClassLoader):開發(fā)人員可以通過繼承java.lang.ClassLoader類的方式實(shí)現(xiàn)自己的類加載器,以滿足一些特殊的需求陨倡。在程序運(yùn)行期間, 通過自定義的java.lang.ClassLoader子類動態(tài)加載class文件。
java.lang.ClassLoader類介紹
方法 | 說明 |
---|---|
getParent() | 返回該類加載器的父類加載器兴革。 |
loadClass(String name) | 加載名稱為 name的類杂曲,返回的結(jié)果是 java.lang.Class類的實(shí)例袁余。 |
findClass(String name) | 查找名稱為 name的類,返回的結(jié)果是 java.lang.Class類的實(shí)例棚饵。 |
findLoadedClass(String name) | 查找名稱為 name的已經(jīng)被加載過的類掩完,返回的結(jié)果是 java.lang.Class類的實(shí)例。 |
defineClass(String name, byte[] b, int off, int len) | 把字節(jié)數(shù)組 b中的內(nèi)容轉(zhuǎn)換成 Java 類欣硼,返回的結(jié)果是 java.lang.Class類的實(shí)例恶阴。這個方法被聲明為 final的。 |
resolveClass(Class<?> c) | 鏈接指定的 Java 類焦匈。 |
以上為ClassLoader對于類加載功能的主要方法介紹括授。
在我們的應(yīng)用程序中岩饼,都是由這4種類加載器互相配合進(jìn)行加載,這4種類加載器在虛擬機(jī)中維護(hù)了一種父子關(guān)系版述,這種關(guān)系叫做“雙親委派模型”寞冯。下面渴析,我們就來看看什么是雙親委派模型俭茧。
雙親委派模型
下面的圖片中,展示的就是“雙親委派模型”午磁,模型中呈現(xiàn)出Java體系架構(gòu)中的四大類加載器的關(guān)系毡们,除了頂層的引導(dǎo)類加載器之外,其余類加載都需要有父加載器存在衙熔,但是此子父類關(guān)系并不是通過java代碼中繼承的方式實(shí)現(xiàn)红氯。具體如何實(shí)現(xiàn),后面講解痢甘。
知道了類加載器的結(jié)構(gòu)模型产阱,那么該模型在代碼整個Java體系中如何工作呢构蹬?
工作流程:一個類加載器收到了類加載請求庄敛,它首先不會自己去嘗試加載這個類,而是把這個類加載請求委派給其父類加載器去完成藻烤,每一個層的類加載器都是如此头滔,依次向父類加載器傳遞,最終所有的類加載請求都會傳送到頂層的啟動類加載器(bootstrap)中兴猩,只有當(dāng)父加載器反饋無法完成這個類加載請求時早歇,子類加載器才會嘗試自己去進(jìn)行類加載操作讨勤,如果子類加載器也依舊無法完成潭千,則代碼層面就會拋出異常刨晴。
此時垛玻,你會不會感到疑惑?為啥兒子自己的活不去干帚桩,而首先交給他爹去完成呢账嚎?這么做的目的何在?
在Java體系中疼邀,雙親委派模型保證了類的唯一性,將Java類與它的類加載器綁定到了一起旁振,當(dāng)父類加載器加載完成后拐袜,子類加載器不會再次加載梢薪。此外,雙親委派模型還保證了Java框架的安全性甜攀。例如:java.lang.Object類琐馆,無論是上述哪個類加載器要加載這個類,最終都會委派給模型中的啟動類加載器去加載姥敛,因此java.lang.Object類在程序中保證了唯一性彤敛。
相反与帆,如果沒有使用該模型玄糟,而是由各個類加載器自行去加載的話阵翎,那么系統(tǒng)中就會出現(xiàn)不同的java.lang.Object類,類的唯一性被打破郭卫,Java體系中的基本行為就得不到保證背稼。例如:,比較兩個類是否“相等”词疼,只有在這兩個類是由同一個類加載器加載的前提下才有意義帘腹。否則,即使兩個類來源于同一個Class文件舵盈,被同一個虛擬機(jī)加載秽晚,只要加載他們的類加載器不同,那這兩個類就必定不相等。涉及到“類相等”的方法有:Class對象的equals()方法锨能、isAssignableFrom()方法址遇、isInstance()方法以及instanceof對象所屬關(guān)系判定斋竞。
試想一下,如果我們自定義一個java.lang.Object類會怎么樣浸剩?(其實(shí)我們自定義的java.lang.Object類無法在程序中被導(dǎo)入,只能模擬定義java.lang.Object類--java.lang.ObjectTest)
當(dāng)JVM請求類加載進(jìn)行自定義的類加載時吏恭,雙親委派模型會將請求傳遞到啟動類加載器中重罪,但是啟動類加載器默認(rèn)只加載<Java_Runtime_Home>/lib路徑下的類,在該路徑下并沒有ObjectTest類搅幅,所以啟動類加載器無法加載茄唐,只能向下傳遞給子類加載器砸讳,最終會將請求傳遞到系統(tǒng)類加載器中簿寂,但是系統(tǒng)類加載器也無法進(jìn)行加載,會拋出異常常遂。
為什么克胳,why?
這是因為以java.開頭的是核心API包捏雌,需要訪問權(quán)限笆搓,強(qiáng)制加載會拋出異常,任何以java.開頭的包都會報錯:
Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang
此異常是代碼層面拋出的肤频,并不是native方法虛擬機(jī)底層拋出宵荒,源碼可見(ClassLoader類):
if ((name != null) && name.startsWith("java.")) {
throw new SecurityException
("Prohibited package name: " +
name.substring(0, name.lastIndexOf('.')));
}
此時,你會不會又突發(fā)奇想侠讯,我自己定義一個類少孝,放在<Java_Runtime_Home>/lib路徑下會如何?
編寫代碼袁翁,并打成jar包粱胜,jar包的名稱就叫做 jiaboyan.jar:
jar cvf jiaboyan.jar com\jiaboyan\test\ObjectTest.class
接下來狐树,把jiaboyan.jar包放入到<Java_Runtime_Home>/lib下抑钟,也同時放入到<Java_Runtime_Home>/lib/ext,并同時保留截圖中的代碼幻件,如圖所示:
執(zhí)行main()方法绰沥,結(jié)果如下:
sun.misc.Launcher$AppClassLoader@8fd9b4d
從輸出可以看出贺待,放置到<Java_Runtime_Home>/lib目錄下的ObjectTest.class類并沒有被啟動類加載器加載徽曲,而是由擴(kuò)展類加載器加載了。
why麸塞?不是說了委派給最頂層的類加載進(jìn)行加載嗎秃臣?其實(shí),這是由于虛擬機(jī)出于安全角度考慮哪工,不會加載<Java_Runtime_Home>/lib中的陌生類奥此,開發(fā)者把自定義的類放置到此目錄下啟動類加載器是不會進(jìn)行加載的。
下一篇正勒,將會對類加載器源碼進(jìn)行分析5迷骸I迪场章贞!