Java類加載器ClassLoader總結


JAVA類裝載方式,有兩種:

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

類加載的動態性體現:

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

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系統變量指定的路徑中類庫。

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

綜上所述,它們之間的關系可以通過下圖形象的描述:

為什么要有三個類加載器,一方面是分工,各自負責各自的區塊,另一方面為了實現委托模型。


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

前面說了,java中有三個類加載器,問題就來了,碰到一個類需要加載時,它們之間是如何協調工作的,即java是如何區分一個類該由哪個類加載器來完成呢。 在這里java采用了委托模型機制,這個機制簡單來講,就是“類裝載器有載入類的需求時,會先請示其Parent使用其搜索路徑幫忙載入,如果Parent 找不到,那么才由自己依照自己的搜索路徑搜索類

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

運行結果:

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

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

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

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

類裝載工作由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。
 
定義自已的類加載器分為兩步:
 
1、繼承java.lang.ClassLoader
 
2、重寫父類的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()來設置當前線程的上下文類加載器。






免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM