Java類加載器ClassLoader總結

JAVA類裝載方式,有兩種:java

1.隱式裝載, 程序在運行過程當中當碰到經過new 等方式生成對象時,隱式調用類裝載器加載對應的類到jvm中。 2.顯式裝載, 經過class.forname()等方法,顯式加載須要的類算法

類加載的動態性體現:apache

一個應用程序老是由n多個類組成,Java程序啓動時,並非一次把全部的類所有加載後再運行,它老是先把保證程序運行的基礎類一次性加載到jvm中,其它類等到jvm用到的時候再加載,這樣的好處是節省了內存的開銷,由於java最先就是爲嵌入式系統而設計的,內存寶貴,這是一種能夠理解的機制,而用到時再加載這也是java動態性的一種體現api

java類裝載器數組

 

JDK 默認提供了以下幾種ClassLoader安全

  1. Bootstrp loader
    Bootstrp加載器是用C++語言寫的,它是在Java虛擬機啓動後初始化的,它主要負責加載%JAVA_HOME%/jre/lib,-Xbootclasspath參數指定的路徑以及%JAVA_HOME%/jre/classes中的類。
    網絡

  1. ExtClassLoader  
    Bootstrp loader加載ExtClassLoader,而且將ExtClassLoader的父加載器設置爲Bootstrp loader.ExtClassLoader是用Java寫的,具體來講就是 sun.misc.Launcher$ExtClassLoader,ExtClassLoader主要加載%JAVA_HOME%/jre/lib/ext,此路徑下的全部classes目錄以及java.ext.dirs系統變量指定的路徑中類庫。
    jvm

  2. AppClassLoader 
    Bootstrp loader加載完ExtClassLoader後,就會加載AppClassLoader,而且將AppClassLoader的父加載器指定爲 ExtClassLoader。AppClassLoader也是用Java寫成的,它的實現類是 sun.misc.Launcher$AppClassLoader,另外咱們知道ClassLoader中有個getSystemClassLoader方法,此方法返回的正是AppclassLoader.AppClassLoader主要負責加載classpath所指定的位置的類或者是jar文檔,它也是Java程序默認的類加載器。
    ide

綜上所述,它們之間的關係能夠經過下圖形象的描述:ui

 

 

爲何要有三個類加載器,一方面是分工,各自負責各自的區塊,另外一方面爲了實現委託模型。

 


 類加載器之間是如何協調工做的

前面說了,java中有三個類加載器,問題就來了,碰到一個類須要加載時,它們之間是如何協調工做的,即java是如何區分一個類該由哪一個類加載器來完成呢。 在這裏java採用了委託模型機制,這個機制簡單來說,就是「類裝載器有載入類的需求時,會先請示其Parent使用其搜索路徑幫忙載入,若是Parent 找不到,那麼才由本身依照本身的搜索路徑搜索類

下面舉一個例子來講明,爲了更好的理解,先弄清楚幾行代碼:

 
 
 
 
 
Java
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Public class Test{
 
    Public static void main(String[] arg){
 
      ClassLoader c  = Test.class.getClassLoader();  //獲取Test類的類加載器
 
        System.out.println(c);
 
      ClassLoader c1 = c.getParent();  //獲取c這個類加載器的父類加載器
 
        System.out.println(c1);
 
      ClassLoader c2 = c1.getParent();//獲取c1這個類加載器的父類加載器
 
        System.out.println(c2);
 
  }
 
}

運行結果:

 
 
 
 
 
Java
 
1
2
3
4
5
……AppClassLoader……
 
……ExtClassLoader……
 
Null

能夠看出Test是由AppClassLoader加載器加載的,AppClassLoaderParent 加載器是 ExtClassLoader,可是ExtClassLoaderParent爲 null 是怎麼回事呵,朋友們留意的話,前面有提到Bootstrap Loader是用C++語言寫的,依java的觀點來看,邏輯上並不存在Bootstrap Loader的類實體,因此在java程序代碼裏試圖打印出其內容時,咱們就會看到輸出爲null

