類的加載過程指通過一個類的全限定名來獲取描述此類的二進制字節流,並將其轉化為方法區的數據結構,進而生成一個java.lang.Class對象作為方法區這個類各種數據訪問的入口。這個過程通過Java中的類加載器(ClassLoader)來完成。
類裝載器是用來把類(class)裝載進JVM的。JVM規范定義了兩種類型的類裝載器:啟動內裝載器(bootstrap)和用戶自定義裝載器(user-defined class loader)。
一、Java默認提供的三個ClassLoader
JVM在運行時會產生三個ClassLoader:Bootstrap ClassLoader、Extension ClassLoader和AppClassLoader(System ClassLoader)。
1、 Bootstrap ClassLoader(啟動類加載器)負責將%JAVA_HOME%/lib目錄中或-Xbootclasspath中參數指定的路徑中的,並且是虛擬機識別的(按名稱)類庫加載到JVM中。
也可以通過-Xbootclasspath參數定義。該ClassLoader不能被Java代碼實例化,因為它是JVM本身的一部分。
2、Extension ClassLoader(擴展類加載器)負責加載%JAVA_HOME%/lib/ext中的所有類庫;
只要jar包放置這個位置,就會被虛擬機加載。一個常見的、類似的問題是,你將mysql的低版本驅動不小心放置在這兒,但你的Web應用程序的lib下有一個新的jdbc驅動,但怎么都報錯,譬如不支持JDBC2.0的 DataSource,這時你就要當心你的新jdbc可能並沒有被加載。這就是ClassLoader的delegate現象。常見的有log4j、 common-log、dbcp會出現問題,因為它們很容易被人塞到這個ext目錄,或是Tomcat下的common/lib目錄
3、Application ClassLoader:也稱為System ClassLoaer(加載%CLASSPATH%路徑的類庫)以及其它自定義的ClassLoader。缺省情況下,它是用戶創建的任何ClassLoader的父ClassLoader。
我們創建的standalone應用的main class缺省情況下也是由它加載(通過Thread.currentThread().getContextClassLoader()查看)。實際開發中用ClassLoader更多時候是用其加載classpath下的資源,特別是配置文件,如ClassLoader.getResource(),比FileInputStream直接。
類加載器 classloader 是具有層次結構的,也就是父子關系。其中,Bootstrap 是所有類加載器的父親。如下圖所示:
注意: 除了Java默認提供的三個ClassLoader之外,用戶還可以根據需要定義自已的ClassLoader,而這些自定義的ClassLoader都必須繼承自java.lang.ClassLoader類,也包括Java提供的另外二個ClassLoader(Extension ClassLoader和App ClassLoader)在內,但是Bootstrap ClassLoader不繼承自ClassLoader,因為它不是一個普通的Java類,底層由C++編寫,已嵌入到了JVM內核當中,當JVM啟動后,Bootstrap ClassLoader也隨着啟動,負責加載完核心類庫后,並構造Extension ClassLoader和App ClassLoader類加載器。
二、雙親委托模型
Java中ClassLoader的加載采用了雙親委托機制,采用雙親委托機制加載類的時候采用如下的幾個步驟:
1、當前ClassLoader首先從自己已經加載的類中查詢是否此類已經加載,如果已經加載則直接返回原來已經加載的類;
2、當前classLoader的緩存中沒有找到被加載的類的時候,委托父類加載器去加載,父類加載器采用同樣的策略,首先查看自己的緩存,然后委托父類的父類去加載,一直到bootstrp ClassLoader.
3、當所有的父類加載器都沒有加載的時候,再由當前的類加載器加載,並將其放入它自己的緩存中,以便下次有加載請求的時候直接返回。
說到這里大家可能會想,Java為什么要采用這樣的委托機制?理解這個問題,我們引入另外一個關於Classloader的概念“命名空間”, 它是指要確定某一個類,需要類的全限定名以及加載此類的ClassLoader來共同確定。也就是說即使兩個類的全限定名是相同的,但是因為不同的 ClassLoader加載了此類,那么在JVM中它是不同的類。明白了命名空間以后,我們再來看看委托模型。采用了委托模型以后加大了不同的 ClassLoader的交互能力,比如上面說的,我們JDK本生提供的類庫,比如hashmap,linkedlist等等,這些類由bootstrp 類加載器加載了以后,無論你程序中有多少個類加載器,那么這些類其實都是可以共享的,這樣就避免了不同的類加載器加載了同樣名字的不同類以后造成混亂。
JVM中類加載的機制——雙親委派模型。這個模型要求除了Bootstrap ClassLoader外,其余的類加載器都要有自己的父加載器。子加載器通過組合來復用父加載器的代碼,而不是使用繼承。在某個類加載器加載class文件時,它首先委托父加載器去加載這個類,依次傳遞到頂層類加載器(Bootstrap)。如果頂層加載不了(它的搜索范圍中找不到此類),子加載器才會嘗試加載這個類。
當JVM請求某個ClassLoader實例使用這種模型來加載某個類時,首先檢查該類是否已經被當前類加載器加載,如果沒有被加載,則先委托給她的父類加載器即調用parent.loadClass()方法,這樣一直請求調用到請求頂層類加載ClassLoader#findBootStrapClassOrNull,如果這個方法依然加載不了,則會調用ClassLoader#findClass()方法,這個方法再找不到則會拋出ClassNotFoundException異常,但是這里的異常會被捕獲,然后返回給委托發起者,最后由當前類加載器的findClass()方法類加載類,如果找不到則拋出ClassNotFoundException異常。
Class查找的位置和順序依次是:Cache、parent、self
三、ClassLoader加載類的原理
1、原理介紹
ClassLoader使用的是雙親委托模型來搜索類的,每個ClassLoader實例都有一個父類加載器的引用(不是繼承的關系,是一個包含的關系),虛擬機內置的類加載器(Bootstrap ClassLoader)本身沒有父類加載器,但可以用作其它ClassLoader實例的的父類加載器。當一個ClassLoader實例需要加載某個類時,它會試圖親自搜索某個類之前,先把這個任務委托給它的父類加載器,這個過程是由上至下依次檢查的,首先由最頂層的類加載器Bootstrap ClassLoader試圖加載,如果沒加載到,則把任務轉交給Extension ClassLoader試圖加載,如果也沒加載到,則轉交給App ClassLoader 進行加載,如果它也沒有加載得到的話,則返回給委托的發起者,由它到指定的文件系統或網絡等URL中加載該類。如果它們都沒有加載到這個類時,則拋出ClassNotFoundException異常。否則將這個找到的類生成一個類的定義,並將它加載到內存當中,最后返回這個類在內存中的Class實例對象。
2、為什么要使用雙親委托這種模型呢?
因為這樣可以避免重復加載,當父親已經加載了該類的時候,就沒有必要子ClassLoader再加載一次。考慮到安全因素,我們試想一下,如果不使用這種委托模式,那我們就可以隨時使用自定義的String來動態替代java核心api中定義的類型,這樣會存在非常大的安全隱患,而雙親委托的方式,就可以避免這種情況,因為String已經在啟動時就被引導類加載器(Bootstrcp ClassLoader)加載,所以用戶自定義的ClassLoader永遠也無法加載一個自己寫的String,除非你改變JDK中ClassLoader搜索類的默認算法。
3、JVM在搜索類的時候,如何判斷兩個class相同呢?
JVM在判定兩個class是否相同時,不僅要判斷兩個類名是否相同,而且要判斷是否由同一個類加載器實例加載的。只有兩者同時滿足的情況下,JVM才認為這兩個class是相同的。就算兩個class是同一份class字節碼,如果被兩個不同的ClassLoader實例所加載,JVM也會認為它們是兩個不同class。
比如網絡上的一個Java類org.classloader.simple.NetClassLoaderSimple,javac編譯之后生成字節碼文件NetClassLoaderSimple.class,ClassLoaderA和ClassLoaderB這兩個類加載器並讀取了NetClassLoaderSimple.class文件,並分別定義出了java.lang.Class實例來表示這個類,對於JVM來說,它們是兩個不同的實例對象,但它們確實是同一份字節碼文件,如果試圖將這個Class實例生成具體的對象進行轉換時,就會拋運行時異常java.lang.ClassCaseException,提示這是兩個不同的類型。
在一個單虛擬機環境下,標識一個類有兩個因素:class的全路徑名、該類的ClassLoader。
4、ClassLoader 體系架構
1、先檢查需要加載的類是否已經被加載,這個過程是從下->上;
2、如果沒有被加載,則委托父加載器加載,如果加載不了,再由自己加載, 這個過程是從上->下
四、自定義ClassLoader
為什么我們需要自定義類加載?
主要原因:1、需要加載外部的Class,JVM提供的默認ClassLoader只能加載指定目錄下的.jar和.class,如果我們想加載其它位置的class或者jar時,這些默認的類加載器是加載不到的(如果是文件格式必須配置到classpath)。例如:我們需要加載網絡上的一個class字節流;
2、需要實現Class的隔離性。目前我們常用的Web服務器,如tomcat、jetty都實現了自己定義的類加載,這些類加載主要完成以下三個功能:
A.實現加載Web應用指定目錄下的jar和class
B.實現部署在容器中的Web應用程共同使用的類庫的共享
C.實現部署在容器中各個Web應用程序自己私有類庫的相互隔離
如何自定義類加載?
- 繼承java.lang.ClassLoader
- 覆寫父類的findClass()方法
Java除了上面所說的默認提供的classloader以外,它還容許應用程序可以自定義classloader,那么要想自定義classloader我們需要通過繼承java.lang.ClassLoader來實現,接下來我們就來看看再自定義Classloader的時候,我們需要注意的幾個重要的方法:
1.loadClass 方法
loadClass method declare
public Class<?> loadClass(String name) throws ClassNotFoundException
上面是loadClass方法的原型聲明,上面所說的雙親委托機制的實現其實就實在此方法中實現的。下面我們就來看看此方法的代碼來看看它到底如何實現雙親委托的。
loadClass method implement
public Class<?> loadClass(String name) throws ClassNotFoundException { return loadClass(name, false); }
從上面可以看出loadClass方法調用了loadcClass(name,false)方法,那么接下來我們再來看看另外一個loadClass方法的實現。
Class loadClass(String name, boolean resolve)
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { // First, check if the class has already been loaded Class c = findLoadedClass(name); //檢查class是否已經被加載過了 if (c == null) { try { if (parent != null) { c = parent.loadClass(name, false); //如果沒有被加載,且指定了父類加載器,則委托父加載器加載。 } else { c = findBootstrapClass0(name);//如果沒有父類加載器,則委托bootstrap加載器加載 } } catch (ClassNotFoundException e) { // If still not found, then invoke findClass in order // to find the class. c = findClass(name);//如果父類加載沒有加載到,則通過自己的findClass來加載。 } } if (resolve) { resolveClass(c); } return c; }
上面的代碼,通過注釋可以清晰看出loadClass的雙親委托機制是如何工作的。 這里我們需要注意一點就是public Class<?> loadClass(String name) throws ClassNotFoundException沒有被標記為final,也就意味着我們是可以override這個方法的,也就是說雙親委托機制是可以打破的。另外上面注意到有個findClass方法,接下來我們就來說說這個方法到底是做什么的。
2.findClass
我們查看java.lang.ClassLoader的源代碼,我們發現findClass的實現如下:
protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); }
我們可以看出此方法默認的實現是直接拋出異常,其實這個方法就是留給我們應用程序來override的。那么具體的實現就看你的實現邏輯了,你可以從磁盤讀取,也可以從網絡上獲取class文件的字節流,獲取class二進制了以后就可以交給defineClass來實現進一步的加載。defineClass我們再下面再來描述。通過上面的分析,我們可以得出如下結論:
3.defineClass
我們首先還是來看看defineClass的源碼:
defineClass
protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError { return defineClass(name, b, off, len, null); }
從上面的代碼我們看出此方法被定義為了final,這也就意味着此方法不能被Override,其實這也是jvm留給我們的唯一的入口,通過這個唯 一的入口,jvm保證了類文件必須符合Java虛擬機規范規定的類的定義。此方法最后會調用native的方法來實現真正的類的加載工作。
五、不遵循“雙親委托機制”的場景
上面說了雙親委托機制主要是為了實現不同的ClassLoader之間加載的類的交互問題,被大家公用的類就交由父加載器去加載,但是Java中確實也存在父類加載器加載的類需要用到子加載器加載的類的情況。下面我們就來說說這種情況的發生。
Java中有一個SPI(Service Provider Interface)標准,使用了SPI的庫,比如JDBC,JNDI等,我們都知道JDBC需要第三方提供的驅動才可以,而驅動的jar包是放在我們應用程序本身的classpath的,而jdbc 本身的api是jdk提供的一部分,它已經被bootstrp加載了,那第三方廠商提供的實現類怎么加載呢?這里面JAVA引入了線程上下文類加載的概 念,線程類加載器默認會從父線程繼承,如果沒有指定的話,默認就是系統類加載器(AppClassLoader),這樣的話當加載第三方驅動的時候,就可 以通過線程的上下文類加載器來加載。
另外為了實現更靈活的類加載器OSGI以及一些Java app server也打破了雙親委托機制。
另:啟動時如果加上如下系統參數,即可跟蹤JVM類的加載
-XX:+TraceClassLoading
參考鏈接:http://welcome66.iteye.com/blog/2230055
http://www.sczyh30.com/posts/Java/jvm-classloader-parent-delegation-model/
http://blog.csdn.net/xyang81/article/details/7292380
https://segmentfault.com/a/1190000002579346
https://yq.aliyun.com/articles/2890?spm=5176.8067842.taqmain.22.e56iyr