java三大類加載器


摘抄自:java三大類加載器

作者:aworker

類加載器的定義

類加載器基本職責就是根據類的二進制名(binary name)讀取java編譯器編譯好的字節碼文件(.class文件),並且轉化生成一個java.lang.Class類的一個實例。這樣的每個實例用來表示一個Java類,jvm就是用這些實例來生成java對象的。比如new一個String對象;反射生成一個String對象,都會用到String.class 這個java.lang.Class類的對象。基本上所有的類加載器都是java.lang.ClassLoader 類的一個實例。下面介紹這個類加載器的一些核心方法:

方法名 說明
getParent() 返回該類加載器的父類加載器
loadClass(String name) 加載名為name的類,返回java.lang.Class類的實例
findClass(String name) 查找名字為name的類,返回的結果是java.lang.Class類的實例
findLoadedClass(String name) 查找名字為name的已經被加載過的類,返回的結果是java.lang.Class類的實例
defineClass(String name,byte[] b,int off,int len) 根據字節數組b中的數據轉化成Java類,返回的結果是java.lang.Class類的實例

上述方法的name參數都是binary name(類的二進制名字)如:

  • java.lang.String <包名>.<類名>
  • java.concurrent.locks.AbstractQueuedSynchronizer$Node <包名>.<類名>$<內部類名>
  • java.net.URLClassLoader$1 <包名>.<類名>.<匿名內部類名>

類加載器的實例:

即便一個最簡單的helloworld程序:

public class SayHello{
    public void justSayHello(){
        String str = "hello world!";
        System.out.println(str);
    }

    public static void main(String[] args){
        SayHello instance = new SayHello();
        instance.justSayHello();
    }
}

也會用到至少3個類加載器實例:

  • 引導類加載器(Bootstrap ClassLoader)
  • 拓展類加載器(Extension ClassLoader)
  • 應用類加載器(Application ClassLoader)

引導類加載器

引導類加載器是jvm在運行時,內嵌在jvm中的一段特殊的用來加載java核心類庫的C++代碼。String.class 對象就是由引導類加載器加載的,引導類加載器具體加載哪些核心代碼可以通過獲取值為 "sun.boot.class.path" 的系統屬性獲得。引導類加載器不是java原生代碼編寫的,所以其也不是java.lang.ClassLoader類的實例,其沒有getParent方法。

拓展類加載器

拓展類加載器用來加載jvm實現的一個拓展目錄,該目錄下的所有java類都由此類加載器加載。此路徑可以通過獲取"java.ext.dirs"的系統屬性獲得。拓展類加載器就是java.lang.ClassLoader類的一個實例,其getParent方法返回的是引導類加載器(在 HotSpot虛擬機中用null表示引導類加載)。

應用類加載器

應用類加載器又稱為系統類加載器,開發者可用通過 java.lang.ClassLoader.getSystemClassLoader()方法獲得此類加載器的實例,系統類加載器也因此得名。其主要負責加載程序開發者自己編寫的java類。一般來說,java應用都是用此類加載器完成加載的,可以通過獲取"java.class.path"的系統屬性(也就是我們常說的classpath)來獲取應用類加載器加載的類路徑。應用類加載器是java.lang.ClassLoader類的一個實例,其getParent方法返回的是拓展類加載器。

拓展類加載器和應用類加載器的類圖如下:

其中的AppClassLoader和ExtClassLoader分別是系統類加載器實例的定義類和拓展類加載器實例的定義類,當然它們也是ClassLoader的一個實例了。

類加載器的加載機制

當jvm要加載某個類時,jvm會先指定一個類加載器,負責加載此類。而此指定的類加載器在嘗試自己去根據某個類的二進制名字查找其相應的字節碼文件並定義之前,會首先委托給其父親(getParent方法返回的類加載器)嘗試加載,如果加載失敗,就會由自己來嘗試加載此類。一般情況下,這個由jvm指定的類加載器就是應用類加載器,jvm會自動調用其loadClass(String name)方法來開啟類的加載過程,具體細節如下圖:

以上面的SayHello類為例,jvm首先調用系統類加載器的loadClass方法(String name)來獲得SayHello.class對象,系統類加載器委托給拓展類加載器代勞,拓展類加載器委托給引導類加載器代勞,引導類加載器是jvm的根加載器,其沒有委托對象,嘗試自己加載SayHello.class對象但是沒有成功,其將結果(null)返回給拓展類加載器,拓展類加載器根據結果發現引導類加載器沒有加載成功,其自己嘗試加載SayHello.class對象,並將結果(null)返回給應用類加載器,應用類加載器根據加載結果發現拓展類加載器也沒有加載成功,那么其就自己嘗試加載SayHello.class對象,並且將最終的結果(SayHello.class)對象返回給jvm,jvm就是根據這個SayHello.class對象來創建SayHello對象的實例的。而這個加載機制就稱為類加載的雙親委托模型,這個機制的優缺點我們會在下文會論述。

定義加載器和初始加載器

