天天看點

關于類加載器

每個java開發人員對java.lang.ClassNotFoundExcetpion這個異常肯定都不陌生,這背後就涉及到了java技術體系中的類加載。Java的類加載機制是java技術體系中比較核心的部分,雖然平時沒又怎麼接觸,雖然和大部分開發人員直接打交道不多,但是對其背後的機理有一定了解有助于排查程式中出現的類加載失敗等技術問題,對了解java虛拟機的連接配接模型和java語言的動态性都有很大幫助。

類加載器(ClassLoader)用來加載 class位元組碼到 Java 虛拟機中。一般來說,Java 虛拟機使用 Java 類的方式如下:Java 源檔案在經過 Javac之後就被轉換成 Java 位元組碼檔案(.class 檔案)。類加載器負責讀取 Java 位元組代碼,并轉換成 java.lang.Class 類的一個執行個體。每一個這樣的執行個體用來表示一個 Java 類。

類加載器雖然隻用于實作類的加載動作,但它在Java程式中起到的作用卻遠遠不限于類加載階段。對于任意一個類,都需要由加載它的類加載器和這個類本身一同确立其在Java虛拟中的唯一性。說通俗一些,比較兩個類是否“相等”,隻有在兩個類是由同一個類加載器的前提之下才有意義,否則,即使這兩個類來源于同一個class檔案,隻要加載它的類加載器不同,那這兩個類必定不相等。這裡所指的“相等”包括代表類的Class對象的equal方法、isAssignableFrom()、isInstance()方法及instance關鍵字傳回的結果。

在架構中需要手動擷取某一個類的執行個體的時候,最先需要擷取加載到記憶體的類,然後通過反射來執行個體化對象就行了。當然現在的架構都已經幫我們把這事做了,比如Spring架構中的org.springframework.util.ClassUtils封裝了自己的類加載器。

當我們需要自己實作一個架構或者需要需要寫一個類加載器的時候,了解類加載器就非常重要了。

Bootstrap ClassLoader/啟動類加載器

主要負責jdk_home/lib目錄下的核心 api 或 -Xbootclasspath 選項指定的jar包裝入工作。

Extension ClassLoader/擴充類加載器

主要負責jdk_home/lib/ext目錄下的jar包或 -Djava.ext.dirs 指定目錄下的jar包裝入工作。

System ClassLoader/系統類加載器

主要負責java -classpath/-Djava.class.path所指的目錄下的類與jar包裝入工作。

User Custom ClassLoader/使用者自定義類加載器(java.lang.ClassLoader的子類)

在程式運作期間, 通過java.lang.ClassLoader的子類動态加載class檔案, 展現java動态實時類裝入特性。

每個ClassLoader都維護了一份自己的名稱空間, 同一個名稱空間裡不能出現兩個同名的類。

為了實作java安全沙箱模型頂層的類加載器安全機制, java預設采用了 " 雙親委派的加載鍊 " 結構。

關于類加載雙親委派機制

如果一個類加載器收到了一個類加載請求,它首先不會自己去加載這個類,而是把這個請求委托給父類加載器去完成,每一個層次的類加載器都是如此,是以所有的加載請求最終都應該傳送到頂層的啟動類加載器中,隻有當父類加載器回報自己無法完成加載請求(它管理的範圍之中沒有這個類)時,子加載器才會嘗試着自己去加載。

雙親委派機制的好處

避免重複加載,當父親已經加載了該類的時候,就沒有必要子ClassLoader再加載一次。

Java類随着它的類加載器一起具備了一種帶有優先級的層次關系,例如java.lang.Object存放在rt.jar之中,無論那個類加載器要加載這個類,最終都是委托給啟動類加載器進行加載,是以Object類在程式的各種類加載器環境中都是同一個類,相反,如果沒有雙親委托模型,由各個類加載器去完成的話,如果使用者自己寫一個名為java.lang.Object的類,并放在classpath中,應用程式中可能會出現多個不同的Object類,java類型體系中最基本安全行為也就無法保證。

類加載器的幾個重要方法

類加載器均是繼承自java.lang.ClassLoader抽象類。我們下面我們就看簡要介紹一下java.lang.ClassLoader中幾個最重要的方法:

loadClass: 此方法負責加載指定名字的類,首先會從已加載的類中去尋找,如果沒有找到;從parent ClassLoader[ExtClassLoader]中加載;如果沒有加載到,則從Bootstrap ClassLoader中嘗試加載(findBootstrapClassOrNull方法), 如果還是加載失敗,則抛出異常ClassNotFoundException, 在調用自己的findClass方法進行加載。如果要改變類的加載順序可以覆寫此方法;如果加載順序相同,則可以通過覆寫findClass方法來做特殊處理,例如:解密,固定路徑尋找等。當通過整個尋找類的過程仍然未擷取Class對象,則抛出ClassNotFoundException異常。

