Java Classloader原理分析

   類的加載過程指通過一個類的全限定名來獲取描述此類的二進制字節流,並將其轉化爲方法區的數據結構,進而生成一個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

發佈了11 篇原創文章 · 獲贊 69 · 訪問量 14萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章