通過 類加載器的加載機制 小節我們可以得出這樣一個結論:真正完成一個類的加載工作和啟動這個類的加載過程的類加載器可能不是同一個。真正完成類加載工作的是通過調用defineClass方法來實現的;而啟動類的加載過程是調用loadClass方法實現的。前者稱為此類的定義加載器(defining loader),后者稱為此類的初始加載器(initiating loader)。如SayHello這個例子中,SayHello類的定義加載器和初始加載器都是應用類加載器;而String類的定義加載器是引導類加載器(java.lang.String類是java核心類庫中的類由引導類加載器完成真正的加載工作),初始加載器是應用類加載器。每一個java.lang.Class類的實例都可以通過getClassLoader()方法返回其定義加載器的引用。通過執行下面的代碼,我們可以驗證上述結論:

System.out.println(SayHello.class.getClassLoader());
System.out.println(String.class.getClassLoader());

返回的結果如下:

sun.misc.Launcher$AppClassLoader@18b4aac2
null

定義加載器和初始加載器具有如下關系:

一個類的定義加載器是這個類中引用的其它類的初始加載器

如果com.demo.Outer 引用了 com.demo.Inner,那么定義了com.demo.Outer類的定義加載器是com.demo.Inner的初始加載器。

類的命名空間

在程序運行過程中,一個類並不是簡單由其二進制名字(binary name)定義的,而是通過其二進制名和其定義加載器所確定的命名空間(run-time package)所共同確定的。所以同一個二進制名的類由不同的定義加載器加載時,其返回的Class對象不是同一個,那么由不同的Class對象所創建的對象,其類型也不是相同的。類似 Test cannot be cast to Test 的java.lang.ClassCastException 的奇怪錯誤很多情況下都是類的二進制名相同,而定義加載器不同造成的。具體演示代碼如下:

package jvm.classloader;

import sun.misc.Launcher;

public class RunTimePackageDemo {
    public static void main(String[] args) throws Exception {
        ClassLoader classLoader = new Launcher().getClassLoader(); //1 new一個新的類加載器
        Class<?> aClass = classLoader.loadClass("jvm.classloader.RunTimePackageDemo");
        RunTimePackageDemo runTimePackageDemo  = (RunTimePackageDemo)aClass.newInstance(); //2
    }
}

運行上述代碼,會在 //2處報如下異常:

Exception in thread "main" java.lang.ClassCastException: jvm.classloader.RunTimePackageDemo cannot be cast to jvm.classloader.RunTimePackageDemo
    at jvm.classloader.RunTimePackageDemo.main(RunTimePackageDemo.java:19)

這是因為 //1 處獲取的應用類加載器a和jvm用來加載器RunTimePackageDemo.class對象的應用類加載器b不是同一個實例,那么構成這兩個類的run-time package也就是不同的。所以即使它們的二進制名字相同,但是由a定義的RunTimePackageDemo類所創建的對象顯然不能轉化為由b定義的RunTimePackageDemo類的實例。這種情況下jvm就會拋出ClassCastException。

雙親委托機制的優缺點

類的命名空間 小節我們得知,即使是相同的二進制名字的類如果其定義加載器不同,那么其也算是不同的兩個類。這種機制有如下好處:

  • 可以保證java核心類庫的安全,即保證由引導類加載器加載的類不能被用戶隨便替換,用戶不能自己隨便定義一個二進制名也為
    java.lang.String 的類來替換java核心類庫的java.lang.String類,否則會拋出ClassCastException。
  • 使得一個類的不同版本可以共存在jvm中,帶來了極大的靈活性,OSGi技術的實現就是得益於此。

而根據一個類的定義加載器是這個類中引用的其它類的初始加載器可知,java核心類庫中定義的類是不能使用系統類加載器定義的類。而java提供了很多服務提供者接口(Service Provider Interface SPI),許可第三方來實現這些類的接口。第三方開發的類通常是由應用類加載器在類路徑下(classpath)來找到並且定義的。引導類加載器是無法找到 SPI 的實現類的,因為它只加載 Java 的核心庫。它也不能代理給系統類加載器,因為它是系統類加載器的祖先類加載器。也就是說,類加載器的雙親委托模型無法解決這個問題,這是雙親委托模型的缺點。

線程上下文類加載器

線程上下文類加載器就是用來解決類的雙親委托模型的缺點的。如java核心類庫中的javax.xml.parsers.DocumentBuilderFactory類就是java提供的一個服務提供者接口,其newInstance()方法用來創建此接口的一個實現類,並返回該實現類。如果僅僅依賴類的雙親委托模型,這就不可能完成的任務。通過查看源碼得知newInstance方法中會調用java.util.ServiceLoader.load(Class service)方法,來獲取DocumentBuilderFactory類的實現類,而java.util.ServiceLoader.load(Class service)方法正是通過線程上下文類加載器來完成對DocumentBuilderFactory類的實現類的加載工作的。而在默認情況下線程上下文類加載器就是應用類加載器。由應用類加載器負責加classpath中的javax.xml.parsers.DocumentBuilderFactory類的實現類,這樣newInstance接口就能正常返回一個實現類。

總結

java類加載器是java語言的一個創新,它使得動態安裝和更新軟件組件成為可能。而java類加載器又是用戶編寫應用代碼和JVM虛擬機實現的交界處。只有先搞懂java類加載器才能為搞懂JVM邁出堅實的一步。


免責聲明!

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



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