原文鏈接:http://blog.csdn.net/u011080472/article/details/51332866,http://www.cnblogs.com/lanxuezaipiao/p/4138511.html
加載類的開放性
類加載器(ClassLoader)是Java語言的一項創新,也是Java流行的一個重要原因。在類加載的第一階段“加載”過程中,需要通過一個類的全限定名來獲取定義此類的二進制字節流,完成這個動作的代碼塊就是類加載器。這一動作是放在Java虛擬機外部去實現的,以便讓應用程序自己決定如何獲取所需的類。
虛擬機規范並沒有指明二進制字節流要從一個Class文件獲取,或者說根本沒有指明從哪里獲取、怎樣獲取。這種開放使得Java在很多領域得到充分運用,例如:
- 從ZIP包中讀取,這很常見,成為JAR,EAR,WAR格式的基礎
- 從網絡中獲取,最典型的應用就是Applet
- 運行時計算生成,最典型的是動態代理技術,在java.lang.reflect.Proxy中,就是用了ProxyGenerator.generateProxyClass來為特定接口生成形式為“*$Proxy”的代理類的二進制字節流
- 有其他文件生成,最典型的JSP應用,由JSP文件生成對應的Class類
……
類加載器與類的唯一性
類加載器雖然只用於實現類的加載動作,但是對於任意一個類,都需要由加載它的類加載器和這個類本身共同確立其在Java虛擬機中的唯一性。通俗的說,JVM中兩個類是否“相等”,首先就必須是同一個類加載器加載的,否則,即使這兩個類來源於同一個Class文件,被同一個虛擬機加載,只要類加載器不同,那么這兩個類必定是不相等的。
這里的“相等”,包括代表類的Class對象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回結果,也包括使用instanceof關鍵字做對象所屬關系判定等情況。
以下代碼說明了不同的類加載器對instanceof關鍵字運算的結果的影響。
1 package com.jvm.classloading; 2 3 import java.io.IOException; 4 import java.io.InputStream; 5 6 /** 7 * 類加載器在類相等判斷中的影響 8 * 9 * instanceof關鍵字 10 * 11 */ 12 13 public class ClassLoaderTest { 14 public static void main(String[] args) throws Exception { 15 // 自定義類加載器 16 ClassLoader myLoader = new ClassLoader() { 17 @Override 18 public Class<?> loadClass(String name) throws ClassNotFoundException { 19 try { 20 String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class"; 21 InputStream is = getClass().getResourceAsStream(fileName); 22 if (is == null) { 23 return super.loadClass(fileName); 24 } 25 byte[] b = new byte[is.available()]; 26 is.read(b); 27 return defineClass(name, b, 0, b.length); 28 } catch (IOException e) { 29 throw new ClassNotFoundException(); 30 } 31 } 32 }; 33 34 // 使用ClassLoaderTest的類加載器加載本類 35 Object obj1 = ClassLoaderTest.class.getClassLoader().loadClass("com.jvm.classloading.ClassLoaderTest").newInstance(); 36 System.out.println(obj1.getClass()); 37 System.out.println(obj1 instanceof com.jvm.classloading.ClassLoaderTest); 38 39 // 使用自定義類加載器加載本類 40 Object obj2 = myLoader.loadClass("com.jvm.classloading.ClassLoaderTest").newInstance(); 41 System.out.println(obj2.getClass()); 42 System.out.println(obj2 instanceof com.jvm.classloading.ClassLoaderTest); 43 } 44 }
輸出結果:
1 class com.jvm.classloading.ClassLoaderTest 2 true 3 class com.jvm.classloading.ClassLoaderTest 4 false
myLoader是自定義的類加載器,可以用來加載與自己在同一路徑下的Class文件。main函數的第一部分使用系統加載主類ClassLoaderTest的類加載器加載ClassLoaderTest,輸出顯示,obj1的所屬類型檢查正確,這是虛擬機中有2個ClassLoaderTest類,一個是主類,另一個是main()方法中加載的類,由於這兩個類使用同一個類加載器加載並且來源於同一個Class文件,因此這兩個類是完全相同的。
第二部分使用自定義的類加載器加載ClassLoaderTest,class com.jvm.classloading.ClassLoderTest
顯示,obj2確實是類com.jvm.classloading.ClassLoaderTest
實例化出來的對象,但是第二句輸出false。此時虛擬機中有3個ClassLoaderTest類,由於第3個類的類加載器與前面2個類加載器不同,雖然來源於同一個Class文件,但它是一個獨立的類,所屬類型檢查是返回結果自然是false。
雙親委派模型
類加載器種類
從Java虛擬機的角度來說,只存在兩種不同的類加載器:一種是啟動類加載器(Bootstrap ClassLoader),這個類加載器使用C++語言實現(HotSpot虛擬機中),是虛擬機自身的一部分;另一種就是所有其他的類加載器,這些類加載器都有Java語言實現,獨立於虛擬機外部,並且全部繼承自java.lang.ClassLoader。
從開發者的角度,類加載器可以細分為:
-
啟動(Bootstrap)類加載器:負責將 Java_Home/lib下面的類庫加載到內存中(比如rt.jar)。由於引導類加載器涉及到虛擬機本地實現細節,開發者無法直接獲取到啟動類加載器的引用,所以不允許直接通過引用進行操作。
-
標准擴展(Extension)類加載器:是由 Sun 的 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)實現的。它負責將Java_Home /lib/ext或者由系統變量 java.ext.dir指定位置中的類庫加載到內存中。開發者可以直接使用標准擴展類加載器。
-
應用程序(Application)類加載器:是由 Sun 的 AppClassLoader(sun.misc.Launcher$AppClassLoader)實現的。它負責將系統類路徑(CLASSPATH)中指定的類庫加載到內存中。開發者可以直接使用系統類加載器。由於這個類加載器是ClassLoader中的getSystemClassLoader()方法的返回值,因此一般稱為系統(System)加載器。
除此之外,還有自定義的類加載器,它們之間的層次關系被稱為類加載器的雙親委派模型。該模型要求除了頂層的啟動類加載器外,其余的類加載器都應該有自己的父類加載器,而這種父子關系一般通過組合(Composition)關系來實現,而不是通過繼承(Inheritance)。
雙親委派模型
雙親委派模型過程
某個特定的類加載器在接到加載類的請求時,首先將加載任務委托給父類加載器,依次遞歸,如果父類加載器可以完成類加載任務,就成功返回;只有父類加載器無法完成此加載任務時,才自己去加載。
使用雙親委派模型的好處在於Java類隨着它的類加載器一起具備了一種帶有優先級的層次關系。例如類java.lang.Object,它存在在rt.jar中,無論哪一個類加載器要加載這個類,最終都是委派給處於模型最頂端的Bootstrap ClassLoader進行加載,因此Object類在程序的各種類加載器環境中都是同一個類。相反,如果沒有雙親委派模型而是由各個類加載器自行加載的話,如果用戶編寫了一個java.lang.Object的同名類並放在ClassPath中,那系統中將會出現多個不同的Object類,程序將混亂。因此,如果開發者嘗試編寫一個與rt.jar類庫中重名的Java類,可以正常編譯,但是永遠無法被加載運行。
雙親委派模型的系統實現
在java.lang.ClassLoader的loadClass()方法中,先檢查是否已經被加載過,若沒有加載則調用父類加載器的loadClass()方法,若父加載器為空則默認使用啟動類加載器作為父加載器。如果父加載失敗,則拋出ClassNotFoundException異常后,再調用自己的findClass()方法進行加載。
1 protected synchronized Class<?> loadClass(String name,boolean resolve)throws ClassNotFoundException{ 2 //check the class has been loaded or not 3 Class c = findLoadedClass(name); 4 if(c == null){ 5 try{ 6 if(parent != null){ 7 c = parent.loadClass(name,false); 8 }else{ 9 c = findBootstrapClassOrNull(name); 10 } 11 }catch(ClassNotFoundException e){ 12 //if throws the exception ,the father can not complete the load 13 } 14 if(c == null){ 15 c = findClass(name); 16 } 17 } 18 if(resolve){ 19 resolveClass(c); 20 } 21 return c; 22 }
注意,雙親委派模型是Java設計者推薦給開發者的類加載器的實現方式,並不是強制規定的。大多數的類加載器都遵循這個模型,但是JDK中也有較大規模破壞雙親模型的情況,例如線程上下文類加載器(Thread Context ClassLoader)的出現,具體分析可以參見周志明著《深入理解Java虛擬機》。
關於Java類加載雙親委派機制的思考(附一道面試題)
幾點思考
1.Java虛擬機的第一個類加載器是Bootstrap,這個加載器很特殊,它不是Java類,因此它不需要被別人加載,它嵌套在Java虛擬機內核里面,也就是JVM啟動的時候Bootstrap就已經啟動,它是用C++寫的二進制代碼(不是字節碼),它可以去加載別的類。
這也是我們在測試時為什么發現System.class.getClassLoader()結果為null的原因,這並不表示System這個類沒有類加載器,而是它的加載器比較特殊,是BootstrapClassLoader,由於它不是Java類,因此獲得它的引用肯定返回null。
2.委托機制具體含義
當Java虛擬機要加載一個類時,到底派出哪個類加載器去加載呢?
*首先當前線程的類加載器去加載線程中的第一個類(假設為類A)。
注:當前線程的類加載器可以通過Thread類的getContextClassLoader()獲得,也可以通過setContextClassLoader()自己設置類加載器。
*如果類A中引用了類B,Java虛擬機將使用加載類A的類加載器去加載類B。
*還可以直接調用ClassLoader.loadClass()方法來指定某個類加載器去加載某個類。
3.委托機制的意義 — 防止內存中出現多份同樣的字節碼
比如兩個類A和類B都要加載System類:
*如果不用委托而是自己加載自己的,那么類A就會加載一份System字節碼,然后類B又會加載一份System字節碼,這樣內存中就出現了兩份System字節碼。
*如果使用委托機制,會遞歸的向父類查找,也就是首選用Bootstrap嘗試加載,如果找不到再向下。這里的System就能在Bootstrap中找到然后加載,如果此時類B也要加載System,也從Bootstrap開始,此時Bootstrap發現已經加載過了System那么直接返回內存中的System即可而不需要重新加載,這樣內存中就只有一份System的字節碼了。
一道面試題
能不能自己寫個類叫java.lang.System
?
答案:通常不可以,但可以采取另類方法達到這個需求。
解釋:為了不讓我們寫System類,類加載采用委托機制,這樣可以保證爸爸們優先,爸爸們能找到的類,兒子就沒有機會加載。而System類是Bootstrap加載器加載的,就算自己重寫,也總是使用Java系統提供的System,自己寫的System類根本沒有機會得到加載。
但是,我們可以自己定義一個類加載器來達到這個目的,為了避免雙親委托機制,這個類加載器也必須是特殊的。由於系統自帶的三個類加載器都加載特定目錄下的類,如果我們自己的類加載器放在一個特殊的目錄,那么系統的加載器就無法加載,也就是最終還是由我們自己的加載器加載。