摘要:作用負(fù)責(zé)將加載到中審查每個(gè)類由誰加載父優(yōu)先的等級加載機(jī)制將字節(jié)碼重新解析成統(tǒng)一要求的對象格式類結(jié)構(gòu)分析為了更好的理解類的加載機(jī)制,我們來深入研究一下和他的方法。就算兩個(gè)是同一份字節(jié)碼,如果被兩個(gè)不同的實(shí)例所加載,也會(huì)認(rèn)為它們是兩個(gè)不同。
申明:本文首發(fā)于 詳細(xì)深入分析 ClassLoader 工作機(jī)制 ,如有轉(zhuǎn)載,注明原出處即可,謝謝配合。
什么是 ClassLoader ?大家都知道,當(dāng)我們寫好一個(gè) Java 程序之后,不是管是 C/S 還是 B/S 應(yīng)用,都是由若干個(gè) .class 文件組織而成的一個(gè)完整的 Java 應(yīng)用程序,當(dāng)程序在運(yùn)行時(shí),即會(huì)調(diào)用該程序的一個(gè)入口函數(shù)來調(diào)用系統(tǒng)的相關(guān)功能,而這些功能都被封裝在不同的 class 文件當(dāng)中,所以經(jīng)常要從這個(gè) class 文件中要調(diào)用另外一個(gè) class 文件中的方法,如果另外一個(gè)文件不存在的,則會(huì)引發(fā)系統(tǒng)異常。而程序在啟動(dòng)的時(shí)候,并不會(huì)一次性加載程序所要用的所有class文件,而是根據(jù)程序的需要,通過Java的類加載機(jī)制(ClassLoader)來動(dòng)態(tài)加載某個(gè) class 文件到內(nèi)存當(dāng)中的,從而只有 class 文件被載入到了內(nèi)存之后,才能被其它 class 所引用。所以 ClassLoader 就是用來動(dòng)態(tài)加載 class 文件到內(nèi)存當(dāng)中用的。
ClassLoader 作用:負(fù)責(zé)將 Class 加載到 JVM 中
審查每個(gè)類由誰加載(父優(yōu)先的等級加載機(jī)制)
將 Class 字節(jié)碼重新解析成 JVM 統(tǒng)一要求的對象格式
1、ClassLoader 類結(jié)構(gòu)分析為了更好的理解類的加載機(jī)制,我們來深入研究一下 ClassLoader 和他的方法。
public abstract class ClassLoader
ClassLoader類是一個(gè)抽象類,sun公司是這么解釋這個(gè)類的:
/** * A class loader is an object that is responsible for loading classes. The * class ClassLoader is an abstract class. Given the binary name of a class, a class loader should attempt to * locate or generate data that constitutes a definition for the class. A * typical strategy is to transform the name into a file name and then read a * "class file" of that name from a file system. **/
大致意思如下:
class loader 是一個(gè)負(fù)責(zé)加載 classes 的對象,ClassLoader 類是一個(gè)抽象類,需要給出類的二進(jìn)制名稱,class loader 嘗試定位或者產(chǎn)生一個(gè) class 的數(shù)據(jù),一個(gè)典型的策略是把二進(jìn)制名字轉(zhuǎn)換成文件名然后到文件系統(tǒng)中找到該文件。
以下是 ClassLoader 常用到的幾個(gè)方法及其重載方法:
ClassLoader
defineClass(byte[], int, int) 把字節(jié)數(shù)組 b中的內(nèi)容轉(zhuǎn)換成 Java 類,返回的結(jié)果是java.lang.Class類的實(shí)
例。這個(gè)方法被聲明為final的
findClass(String name) 查找名稱為 name的類,返回的結(jié)果是java.lang.Class類的實(shí)例
loadClass(String name) 加載名稱為 name的類,返回的結(jié)果是java.lang.Class類的實(shí)例
resolveClass(Class>) 鏈接指定的 Java 類
其中 defineClass 方法用來將 byte 字節(jié)流解析成 JVM 能夠識別的 Class 對象,有了這個(gè)方法意味著我們不僅僅可以通過 class 文件實(shí)例化對象,還可以通過其他方式實(shí)例化對象,如果我們通過網(wǎng)絡(luò)接收到一個(gè)類的字節(jié)碼,拿到這個(gè)字節(jié)碼流直接創(chuàng)建類的 Class 對象形式實(shí)例化對象。如果直接調(diào)用這個(gè)方法生成類的 Class 對象,這個(gè)類的 Class 對象還沒有 resolve ,這個(gè) resolve 將會(huì)在這個(gè)對象真正實(shí)例化時(shí)才進(jìn)行。
接下來我們看loadClass方法的實(shí)現(xiàn)方式:
protected Class> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded 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; } }
該方法大概意思:
2、ClassLoader 的等級加載機(jī)制 Java默認(rèn)提供的三個(gè)ClassLoader使用指定的二進(jìn)制名稱來加載類,這個(gè)方法的默認(rèn)實(shí)現(xiàn)按照以下順序查找類: 調(diào)用findLoadedClass(String) 方法檢查這個(gè)類是否被加載過 使用父加載器調(diào)用 loadClass(String) 方法,如果父加載器為 Null,類加載器裝載虛擬機(jī)內(nèi)置的加載器調(diào)用 findClass(String) 方法裝載類, 如果,按照以上的步驟成功的找到對應(yīng)的類,并且該方法接收的 resolve 參數(shù)的值為 true,那么就調(diào)用resolveClass(Class) 方法來處理類。 ClassLoader 的子類最好覆蓋 findClass(String) 而不是這個(gè)方法。 除非被重寫,這個(gè)方法默認(rèn)在整個(gè)裝載過程中都是同步的(線程安全的)。
BootStrap ClassLoader:稱為啟動(dòng)類加載器,是Java類加載層次中最頂層的類加載器,負(fù)責(zé)加載JDK中的核心類庫,如:rt.jar、resources.jar、charsets.jar等,可通過如下程序獲得該類加載器從哪些地方加載了相關(guān)的jar或class文件:
public class BootStrapTest { public static void main(String[] args) { URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs(); for (int i = 0; i < urls.length; i++) { System.out.println(urls[i].toExternalForm()); } } }
以下內(nèi)容是上述程序從本機(jī)JDK環(huán)境所獲得的結(jié)果:
其實(shí)上述結(jié)果也是通過查找 sun.boot.class.path 這個(gè)系統(tǒng)屬性所得知的。
System.out.println(System.getProperty("sun.boot.class.path"));
打印結(jié)果:C:Javajdk1.8.0_60jrelib esources.jar;C:Javajdk1.8.0_60jrelib t.jar;C:Javajdk1.8.0_60jrelibsunrsasign.jar;C:Javajdk1.8.0_60jrelibjsse.jar;C:Javajdk1.8.0_60jrelibjce.jar;C:Javajdk1.8.0_60jrelibcharsets.jar;C:Javajdk1.8.0_60jrelibjfr.jar;C:Javajdk1.8.0_60jreclasses
Extension ClassLoader:稱為擴(kuò)展類加載器,負(fù)責(zé)加載Java的擴(kuò)展類庫,Java 虛擬機(jī)的實(shí)現(xiàn)會(huì)提供一個(gè)擴(kuò)展庫目錄。該類加載器在此目錄里面查找并加載 Java 類。默認(rèn)加載JAVA_HOME/jre/lib/ext/目下的所有jar。
App ClassLoader:稱為系統(tǒng)類加載器,負(fù)責(zé)加載應(yīng)用程序classpath目錄下的所有jar和class文件。一般來說,Java 應(yīng)用的類都是由它來完成加載的??梢酝ㄟ^ ClassLoader.getSystemClassLoader()來獲取它。
?
除了系統(tǒng)提供的類加載器以外,開發(fā)人員可以通過繼承java.lang.ClassLoader類的方式實(shí)現(xiàn)自己的類加載器,以滿足一些特殊的需求。
除了引導(dǎo)類加載器之外,所有的類加載器都有一個(gè)父類加載器。 給出的 getParent()方法可以得到。對于
系統(tǒng)提供的類加載器來說,系統(tǒng)類加載器的父類加載器是擴(kuò)展類加載器,而擴(kuò)展類加載器的父類加載器是引導(dǎo)類加載器;對于開發(fā)人員編寫的類加載器來說,其父類加載器是加載此類加載器 Java 類的類加載器。因?yàn)轭惣虞d器 Java 類如同其它的 Java 類一樣,也是要由類加載器來加載的。一般來說,開發(fā)人員編寫的類加載器的父類加載器是系統(tǒng)類加載器。類加載器通過這種方式組織起來,形成樹狀結(jié)構(gòu)。樹的根節(jié)點(diǎn)就是引導(dǎo)類加載器。
?
ClassLoader加載類的原理ClassLoader使用的是雙親委托模型來搜索類的,每個(gè)ClassLoader實(shí)例都有一個(gè)父類加載器的引用(不是繼承的關(guān)系,是一個(gè)包含的關(guān)系),虛擬機(jī)內(nèi)置的類加載器(Bootstrap ClassLoader)本身沒有父類加載器,但可以用作其它ClassLoader實(shí)例的的父類加載器。當(dāng)一個(gè)ClassLoader實(shí)例需要加載某個(gè)類時(shí),它會(huì)試圖親自搜索某個(gè)類之前,先把這個(gè)任務(wù)委托給它的父類加載器,這個(gè)過程是由上至下依次檢查的,首先由最頂層的類加載器Bootstrap ClassLoader試圖加載,如果沒加載到,則把任務(wù)轉(zhuǎn)交給Extension ClassLoader試圖加載,如果也沒加載到,則轉(zhuǎn)交給App ClassLoader 進(jìn)行加載,如果它也沒有加載得到的話,則返回給委托的發(fā)起者,由它到指定的文件系統(tǒng)或網(wǎng)絡(luò)等URL中加載該類。如果它們都沒有加載到這個(gè)類時(shí),則拋出ClassNotFoundException異常。否則將這個(gè)找到的類生成一個(gè)類的定義,并將它加載到內(nèi)存當(dāng)中,最后返回這個(gè)類在內(nèi)存中的Class實(shí)例對象。
因?yàn)檫@樣可以避免重復(fù)加載,當(dāng)父親已經(jīng)加載了該類的時(shí)候,就沒有必要 ClassLoader再加載一次。考慮到安全因素,我們試想一下,如果不使用這種委托模式,那我們就可以隨時(shí)使用自定義的String來動(dòng)態(tài)替代java核心api中定義的類型,這樣會(huì)存在非常大的安全隱患,而雙親委托的方式,就可以避免這種情況,因?yàn)镾tring已經(jīng)在啟動(dòng)時(shí)就被引導(dǎo)類加載器(Bootstrcp ClassLoader)加載,所以用戶自定義的ClassLoader永遠(yuǎn)也無法加載一個(gè)自己寫的String,除非你改變JDK中ClassLoader搜索類的默認(rèn)算法。
JVM在判定兩個(gè)class是否相同時(shí),不僅要判斷兩個(gè)類名是否相同,而且要判斷是否由同一個(gè)類加載器實(shí)例加載的。只有兩者同時(shí)滿足的情況下,JVM才認(rèn)為這兩個(gè)class是相同的。就算兩個(gè)class是同一份class字節(jié)碼,如果被兩個(gè)不同的ClassLoader實(shí)例所加載,JVM也會(huì)認(rèn)為它們是兩個(gè)不同class。比如網(wǎng)絡(luò)上的一個(gè)Java類org.classloader.simple.NetClassLoaderSimple,javac編譯之后生成字節(jié)碼文件NetClassLoaderSimple.class,ClassLoaderA和ClassLoaderB這兩個(gè)類加載器并讀取了NetClassLoaderSimple.class文件,并分別定義出了java.lang.Class實(shí)例來表示這個(gè)類,對于JVM來說,它們是兩個(gè)不同的實(shí)例對象,但它們確實(shí)是同一份字節(jié)碼文件,如果試圖將這個(gè)Class實(shí)例生成具體的對象進(jìn)行轉(zhuǎn)換時(shí),就會(huì)拋運(yùn)行時(shí)異常java.lang.ClassCaseException,提示這是兩個(gè)不同的類型。現(xiàn)在通過實(shí)例來驗(yàn)證上述所描述的是否正確:
1)、在web服務(wù)器上建一個(gè)org.classloader.simple.NetClassLoaderSimple.java類
public class NetClassLoaderSimple { private NetClassLoaderSimple instance; public void setNetClassLoaderSimple(Object object){ this.instance = (NetClassLoaderSimple)object; } }
org.classloader.simple.NetClassLoaderSimple類的setNetClassLoaderSimple方法接收一個(gè)Object類型參數(shù),并將它強(qiáng)制轉(zhuǎn)換成org.classloader.simple.NetClassLoaderSimple類型。
2)、測試兩個(gè)class是否相同 NetWorkClassLoader.java
package classloader; public class NewworkClassLoaderTest { public static void main(String[] args) { try { //測試加載網(wǎng)絡(luò)中的class文件 String rootUrl = "http://localhost:8080/httpweb/classes"; String className = "org.classloader.simple.NetClassLoaderSimple"; NetworkClassLoader ncl1 = new NetworkClassLoader(rootUrl); NetworkClassLoader ncl2 = new NetworkClassLoader(rootUrl); Class> clazz1 = ncl1.loadClass(className); Class> clazz2 = ncl2.loadClass(className); Object obj1 = clazz1.newInstance(); Object obj2 = clazz2.newInstance(); clazz1.getMethod("setNetClassLoaderSimple", Object.class).invoke(obj1, obj2); } catch (Exception e) { e.printStackTrace(); } } }
首先獲得網(wǎng)絡(luò)上一個(gè)class文件的二進(jìn)制名稱,然后通過自定義的類加載器NetworkClassLoader創(chuàng)建兩個(gè)實(shí)例,并根據(jù)網(wǎng)絡(luò)地址分別加載這份class,并得到這兩個(gè)ClassLoader實(shí)例加載后生成的Class實(shí)例clazz1和clazz2,最后將這兩個(gè)Class實(shí)例分別生成具體的實(shí)例對象obj1和obj2,再通過反射調(diào)用clazz1中的setNetClassLoaderSimple方法。
3)、查看測試結(jié)果
結(jié)論:從結(jié)果中可以看出,運(yùn)行時(shí)拋出了java.lang.ClassCastException異常。雖然兩個(gè)對象obj1和 obj2的類的名字相同,但是這兩個(gè)類是由不同的類加載器實(shí)例來加載的,所以JVM認(rèn)為它們就是兩個(gè)不同的類。
了解了這一點(diǎn)之后,就可以理解代理模式的設(shè)計(jì)動(dòng)機(jī)了。代理模式是為了保證 Java 核心庫的類型安全。所有 Java 應(yīng)用都至少需要引用 java.lang.Object類,也就是說在運(yùn)行的時(shí)候,java.lang.Object這個(gè)類需要被加載到 Java 虛擬機(jī)中。如果這個(gè)加載過程由 Java 應(yīng)用自己的類加載器來完成的話,很可能就存在多個(gè)版本的 java.lang.Object類,而且這些類之間是不兼容的。通過代理模式,對于 Java 核心庫的類的加載工作由引導(dǎo)類加載器來統(tǒng)一完成,保證了Java 應(yīng)用所使用的都是同一個(gè)版本的 Java 核心庫的類,是互相兼容的。
不同的類加載器為相同名稱的類創(chuàng)建了額外的名稱空間。相同名稱的類可以并存在 Java 虛擬機(jī)中,只需要用不同的類加載器來加載它們即可。不同類加載器加載的類之間是不兼容的,這就相當(dāng)于在 Java 虛擬機(jī)內(nèi)部創(chuàng)建了一個(gè)個(gè)相互隔離的 Java 類空間。
ClassLoader的體系架構(gòu): 類加載器的樹狀組織結(jié)構(gòu)測試一:
public class ClassLoaderTree { public static void main(String[] args) { ClassLoader loader = ClassLoaderTree.class.getClassLoader(); while (loader!=null){ System.out.println(loader.toString()); loader = loader.getParent(); } System.out.println(loader); } }
每個(gè) Java 類都維護(hù)著一個(gè)指向定義它的類加載器的引用,通過 getClassLoader()方法就可以獲取到此引用。代碼中通過遞歸調(diào)用 getParent()方法來輸出全部的父類加載器。
結(jié)果是:
第一個(gè)輸出的是 ClassLoaderTree類的類加載器,即系統(tǒng)類加載器。它是sun.misc.Launcher$AppClassLoader類的實(shí)例;第二個(gè)輸出的是擴(kuò)展類加載器,是sun.misc.Launcher$ExtClassLoader類的實(shí)例。需要注意的是這里并沒有輸出引導(dǎo)類加載器,這是由于有些 JDK 的實(shí)現(xiàn)對于父類加載器是引導(dǎo)類加載器的情況,getParent()方法返回 null。第三行結(jié)果說明:ExtClassLoader的類加器是Bootstrap ClassLoader,因?yàn)?b>Bootstrap ClassLoader不是一個(gè)普通的Java類,所以ExtClassLoader的parent=null,所以第三行的打印結(jié)果為null就是這個(gè)原因。
測試二:
將ClassLoaderTree.class打包成ClassLoaderTree.jar,放到Extension ClassLoader的加載目錄下(JAVA_HOME/jre/lib/ext),然后重新運(yùn)行這個(gè)程序,得到的結(jié)果會(huì)是什么樣呢?
此處我在 IDEA 中的運(yùn)行結(jié)果還和上面的一樣,與文章 深入分析Java ClassLoader原理 中的有差距,具體原因未弄清楚,還希望讀者能夠親自測試。
那文章中的結(jié)果是:
打印結(jié)果分析:
為什么第一行的結(jié)果是ExtClassLoader呢?
因?yàn)?ClassLoader 的委托模型機(jī)制,當(dāng)我們要用 ClassLoaderTest.class 這個(gè)類的時(shí)候,AppClassLoader 在試圖加載之前,先委托給 Bootstrcp ClassLoader,Bootstracp ClassLoader 發(fā)現(xiàn)自己沒找到,它就告訴 ExtClassLoader,兄弟,我這里沒有這個(gè)類,你去加載看看,然后 Extension ClassLoader 拿著這個(gè)類去它指定的類路徑(JAVA_HOME/jre/lib/ext)試圖加載,唉,它發(fā)現(xiàn)在ClassLoaderTest.jar 這樣一個(gè)文件中包含 ClassLoaderTest.class 這樣的一個(gè)文件,然后它把找到的這個(gè)類加載到內(nèi)存當(dāng)中,并生成這個(gè)類的 Class 實(shí)例對象,最后把這個(gè)實(shí)例返回。所以 ClassLoaderTest.class 的類加載器是 ExtClassLoader。
第二行的結(jié)果為null,是因?yàn)镋xtClassLoader的父類加載器是Bootstrap ClassLoader。
JVM加載class文件的兩種方法;隱式加載, 程序在運(yùn)行過程中當(dāng)碰到通過new 等方式生成對象時(shí),隱式調(diào)用類裝載器加載對應(yīng)的類到j(luò)vm中。
顯式加載, 通過class.forname()、this.getClass.getClassLoader().loadClass()等方法顯式加載需要的類,或者我們自己實(shí)現(xiàn)的 ClassLoader 的 findlass() 方法。
下面介紹下 class.forName的加載類方法:
類加載的動(dòng)態(tài)性體現(xiàn):Class.forName是一個(gè)靜態(tài)方法,同樣可以用來加載類。該方法有兩種形式:Class.forName(String name,boolean initialize, ClassLoader loader)和Class.forName(String className)。第一種形式的參數(shù) name表示的是類的全名;initialize表示是否初始化類;loader表示加載時(shí)使用的類加載器。第二種形式則相當(dāng)于設(shè)置了參數(shù) initialize的值為 true,loader的值為當(dāng)前類的類加載器。Class.forName的一個(gè)很常見的用法是在加載數(shù)據(jù)庫驅(qū)動(dòng)的時(shí)候。如
Class.forName("org.apache.derby.jdbc.EmbeddedDriver")用來加載 Apache Derby 數(shù)據(jù)庫的驅(qū)動(dòng)。
一個(gè)應(yīng)用程序總是由n多個(gè)類組成,Java程序啟動(dòng)時(shí),并不是一次把所有的類全部加載后再運(yùn)行,它總是先把保證程序運(yùn)行的基礎(chǔ)類一次性加載到j(luò)vm中,其它類等到j(luò)vm用到的時(shí)候再加載,這樣的好處是節(jié)省了內(nèi)存的開銷,因?yàn)閖ava最早就是為嵌入式系統(tǒng)而設(shè)計(jì)的,內(nèi)存寶貴,這是一種可以理解的機(jī)制,而用到時(shí)再加載這也是java動(dòng)態(tài)性的一種體現(xiàn)。
3、如何加載 class 文件第一階段找到 .class 文件并把這個(gè)文件包含的字節(jié)碼加載到內(nèi)存中。
第二階段中分三步,字節(jié)碼驗(yàn)證;class 類數(shù)據(jù)結(jié)構(gòu)分析及相應(yīng)的內(nèi)存分配;最后的符號表的鏈接。
第三階段是類中靜態(tài)屬性和初始化賦值,以及靜態(tài)塊的執(zhí)行等。
3.1 、加載字節(jié)碼到內(nèi)存。。
3.2 、驗(yàn)證與分析字節(jié)碼驗(yàn)證,類裝入器對于類的字節(jié)碼要做很多檢測,以確保格式正確,行為正確。
類裝備,準(zhǔn)備代表每個(gè)類中定義的字段、方法和實(shí)現(xiàn)接口所必須的數(shù)據(jù)結(jié)構(gòu)。
解析,裝入器裝入類所引用的其他所有類。
4、常見加載類錯(cuò)誤分析 4.1 、 ClassNotFoundExecptionClassNotFoundExecption 異常是平常碰到的最多的。這個(gè)異常通常發(fā)生在顯示加載類的時(shí)候。
public class ClassNotFoundExceptionTest { public static void main(String[] args) { try { Class.forName("NotFoundClass"); }catch (ClassNotFoundException e){ e.printStackTrace(); } } }
顯示加載一個(gè)類通常有:
通過類 Class 中的 forName() 方法
通過類 ClassLoader 中的 loadClass() 方法
通過類 ClassLoader 中的 findSystemClass() 方法
出現(xiàn)這種錯(cuò)誤其實(shí)就是當(dāng) JVM 要加載指定文件的字節(jié)碼到內(nèi)存時(shí),并沒有找到這個(gè)文件對應(yīng)的字節(jié)碼,也就是這個(gè)文件并不存在。解決方法就是檢查在當(dāng)前的 classpath 目錄下有沒有指定的文件。
4.2 、 NoClassDefFoundError在JavaDoc中對NoClassDefFoundError的產(chǎn)生可能的情況就是使用new關(guān)鍵字、屬性引用某個(gè)類、繼承了某個(gè)接口或者類,以及方法的某個(gè)參數(shù)中引用了某個(gè)類,這時(shí)就會(huì)觸發(fā)JVM或者類加載器實(shí)例嘗試加載類型的定義,但是該定義卻沒有找到,影響了執(zhí)行路徑。換句話說,在編譯時(shí)這個(gè)類是能夠被找到的,但是在執(zhí)行時(shí)卻沒有找到。
解決這個(gè)錯(cuò)誤的方法就是確保每個(gè)類引用的類都在當(dāng)前的classpath下面。
4.3 、 UnsatisfiedLinkError該錯(cuò)誤通常是在 JVM 啟動(dòng)的時(shí)候,如果 JVM 中的某個(gè) lib 刪除了,就有可能報(bào)這個(gè)錯(cuò)誤。
public class UnsatisfiedLinkErrorTest { public native void nativeMethod(); static { System.loadLibrary("NoLib"); } public static void main(String[] args) { new UnsatisfiedLinkErrorTest().nativeMethod(); //解析native標(biāo)識的方法時(shí)JVM找不到對應(yīng)的庫文件 } }4.4 、 ClassCastException
該錯(cuò)誤通常出現(xiàn)強(qiáng)制類型轉(zhuǎn)換時(shí)出現(xiàn)這個(gè)錯(cuò)誤。
public class ClassCastExceptionTest { public static Map m = new HashMap(){ { put("a", "2"); } }; public static void main(String[] args) { Integer integer = (Integer) m.get("a"); //將m強(qiáng)制轉(zhuǎn)換成Integer類型 System.out.println(integer); } }
注意:JVM 在做類型轉(zhuǎn)換時(shí)的規(guī)則:
對于普通對象,對象必須是目標(biāo)類的實(shí)例或目標(biāo)類的子類的實(shí)例。如果目標(biāo)類是接口,那么會(huì)把它當(dāng)作實(shí)現(xiàn)了該接口的一個(gè)子類。
對于數(shù)組類型,目標(biāo)類必須是數(shù)組類型或 java.lang.Object、java.lang.Cloneable、java.io.Serializable。
如果不滿足上面的規(guī)則,JVM 就會(huì)報(bào)錯(cuò),有兩種方式可避免錯(cuò)誤:
在容器類型中顯式的指明這個(gè)容器所包含的對象類型。
先通過 instanceof 檢查是不是目標(biāo)類型,然后再進(jìn)行強(qiáng)制類型的轉(zhuǎn)換。
上面代碼中改成如下就可以避免錯(cuò)誤了:
4.5 、 ExceptionInInitializerErrorpublic class ExceptionInInitializerErrorTest { public static Map m = new HashMap(){{ m.put("a", "2"); }}; public static void main(String[] args) { Integer integer = (Integer) m.get("a"); System.out.println(integer); } }
在初始化這個(gè)類時(shí),給靜態(tài)屬性 m 賦值時(shí)出現(xiàn)了異常導(dǎo)致拋出錯(cuò)誤 ExceptionInInitializerError。
4.6 NoSuchMethodErrorNoSuchMethodError代表這個(gè)類型確實(shí)存在,但是一個(gè)不正確的版本被加載了。為了解決這個(gè)問題我們可以使用 ‘-verbose:class’ 來判斷該JVM加載的到底是哪個(gè)版本。
4.7 LinkageError有時(shí)候事情會(huì)變得更糟,和 ClassCastException 本質(zhì)一樣,加載自不同位置的相同類在同一段邏輯(比如:方法)中交互時(shí),會(huì)出現(xiàn) LinkageError 。
LinkageError 需要觀察哪個(gè)類被不同的類加載器加載了,在哪個(gè)方法或者調(diào)用處發(fā)生(交匯)的,然后才能想解決方法,解決方法無外乎兩種。第一,還是不同的類加載器加載,但是相互不再交匯影響,這里需要針對發(fā)生問題的地方做一些改動(dòng),比如更換實(shí)現(xiàn)方式,避免出現(xiàn)上述問題;第二,沖突的類需要由一個(gè)Parent類加載器進(jìn)行加載。LinkageError 和ClassCastException 本質(zhì)是一樣的,加載自不同類加載器的類型,在同一個(gè)類的方法或者調(diào)用中出現(xiàn),如果有轉(zhuǎn)型操作那么就會(huì)拋 ClassCastException ,如果是直接的方法調(diào)用處的參數(shù)或者返回值解析,那么就會(huì)產(chǎn)生 LinkageError 。
5、常用的 ClassLoader 分析。。參見書籍《深入分析Java Web技術(shù)內(nèi)幕》
6、如何實(shí)現(xiàn)自己的 ClassLoaderClassLoader 能夠完成的事情有以下情況:
在自定義路徑下查找自定義的class類文件。
對我們自己要加載的類做特殊處理。
可以定義類的實(shí)現(xiàn)機(jī)制。
雖然在絕大多數(shù)情況下,系統(tǒng)默認(rèn)提供的類加載器實(shí)現(xiàn)已經(jīng)可以滿足需求。但是在某些情況下,您還是需要為應(yīng)用開發(fā)出自己的類加載器。比如您的應(yīng)用通過網(wǎng)絡(luò)來傳輸 Java 類的字節(jié)代碼,為了保證安全性,這些字節(jié)代碼經(jīng)過了加密處理。這個(gè)時(shí)候您就需要自己的類加載器來從某個(gè)網(wǎng)絡(luò)地址上讀取加密后的字節(jié)代碼,接著進(jìn)行解密和驗(yàn)證,最后定義出要在 Java 虛擬機(jī)中運(yùn)行的類來。
定義自已的類加載器分為兩步:
1、繼承java.lang.ClassLoader
2、重寫父類的findClass方法
加載存儲(chǔ)在文件系統(tǒng)上的 Java 字節(jié)代碼。
public class FileSystemClassLoader extends ClassLoader { private String rootDir; public FileSystemClassLoader(String rootDir){ this.rootDir = rootDir; } protected Class> findClass(String name) throws ClassNotFoundException { byte[] classData = getClassData(name); if (classData == null){ throw new ClassNotFoundException(); } else { return defineClass(name, classData, 0, classData.length); } } private byte[] getClassData(String className) { String path = classNameToPath(className); try { InputStream ins = new FileInputStream(path); ByteArrayOutputStream baos = new ByteArrayOutputStream(); int bufferSize = 4096; byte[] buffer = new byte[bufferSize]; int bytesNumRead = 0; while ((bytesNumRead = ins.read(buffer)) != -1){ baos.write(buffer, 0, bytesNumRead); } return baos.toByteArray(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return null; } private String classNameToPath(String className) { return rootDir + File.separatorChar + className.replace(".", File.separatorChar) + ".class"; } }
類 FileSystemClassLoader繼承自類java.lang.ClassLoader。java.lang.ClassLoader類的方法loadClass()封裝了前面提到的代理模式的實(shí)現(xiàn)。該方法會(huì)首先調(diào)用 findLoadedClass()方法來檢查該類是否已經(jīng)被加載過;如果沒有加載過的話,會(huì)調(diào)用父類加載器的loadClass()方法來嘗試加載該類;如果父類加載器無法加載該類的話,就調(diào)用 findClass()方法來查找該類。因此,為了保證類加載器都正確實(shí)現(xiàn)代理模式,在開發(fā)自己的類加載器時(shí),最好不要覆寫 loadClass()方法,而是覆寫findClass()方法。
類 FileSystemClassLoader的 findClass()方法首先根據(jù)類的全名在硬盤上查找類的字節(jié)代碼文件(.class 文
件),然后讀取該文件內(nèi)容,最后通過 defineClass()方法來把這些字節(jié)代碼轉(zhuǎn)換成 java.lang.Class類的實(shí)例。
一個(gè)網(wǎng)絡(luò)類加載器來說明如何通過類加載器來實(shí)現(xiàn)組件的動(dòng)態(tài)更新。即基本的場景是:Java 字節(jié)代碼(.class)文件存放在服務(wù)器上,客戶端通過網(wǎng)絡(luò)的方式獲取字節(jié)代碼并執(zhí)行。當(dāng)有版本更新的時(shí)候,只需要替換掉服務(wù)
器上保存的文件即可。通過類加載器可以比較簡單的實(shí)現(xiàn)這種需求。
類 NetworkClassLoader 負(fù)責(zé)通過網(wǎng)絡(luò)下載 Java 類字節(jié)代碼并定義出 Java 類。它的實(shí)現(xiàn)與FileSystemClassLoader 類似。在通過 NetworkClassLoader 加載了某個(gè)版本的類之后,一般有兩種做法來使用它。第一種做法是使用 Java 反射 API。另外一種做法是使用接口。需要注意的是,并不能直接在客戶端代碼中引用從服務(wù)器上下載的類,因?yàn)榭蛻舳舜a的類加載器找不到這些類。使用 Java 反射 API 可以直接調(diào)用 Java 類的
方法。而使用接口的做法則是把接口的類放在客戶端中,從服務(wù)器上加載實(shí)現(xiàn)此接口的不同版本的類。在客戶端通過相同的接口來使用這些實(shí)現(xiàn)類。
網(wǎng)絡(luò)類加載器的代碼:ClassLoader
7、類加載器與Web容器對于運(yùn)行在 Java EE?容器中的 Web 應(yīng)用來說,類加載器的實(shí)現(xiàn)方式與一般的 Java 應(yīng)用有所不同。不同的 Web 容器的實(shí)現(xiàn)方式也會(huì)有所不同。以 Apache Tomcat 來說,每個(gè)Web 應(yīng)用都有一個(gè)對應(yīng)的類加載器實(shí)例。該類加載器也使用代理模式,所不同的是它是首先嘗試去加載某個(gè)類,如果找不到再代理給父類加載器。這與一般類加載器的順序是相反的。這是 Java Servlet 規(guī)范中的推薦做法,其目的是使得Web 應(yīng)用自己的類的優(yōu)先級高于 Web 容器提供的類。這種代理模式的一個(gè)例外是:Java 核心庫的類是不在查找范圍之內(nèi)的。這也是為了保證 Java 核心庫的類型安全。
絕大多數(shù)情況下,Web 應(yīng)用的開發(fā)人員不需要考慮與類加載器相關(guān)的細(xì)節(jié)。下面給出幾條簡單的原則:
每個(gè) Web 應(yīng)用自己的 Java 類文件和使用的庫的 jar 包,分別放在 WEB-INF/classes和 WEB-INF/lib目錄下面。
多個(gè)應(yīng)用共享的 Java 類文件和 jar 包,分別放在 Web 容器指定的由所有 Web 應(yīng)用共享的目錄下面。
當(dāng)出現(xiàn)找不到類的錯(cuò)誤時(shí),檢查當(dāng)前類的類加載器和當(dāng)前線程的上下文類加載器是否正確
8、總結(jié)本篇文章詳細(xì)深入的介紹了 ClassLoader 的工作機(jī)制,還寫了如何自己實(shí)現(xiàn)所需的 ClassLoader 。
參考資料1、深度分析 Java 的 ClassLoader 機(jī)制(源碼級別)
2、深入淺出ClassLoader
3、深入探討 Java 類加載器
4、深入分析Java ClassLoader原理
5、《深入分析 Java Web 技術(shù)內(nèi)幕》修訂版 —— 深入分析 ClassLoader 工作機(jī)制
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/66723.html
摘要:在之前,它是一個(gè)備受爭議的關(guān)鍵字,因?yàn)樵诔绦蛑惺褂盟占骼斫夂驮矸治龊喎Q,是后提供的面向大內(nèi)存區(qū)數(shù)到數(shù)多核系統(tǒng)的收集器,能夠?qū)崿F(xiàn)軟停頓目標(biāo)收集并且具有高吞吐量具有更可預(yù)測的停頓時(shí)間。 35 個(gè) Java 代碼性能優(yōu)化總結(jié) 優(yōu)化代碼可以減小代碼的體積,提高代碼運(yùn)行的效率。 從 JVM 內(nèi)存模型談線程安全 小白哥帶你打通任督二脈 Java使用讀寫鎖替代同步鎖 應(yīng)用情景 前一陣有個(gè)做...
摘要:最終形成可以被虛擬機(jī)最直接使用的類型的過程就是虛擬機(jī)的類加載機(jī)制。即重寫一個(gè)類加載器的方法驗(yàn)證驗(yàn)證是連接階段的第一步,這一階段的目的是為了確保文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求,并且不會(huì)危害虛擬機(jī)自身的安全。 《深入理解Java虛擬機(jī):JVM高級特性與最佳實(shí)踐(第二版》讀書筆記與常見相關(guān)面試題總結(jié) 本節(jié)常見面試題(推薦帶著問題閱讀,問題答案在文中都有提到): 簡單說說類加載過...
摘要:的打包結(jié)構(gòu)改動(dòng)是這個(gè)引入的這個(gè)的本意是簡化的繼承關(guān)系,以一種直觀的優(yōu)先的方式來實(shí)現(xiàn),同時(shí)打包結(jié)構(gòu)和傳統(tǒng)的包應(yīng)用更接近。目前的繼承關(guān)系帶來的一些影響有很多用戶可能會(huì)發(fā)現(xiàn),一些代碼在里跑得很好,但是在實(shí)際部署運(yùn)行時(shí)不工作。 前言 對spring boot本身啟動(dòng)原理的分析,請參考:http://hengyunabc.github.io/s... Spring boot里的ClassLoad...
摘要:而字節(jié)碼運(yùn)行在之上,所以不用關(guān)心字節(jié)碼是在哪個(gè)操作系統(tǒng)編譯的,只要符合規(guī)范,那么,這個(gè)字節(jié)碼文件就是可運(yùn)行的。好處防止內(nèi)存中出現(xiàn)多份同樣的字節(jié)碼安全性角度特別說明類加載器在成功加載某個(gè)類之后,會(huì)把得到的類的實(shí)例緩存起來。 前言 只有光頭才能變強(qiáng) JVM在準(zhǔn)備面試的時(shí)候就有看了,一直沒時(shí)間寫筆記?,F(xiàn)在到了一家公司實(shí)習(xí),閑的時(shí)候就寫寫,刷刷JVM博客,刷刷電子書。 學(xué)習(xí)JVM的目的也很簡單...
閱讀 1458·2021-09-22 16:04
閱讀 2809·2019-08-30 15:44
閱讀 896·2019-08-30 15:43
閱讀 776·2019-08-29 15:24
閱讀 1857·2019-08-29 14:07
閱讀 1146·2019-08-29 12:30
閱讀 1741·2019-08-29 11:15
閱讀 2751·2019-08-28 18:08