類裝載器ClassLoader(一個抽象類)描述一下JVM加載class文件的原理機制

類裝載器就是尋找類或接口字節碼文件進行解析並構造JVM內部對象表示的組件,在java中類裝載器把一個類裝入JVM,通過如下步驟:

一、裝載:查找和導入Class文件 二、連接:其中解析步驟是能夠選擇的 (a)檢查:檢查載入的class文件數據的正確性 (b)準備:給類的靜態變量分配存儲空間 (c)解析:將符號引用轉成直接引用 三、初始化:對靜態變量,靜態代碼塊執行初始化工做

類裝載工做由ClassLoder和其子類負責。JVM在運行時會產生三個ClassLoader:根裝載器ExtClassLoader(擴展類裝載器)和AppClassLoader,其中根裝載器不是ClassLoader的子類,由C++編寫,所以在java中看不到他,負責裝載JRE的核心類庫,如JRE目錄下的rt.jar,charsets.jar等。ExtClassLoaderClassLoder的子類,負責裝載JRE擴展目錄ext下的jar類包;AppClassLoader負責裝載classpath路徑下的類包,這三個類裝載器存在父子層級關係****,即根裝載器是ExtClassLoader的父裝載器,ExtClassLoader是AppClassLoader的父裝載器。默認狀況下使用AppClassLoader裝載應用程序的類

Java裝載類使用「全盤負責委託機制」。「全盤負責」是指當一個ClassLoder裝載一個類時,除非顯示的使用另一個ClassLoder,該類所依賴及引用的類也由這個ClassLoder載入;「委託機制」是指先委託父類裝載器尋找目標類,只有在找不到的狀況下才從本身的類路徑中查找並裝載目標類。這一點是從安全方面考慮的,試想若是一我的寫了一個惡意的基礎類(如java.lang.String)並加載到JVM將會引發嚴重的後果,但有了全盤負責制,java.lang.String永遠是由根裝載器來裝載,避免以上狀況發生 除了JVM默認的三個ClassLoder之外,第三方能夠編寫本身的類裝載器,以實現一些特殊的需求。類文件被裝載解析後,在JVM中都有一個對應的java.lang.Class對象,提供了類結構信息的描述。數組,枚舉及基本數據類型,甚至void都擁有對應的Class對象。Class類沒有public的構造方法,Class對象是在裝載類時由JVM經過調用類裝載器中的defineClass()方法自動構造的。

 

爲何要使用這種雙親委託模式呢?
由於這樣能夠避免重複加載,當父親已經加載了該類的時候,就沒有必要子ClassLoader再加載一次。
考慮到安全因素,咱們試想一下,若是不使用這種委託模式,那咱們就能夠隨時使用自定義的String來動態替代java核心api中定義類型,這樣會存在很是大的安全隱患,而雙親委託的方式,就能夠避免這種狀況,由於String已經在啓動時被加載,因此用戶自定義類是沒法加載一個自定義的ClassLoader。

 

思考:假如咱們本身寫了一個java.lang.String的類,咱們是否能夠替換調JDK自己的類?
答案是否認的。咱們不能實現。爲何呢?我看不少網上解釋是說雙親委託機制解決這個問題,其實不是很是的準確。由於雙親委託機制是能夠打破的,你徹底能夠本身寫一個classLoader來加載本身寫的java.lang.String類,可是你會發現也不會加載成功,具體就是由於針對java.*開頭的類,jvm的實現中已經保證了必須由bootstrp來加載。
 
 定義自已的ClassLoader
 
既然JVM已經提供了默認的類加載器,爲何還要定義自已的類加載器呢?
 
由於Java中提供的默認ClassLoader,只加載指定目錄下的jar和class,若是咱們想加載其它位置的類或jar時,好比:我要加載網絡上的一個class文件,經過動態加載到內存以後,要調用這個類中的方法實現個人業務邏輯。在這樣的狀況下,默認的ClassLoader就不能知足咱們的需求了,因此須要定義本身的ClassLoader。
 
定義自已的類加載器分爲兩步:
 