如果類需要resolve,在調用resolveClass進行連結。

findLoadedClass:此方法負責從目前ClassLoader執行個體對象的緩存中尋找已加載的類,調用的為native方法。

findClass: 此方法直接抛出ClassNotFoundException異常,是以要通過覆寫loadClass或此方法來以自定義的方式加載相應的類。

findSystemClass: 此方法是從sun.misc.Launcher$AppClassLoader中尋找類,如果未找到,則繼續從BootstrapClassLoader中尋找,如果仍然未找到,傳回null

defineClass: 此方法負責将二進制位元組流轉換為Class對象,這個方法對于自定義類加載器而言非常重要。如果二進制的位元組碼的格式不符合jvm class檔案格式規範,則抛出ClassFormatError異常;如果生成的類名和二進制位元組碼不同,則抛出NoClassDefFoundError;如果加載的class是受保護的、采用不同簽名的,或者類名是以java.開頭的,則抛出SecurityException異常。

resolveClass: 此方法負責完成Class對象的連結,如果連結過,則直接傳回。

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 的實作中都會用到。

java預設的線程上下文類加載器是 系統類加載器(AppClassLoader)。

以上代碼摘自sun.misc.Launch的無參構造函數Launch()。

使用線程上下文類加載器, 可以在執行線程中, 抛棄雙親委派加載鍊模式, 使用線程上下文裡的類加載器加載類.

典型的例子有, 通過線程上下文來加載第三方庫jndi實作, 而不依賴于雙親委派.

大部分java app伺服器(jboss, tomcat..)也是采用contextClassLoader來處理web服務。

還有一些采用 hotswap 特性的架構, 也使用了線程上下文類加載器, 比如 seasar (full stack framework in japenese).

線程上下文從根本解決了一般應用不能違背雙親委派模式的問題.

使java類加載體系顯得更靈活.

随着多核時代的來臨, 相信多線程開發将會越來越多地進入程式員的實際編碼過程中. 是以,

在編寫基礎設施時, 通過使用線程上下文來加載類, 應該是一個很好的選擇。

當然, 好東西都有利弊. 使用線程上下文加載類, 也要注意, 保證多根需要通信的線程間的類加載器應該是同一個,

防止因為不同的類加載器, 導緻類型轉換異常(ClassCastException)。

在黃勇的《架構探險》一書中開發類加載器的時候采用的也是線程上下文加載器。

implicit隐式,即利用執行個體化才載入的特性來動态載入class

explicit顯式方式,又分兩種方式:

java.lang.Class的forName()方法

java.lang.ClassLoader的loadClass()方法

用Class.forName加載類

Class.forName使用的是被調用者的類加載器來加載類的。

這種特性, 證明了java類加載器中的名稱空間是唯一的, 不會互相幹擾。

即在一般情況下, 保證同一個類中所關聯的其他類都是由目前類的類加載器所加載的。

上面中 ClassLoader.getCallerClassLoader 就是得到調用目前forName方法的類的類加載器。

上面forName中的initialize參數是很重要的,可以覺得被加載同時是否完成初始化的工作(說明: 單參數版本的forName方法預設是不完成初始化的).有些場景下,需要将initialize設定為true來強制加載同時完成初始化,例如典型的就是利用DriverManager進行JDBC驅動程式類注冊的問題,因為每一個JDBC驅動程式類的靜态初始化方法都用DriverManager注冊驅動程式,這樣才能被應用程式使用,這就要求驅動程式類必須被初始化,而不單單被加載.

有時候為了提高加載類的性能,可以講initialize參數設定為false。

ClassNotFoundException 這是最常見的異常,産生這個異常的原因為在目前的ClassLoader 中加載類時,未找到類檔案,

NoClassDefFoundError 這個異常是因為 加載到的類中引用到的另外類不存在,例如要加載A,而A中盜用了B,B不存在或目前的ClassLoader無法加載B,就會抛出這個異常。

LinkageError 該異常在自定義ClassLoader的情況下更容易出現,主要原因是此類已經在ClassLoader加載過了,重複的加載會造成該異常。

參考

Java虛拟機學習 - 類加載器(ClassLoader)

java classLoader體系結構使用詳解

Java類加載原了解析