一、繼承java.lang.ClassLoader
 
二、重寫父類的findClass方法
 
讀者可能在這裏有疑問,父類有那麼多方法,爲何恰恰只重寫findClass方法?
 
由於JDK已經在loadClass方法中幫咱們實現了ClassLoader搜索類的算法,當在loadClass方法中搜索不到類時,loadClass方法就會調用findClass方法來搜索類,因此咱們只需重寫該方法便可。如沒有特殊的要求,通常不建議重寫loadClass搜索類的算法。
 

線程上下文類加載器

  線程上下文類加載器(context class loader)是從 JDK 1.2 開始引入的。類 java.lang.Thread中的方法 getContextClassLoader()和 setContextClassLoader(ClassLoader cl)用來獲取和設置線程的上下文類加載器。若是沒有經過 setContextClassLoader(ClassLoader cl)方法進行設置的話,線程將繼承其父線程的上下文類加載器。Java 應用運行的初始線程的上下文類加載器是系統類加載器。在線程中運行的代碼能夠經過此類加載器來加載類和資源。

  前面提到的類加載器的代理模式並不能解決 Java 應用開發中會遇到的類加載器的所有問題。Java 提供了不少服務提供者接口(Service Provider Interface,SPI),容許第三方爲這些接口提供實現。常見的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。這些 SPI 的接口由 Java 核心庫來提供,如 JAXP 的 SPI 接口定義包含在 javax.xml.parsers包中。這些 SPI 的實現代碼極可能是做爲 Java 應用所依賴的 jar 包被包含進來,能夠經過類路徑(CLASSPATH)來找到,如實現了 JAXP SPI 的 Apache Xerces所包含的 jar 包。SPI 接口中的代碼常常須要加載具體的實現類。如 JAXP 中的 javax.xml.parsers.DocumentBuilderFactory類中的 newInstance()方法用來生成一個新的 DocumentBuilderFactory的實例。這裏的實例的真正的類是繼承自 javax.xml.parsers.DocumentBuilderFactory,由 SPI 的實現所提供的。如在 Apache Xerces 中,實現的類是 org.apache.xerces.jaxp.DocumentBuilderFactoryImpl。而問題在於,SPI 的接口是 Java 核心庫的一部分,是由引導類加載器來加載的;SPI 實現的 Java 類通常是由系統類加載器來加載的。引導類加載器是沒法找到 SPI 的實現類的,由於它只加載 Java 的核心庫。它也不能代理給系統類加載器,由於它是系統類加載器的祖先類加載器。也就是說,類加載器的代理模式沒法解決這個問題。

  線程上下文類加載器正好解決了這個問題。若是不作任何的設置,Java 應用的線程的上下文類加載器默認就是系統上下文類加載器。在 SPI 接口的代碼中使用線程上下文類加載器,就能夠成功的加載到 SPI 實現的類。線程上下文類加載器在不少 SPI 的實現中都會用到。

 

類加載器與Web容器

  對於運行在 Java EE容器中的 Web 應用來講,類加載器的實現方式與通常的 Java 應用有所不一樣。不一樣的 Web 容器的實現方式也會有所不一樣。以 Apache Tomcat 來講,每一個 Web 應用都有一個對應的類加載器實例。該類加載器也使用代理模式,所不一樣的是它是首先嚐試去加載某個類,若是找不到再代理給父類加載器。這與通常類加載器的順序是相反的。這是 Java Servlet 規範中的推薦作法,其目的是使得 Web 應用本身的類的優先級高於 Web 容器提供的類。這種代理模式的一個例外是:Java 核心庫的類是不在查找範圍以內的。這也是爲了保證 Java 核心庫的類型安全。
  絕大多數狀況下,Web 應用的開發人員不須要考慮與類加載器相關的細節。下面給出幾條簡單的原則:
  (1)每一個 Web 應用本身的 Java 類文件和使用的庫的 jar 包,分別放在 WEB-INF/classes和 WEB-INF/lib目錄下面。
  (2)多個應用共享的 Java 類文件和 jar 包,分別放在 Web 容器指定的由全部 Web 應用共享的目錄下面。
  (3)當出現找不到類的錯誤時,檢查當前類的類加載器和當前線程的上下文類加載器是否正確。

 

 

類加載器與OSGi

 

  OSGi是 Java 上的動態模塊系統。它爲開發人員提供了面向服務和基於組件的運行環境,並提供標準的方式用來管理軟件的生命週期。OSGi 已經被實現和部署在不少產品上,在開源社區也獲得了普遍的支持。Eclipse就是基於OSGi 技術來構建的。
  OSGi 中的每一個模塊(bundle)都包含 Java 包和類。模塊能夠聲明它所依賴的須要導入(import)的其它模塊的 Java 包和類(經過 Import-Package),也能夠聲明導出(export)本身的包和類,供其它模塊使用(經過 Export-Package)。也就是說須要可以隱藏和共享一個模塊中的某些 Java 包和類。這是經過 OSGi 特有的類加載器機制來實現的。OSGi 中的每一個模塊都有對應的一個類加載器。它負責加載模塊本身包含的 Java 包和類。當它須要加載 Java 核心庫的類時(以 java開頭的包和類),它會代理給父類加載器(一般是啓動類加載器)來完成。當它須要加載所導入的 Java 類時,它會代理給導出此 Java 類的模塊來完成加載。模塊也能夠顯式的聲明某些 Java 包和類,必須由父類加載器來加載。只須要設置系統屬性 org.osgi.framework.bootdelegation的值便可。
  假設有兩個模塊 bundleA 和 bundleB,它們都有本身對應的類加載器 classLoaderA 和 classLoaderB。在 bundleA 中包含類 com.bundleA.Sample,而且該類被聲明爲導出的,也就是說能夠被其它模塊所使用的。bundleB 聲明瞭導入 bundleA 提供的類 com.bundleA.Sample,幷包含一個類 com.bundleB.NewSample繼承自 com.bundleA.Sample。在 bundleB 啓動的時候,其類加載器 classLoaderB 須要加載類 com.bundleB.NewSample,進而須要加載類 com.bundleA.Sample。因爲 bundleB 聲明瞭類 com.bundleA.Sample是導入的,classLoaderB 把加載類 com.bundleA.Sample的工做代理給導出該類的 bundleA 的類加載器 classLoaderA。classLoaderA 在其模塊內部查找類 com.bundleA.Sample並定義它,所獲得的類 com.bundleA.Sample實例就能夠被全部聲明導入了此類的模塊使用。對於以 java開頭的類,都是由父類加載器來加載的。若是聲明瞭系統屬性 org.osgi.framework.bootdelegation=com.example.core.*,那麼對於包 com.example.core中的類,都是由父類加載器來完成的。
  OSGi 模塊的這種類加載器結構,使得一個類的不一樣版本能夠共存在 Java 虛擬機中,帶來了很大的靈活性。不過它的這種不一樣,也會給開發人員帶來一些麻煩,尤爲當模塊須要使用第三方提供的庫的時候。下面提供幾條比較好的建議:   (1)若是一個類庫只有一個模塊使用,把該類庫的 jar 包放在模塊中,在 Bundle-ClassPath中指明便可。   (2)若是一個類庫被多個模塊共用,能夠爲這個類庫單獨的建立一個模塊,把其它模塊須要用到的 Java 包聲明爲導出的。其它模塊聲明導入這些類。   (3)若是類庫提供了 SPI 接口,而且利用線程上下文類加載器來加載 SPI 實現的 Java 類,有可能會找不到 Java 類。若是出現了 NoClassDefFoundError異常,首先檢查當前線程的上下文類加載器是否正確。經過 Thread.currentThread().getContextClassLoader()就能夠獲得該類加載器。該類加載器應該是該模塊對應的類加載器。若是不是的話,能夠首先經過 class.getClassLoader()來獲得模塊對應的類加載器,再經過 Thread.currentThread().setContextClassLoader()來設置當前線程的上下文類加載器。

相關文章
相關標籤/搜索