類加載機制(類加載過程和類加載器)

1、爲何要使用類加載器?
Java語言裏,類加載都是在程序運行期間完成的,這種策略雖然會令類加載時稍微增長一些性能開銷,可是會給java應用程序提供高度的靈活性。例如:
1.編寫一個面向接口的應用程序,可能等到運行時再指定其實現的子類;
2.用戶能夠自定義一個類加載器,讓程序在運行時從網絡或其餘地方加載一個二進制流做爲程序代碼的一部分;(這個是Android插件化,動態安裝更新apk的基礎)java

 

2、類加載的過程android

使用java編譯器能夠把java代碼編譯爲存儲字節碼的Class文件,使用其餘語言的編譯器同樣能夠把程序代碼翻譯成Class文件,java虛擬機不關心Class的來源是何種語言。如圖所示:程序員

在Class文件中描述的各類信息,最終都須要加載到虛擬機中才能運行和使用。那麼虛擬機是如何加載這些Class文件的呢?
JVM把描述類數據的字節碼.Class文件加載到內存,並對數據進行校驗、轉換解析和初始化,最終造成能夠被虛擬機直接使用的java類型,這就是虛擬機的類加載機制。面試

 

類從被加載到虛擬機內存中開始,到卸載出內存爲止,它的生命週期包括了:加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸載(Unloading)七個階段,其中驗證、準備、解析三個部分統稱連接。數組


加載(裝載)、驗證、準備、初始化和卸載這五個階段順序是固定的,類的加載過程必須按照這種順序開始,而解析階段不必定;它在某些狀況下能夠在初始化以後再開始,這是爲了運行時動態綁定特性(JIT例如接口只在調用的時候才知道具體實現的是哪一個子類)。值得注意的是:這些階段一般都是互相交叉的混合式進行的,一般會在一個階段執行的過程當中調用或激活另一個階段。安全

 

1.加載:(重點)
加載階段是「類加載機制」中的一個階段,這個階段一般也被稱做「裝載」,主要完成:
1.經過「類全名」來獲取定義此類的二進制字節流網絡

2.將字節流所表明的靜態存儲結構轉換爲方法區的運行時數據結構數據結構

3.在java堆中生成一個表明這個類的java.lang.Class對象,做爲方法區這些數據的訪問入口多線程

相對於類加載過程的其餘階段,加載階段(準備地說,是加載階段中獲取類的二進制字節流的動做)是開發期可控性最強的階段,由於加載階段可使用系統提供的類加載器(ClassLoader)來完成,也能夠由用戶自定義的類加載器完成,開發人員能夠經過定義本身的類加載器去控制字節流的獲取方式。jvm

加載階段完成後,虛擬機外部的二進制字節流就按照虛擬機所需的格式存儲在方法區之中,方法區中的數據存儲格式有虛擬機實現自行定義,虛擬機並未規定此區域的具體數據結構。而後在java堆中實例化一個java.lang.Class類的對象,這個對象做爲程序訪問方法區中的這些類型數據的外部接口。

 

2.驗證:(瞭解)

驗證是連接階段的第一步,這一步主要的目的是確保class文件的字節流中包含的信息符合當前虛擬機的要求,而且不會危害虛擬機自身安全。
驗證階段主要包括四個檢驗過程:文件格式驗證、元數據驗證、字節碼驗證和符號引用驗證。

1.文件格式驗證

 驗證class文件格式規範,例如: class文件是否已魔術0xCAFEBABE開頭 , 主、次版本號是否在當前虛擬機處理範圍以內等

2.元數據驗證

這個階段是對字節碼描述的信息進行語義分析,以保證起描述的信息符合java語言規範要求。驗證點可能包括:這個類是否有父類(除了java.lang.Object以外,全部的類都應當有父類)、這個類是否繼承了不容許被繼承的類(被final修飾的)、若是這個類的父類是抽象類,是否實現了起父類或接口中要求實現的全部方法。

3.字節碼驗證

 進行數據流和控制流分析,這個階段對類的方法體進行校驗分析,這個階段的任務是保證被校驗類的方法在運行時不會作出危害虛擬機安全的行爲。如:保證訪法體中的類型轉換有效,例如能夠把一個子類對象賦值給父類數據類型,這是安全的,但不能把一個父類對象賦值給子類數據類型、保證跳轉命令不會跳轉到方法體之外的字節碼命令上。

4.符號引用驗證

符號引用中經過字符串描述的全限定名是否能找到對應的類、符號引用類中的類,字段和方法的訪問性(private、protected、public、default)是否可被當前類訪問。

3.準備:(瞭解)

準備階段是正式爲類變量分配內存並設置類變量初始值的階段,這些內存都將在方法區中進行分配。這個階段中有兩個容易產生混淆的知識點,首先是這時候進行內存分配的僅包括類變量(static 修飾的變量),而不包括實例變量,實例變量將會在對象實例化時隨着對象一塊兒分配在java堆中。其次是這裏所說的初始值「一般狀況」下是數據類型的零值,假設一個類變量定義爲:

public static int value  = 12;

那麼變量value在準備階段事後的初始值爲0而不是12,由於這時候還沒有開始執行任何java方法,而把value賦值爲123的putstatic指令是程序被編譯後,存放於類構造器<clinit>()方法之中,因此把value賦值爲12的動做將在初始化階段纔會被執行。

上面所說的「一般狀況」下初始值是零值,那相對於一些特殊的狀況,若是類字段的字段屬性表中存在ConstantValue屬性,那在準備階段變量value就會被初始化爲ConstantValue屬性所指定的值,建設上面類變量value定義爲:

public static final int value = 123;

編譯時javac將會爲value生成ConstantValue屬性,在準備階段虛擬機就會根據ConstantValue的設置將value設置爲123。

 

4.解析:(瞭解)
解析階段是虛擬機常量池內的符號引用替換爲直接引用的過程。
符號引用:符號引用是一組符號來描述所引用的目標對象,符號能夠是任何形式的字面量,只要使用時能無歧義地定位到目標便可。符號引用與虛擬機實現的內存佈局無關,引用的目標對象並不必定已經加載到內存中。

直接引用:直接引用能夠是直接指向目標對象的指針、相對偏移量或是一個能間接定位到目標的句柄。直接引用是與虛擬機內存佈局實現相關的,同一個符號引用在不一樣虛擬機實例上翻譯出來的直接引用通常不會相同,若是有了直接引用,那引用的目標一定已經在內存中存在。

虛擬機規範並無規定解析階段發生的具體時間,只要求了在執行anewarry、checkcast、getfield、instanceof、invokeinterface、invokespecial、invokestatic、invokevirtual、multianewarray、new、putfield和putstatic這13個用於操做符號引用的字節碼指令以前,先對它們使用的符號引用進行解析,因此虛擬機實現會根據須要來判斷,究竟是在類被加載器加載時就對常量池中的符號引用進行解析,仍是等到一個符號引用將要被使用前纔去解析它。

解析的動做主要針對類或接口、字段、類方法、接口方法四類符號引用進行。分別對應編譯後常量池內的CONSTANT_Class_Info、CONSTANT_Fieldref_Info、CONSTANT_Methodef_Info、CONSTANT_InterfaceMethoder_Info四種常量類型。

1.類、接口的解析

2.字段解析

3.類方法解析

4.接口方法解析

 

5.初始化:(瞭解)

類的初始化階段是類加載過程的最後一步,在準備階段,類變量已賦過一次系統要求的初始值,而在初始化階段,則是根據程序員經過程序制定的主觀計劃去初始化類變量和其餘資源,或者能夠從另一個角度來表達:初始化階段是執行類構造器<clinit>()方法的過程。在如下四種狀況下初始化過程會被觸發執行:

1.遇到new、getstatic、putstatic或invokestatic這4條字節碼指令時,若是類沒有進行過初始化,則需先觸發其初始化。生成這4條指令的最多見的java代碼場景是:使用new關鍵字實例化對象、讀取或設置一個類的靜態字段(被final修飾、已在編譯器把結果放入常量池的靜態字段除外)的時候,以及調用類的靜態方法的時候。

2.使用java.lang.reflect包的方法對類進行反射調用的時候

3.當初始化一個類的時候,若是發現其父類尚未進行過初始化、則須要先出發其父類的初始化

4.jvm啓動時,用戶指定一個執行的主類(包含main方法的那個類),虛擬機會先初始化這個類

在上面準備階段 public static int value  = 12;  在準備階段完成後 value的值爲0,而在初始化階調用了類構造器<clinit>()方法,這個階段完成後value的值爲12。

*類構造器<clinit>()方法是由編譯器自動收集類中的全部類變量的賦值動做和靜態語句塊(static塊)中的語句合併產生的,編譯器收集的順序是由語句在源文件中出現的順序所決定的,靜態語句塊中只能訪問到定義在靜態語句塊以前的變量,定義在它以後的變量,在前面的靜態語句快能夠賦值,可是不能訪問。

*類構造器<clinit>()方法與類的構造函數(實例構造函數<init>()方法)不一樣,它不須要顯式調用父類構造,虛擬機會保證在子類<clinit>()方法執行以前,父類的<clinit>()方法已經執行完畢。所以在虛擬機中的第一個執行的<clinit>()方法的類確定是java.lang.Object。

*因爲父類的<clinit>()方法先執行,也就意味着父類中定義的靜態語句快要優先於子類的變量賦值操做。

*<clinit>()方法對於類或接口來講並非必須的,若是一個類中沒有靜態語句,也沒有變量賦值的操做,那麼編譯器能夠不爲這個類生成<clinit>()方法。

*接口中不能使用靜態語句塊,但接口與類不太可以的是,執行接口的<clinit>()方法不須要先執行父接口的<clinit>()方法。只有當父接口中定義的變量被使用時,父接口才會被初始化。另外,接口的實現類在初始化時也同樣不會執行接口的<clinit>()方法。

*虛擬機會保證一個類的<clinit>()方法在多線程環境中被正確加鎖和同步,若是多個線程同時去初始化一個類,那麼只會有一個線程執行這個類的<clinit>()方法,其餘線程都須要阻塞等待,直到活動線程執行<clinit>()方法完畢。若是一個類的<clinit>()方法中有耗時很長的操做,那就可能形成多個進程阻塞。

 

 

3、類加載器

JVM設計者把類加載階段中的「經過'類全名'來獲取定義此類的二進制字節流」這個動做放到Java虛擬機外部去實現,以便讓應用程序本身決定如何去獲取所須要的類。實現這個動做的代碼模塊稱爲「類加載器」。

 

1.類與類加載器

對於任何一個類,都須要由加載它的類加載器和這個類來確立其在JVM中的惟一性。也就是說,兩個類來源於同一個Class文件,而且被同一個類加載器加載,這兩個類才相等。

2.雙親委派模型

從虛擬機的角度來講,只存在兩種不一樣的類加載器:一種是啓動類加載器(Bootstrap ClassLoader),該類加載器使用C++語言實現,屬於虛擬機自身的一部分。另一種就是全部其它的類加載器,這些類加載器是由Java語言實現,獨立於JVM外部,而且所有繼承自抽象類java.lang.ClassLoader。

 

從Java開發人員的角度來看,大部分Java程序通常會使用到如下三種系統提供的類加載器:
1)啓動類加載器(Bootstrap ClassLoader):負責加載JAVA_HOME\lib目錄中而且能被虛擬機識別的類庫到JVM內存中,若是名稱不符合的類庫即便放在lib目錄中也不會被加載。該類加載器沒法被Java程序直接引用。
2)擴展類加載器(Extension ClassLoader):該加載器主要是負責加載JAVA_HOME\lib\,該加載器能夠被開發者直接使用。
3)應用程序類加載器(Application ClassLoader):該類加載器也稱爲系統類加載器,它負責加載用戶類路徑(Classpath)上所指定的類庫,開發者能夠直接使用該類加載器,若是應用程序中沒有自定義過本身的類加載器,通常狀況下這個就是程序中默認的類加載器。

咱們的應用程序都是由這三類加載器互相配合進行加載的,咱們也能夠加入本身定義的類加載器。這些類加載器之間的關係以下圖所示:

如上圖所示的類加載器之間的這種層次關係,就稱爲類加載器的雙親委派模型(Parent Delegation Model)。該模型要求除了頂層的啓動類加載器外,其他的類加載器都應當有本身的父類加載器。子類加載器和父類加載器不是以繼承(Inheritance)的關係來實現,而是經過組合(Composition)關係來複用父加載器的代碼。


雙親委派模型的工做過程爲:若是一個類加載器收到了類加載的請求,它首先不會本身去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每個層次的加載器都是如此,所以全部的類加載請求都會傳給頂層的啓動類加載器,只有當父加載器反饋本身沒法完成該加載請求(該加載器的搜索範圍中沒有找到對應的類)時,子加載器纔會嘗試本身去加載。


使用這種模型來組織類加載器之間的關係的好處是Java類隨着它的類加載器一塊兒具有了一種帶有優先級的層次關係。例如java.lang.Object類,不管哪一個類加載器去加載該類,最終都是由啓動類加載器進行加載,所以Object類在程序的各類類加載器環境中都是同一個類。不然的話,若是不使用該模型的話,若是用戶自定義一個java.lang.Object類且存放在classpath中,那麼系統中將會出現多個Object類,應用程序也會變得很混亂。若是咱們自定義一個rt.jar中已有類的同名Java類,會發現JVM能夠正常編譯,但該類永遠沒法被加載運行。
在rt.jar包中的java.lang.ClassLoader類中,咱們能夠查看類加載實現過程的代碼,具體源碼以下:

[java]  view plain  copy
  1. protected synchronized Class loadClass(String name, boolean resolve)  
  2.         throws ClassNotFoundException {  
  3.     // 首先檢查該name指定的class是否有被加載  
  4.     Class c = findLoadedClass(name);  
  5.     if (c == null) {  
  6.         try {  
  7.             if (parent != null) {  
  8.                 // 若是parent不爲null,則調用parent的loadClass進行加載  
  9.                 c = parent.loadClass(name, false);  
  10.             } else {  
  11.                 // parent爲null,則調用BootstrapClassLoader進行加載  
  12.                 c = findBootstrapClass0(name);  
  13.             }  
  14.         } catch (ClassNotFoundException e) {  
  15.             // 若是仍然沒法加載成功,則調用自身的findClass進行加載  
  16.             c = findClass(name);  
  17.         }  
  18.     }  
  19.     if (resolve) {  
  20.         resolveClass(c);  
  21.     }  
  22.     return c;  
  23. }  

經過上面代碼能夠看出,雙親委派模型是經過loadClass()方法來實現的,根據代碼以及代碼中的註釋能夠很清楚地瞭解整個過程其實很是簡單:先檢查是否已經被加載過,若是沒有則調用父加載器的loadClass()方法,若是父加載器爲空則默認使用啓動類加載器做爲父加載器。若是父類加載器加載失敗,則先拋出ClassNotFoundException,而後再調用本身的findClass()方法進行加載。

 

3.自定義類加載器

若要實現自定義類加載器,只須要繼承java.lang.ClassLoader 類,而且重寫其findClass()方法便可。java.lang.ClassLoader 類的基本職責就是根據一個指定的類的名稱,找到或者生成其對應的字節代碼,而後從這些字節代碼中定義出一個 Java 類,即 java.lang.Class 類的一個實例。除此以外,ClassLoader 還負責加載 Java 應用所需的資源,如圖像文件和配置文件等,ClassLoader 中與加載類相關的方法以下:

 
方法                                 說明
getParent()  返回該類加載器的父類加載器。

loadClass(String name) 加載名稱爲 二進制名稱爲name 的類,返回的結果是 java.lang.Class 類的實例。

findClass(String name) 查找名稱爲 name 的類,返回的結果是 java.lang.Class 類的實例。

findLoadedClass(String name) 查找名稱爲 name 的已經被加載過的類,返回的結果是 java.lang.Class 類的實例。

resolveClass(Class<?> c) 連接指定的 Java 類。


注意:在JDK1.2以前,類加載還沒有引入雙親委派模式,所以實現自定義類加載器時經常重寫loadClass方法,提供雙親委派邏輯,從JDK1.2以後,雙親委派模式已經被引入到類加載體系中,自定義類加載器時不須要在本身寫雙親委派的邏輯,所以不鼓勵重寫loadClass方法,而推薦重寫findClass方法。

在Java中,任意一個類都須要由加載它的類加載器和這個類自己一同肯定其在java虛擬機中的惟一性,即比較兩個類是否相等,只有在這兩個類是由同一個類加載器加載的前提之下才有意義,不然,即便這兩個類來源於同一個Class類文件,只要加載它的類加載器不相同,那麼這兩個類一定不相等(這裏的相等包括表明類的Class對象的equals()方法、isAssignableFrom()方法、isInstance()方法和instanceof關鍵字的結果)。例子代碼以下:

[java]  view plain  copy
  1. /** 
  2.      * 1、ClassLoader加載類的順序 
  3.      *  1.調用 findLoadedClass(String) 來檢查是否已經加載類。 
  4.      *  2.在父類加載器上調用 loadClass 方法。若是父類加載器爲 null,則使用虛擬機的內置類加載器。 
  5.      *  3.調用 findClass(String) 方法查找類。 
  6.      * 2、實現本身的類加載器 
  7.      *  1.獲取類的class文件的字節數組 
  8.      *  2.將字節數組轉換爲Class類的實例 
  9.      * @author lei 2011-9-1 
  10.      */  
  11.     public class ClassLoaderTest {  
  12.         public static void main(String[] args) throws InstantiationException, IllegalAccessException, ClassNotFoundException {  
  13.             //新建一個類加載器  
  14.             MyClassLoader cl = new MyClassLoader("myClassLoader");  
  15.             //加載類,獲得Class對象  
  16.             Class<?> clazz = cl.loadClass("classloader.Animal");  
  17.             //獲得類的實例  
  18.             Animal animal=(Animal) clazz.newInstance();  
  19.             animal.say();  
  20.         }  
  21.     }  
  22.     class Animal{  
  23.         public void say(){  
  24.             System.out.println("hello world!");  
  25.         }  
  26.     }  
  27.     class MyClassLoader extends ClassLoader {  
  28.         //類加載器的名稱  
  29.         private String name;  
  30.         //類存放的路徑  
  31.         private String path = "E:\\workspace\\Algorithm\\src";  
  32.         MyClassLoader(String name) {  
  33.             this.name = name;  
  34.         }  
  35.         MyClassLoader(ClassLoader parent, String name) {  
  36.             super(parent);  
  37.             this.name = name;  
  38.         }  
  39.         /** 
  40.          * 重寫findClass方法 
  41.          */  
  42.         @Override  
  43.         public Class<?> findClass(String name) {  
  44.             byte[] data = loadClassData(name);  
  45.             return this.defineClass(name, data, 0, data.length);  
  46.         }  
  47.         public byte[] loadClassData(String name) {  
  48.             try {  
  49.                 name = name.replace(".""//");  
  50.                 FileInputStream is = new FileInputStream(new File(path + name + ".class"));  
  51.                 ByteArrayOutputStream baos = new ByteArrayOutputStream();  
  52.                 int b = 0;  
  53.                 while ((b = is.read()) != -1) {  
  54.                     baos.write(b);  
  55.                 }  
  56.                 return baos.toByteArray();  
  57.             } catch (Exception e) {  
  58.                 e.printStackTrace();  
  59.             }  
  60.             return null;  
  61.         }  
  62.     }  

 

類加載器雙親委派模型是從JDK1.2之後引入的,而且只是一種推薦的模型,不是強制要求的,所以有一些沒有遵循雙親委派模型的特例:(瞭解)

(1).在JDK1.2以前,自定義類加載器都要覆蓋loadClass方法去實現加載類的功能,JDK1.2引入雙親委派模型以後,loadClass方法用於委派父類加載器進行類加載,只有父類加載器沒法完成類加載請求時才調用本身的findClass方法進行類加載,所以在JDK1.2以前的類加載的loadClass方法沒有遵循雙親委派模型,所以在JDK1.2以後,自定義類加載器不推薦覆蓋loadClass方法,而只須要覆蓋findClass方法便可。

(2).雙親委派模式很好地解決了各個類加載器的基礎類統一問題,越基礎的類由越上層的類加載器進行加載,可是這個基礎類統一有一個不足,當基礎類想要調用回下層的用戶代碼時沒法委派子類加載器進行類加載。爲了解決這個問題JDK引入了ThreadContext線程上下文,經過線程上下文的setContextClassLoader方法能夠設置線程上下文類加載器。

JavaEE只是一個規範,sun公司只給出了接口規範,具體的實現由各個廠商進行實現,所以JNDI,JDBC,JAXB等這些第三方的實現庫就能夠被JDK的類庫所調用。線程上下文類加載器也沒有遵循雙親委派模型。

(3).近年來的熱碼替換,模塊熱部署等應用要求不用重啓java虛擬機就能夠實現代碼模塊的即插即用,催生了OSGi技術,在OSGi中類加載器體系被髮展爲網狀結構。OSGi也沒有徹底遵循雙親委派模型。

4.動態加載Jar && ClassLoader 隔離問題

動態加載Jar:

Java 中動態加載 Jar 比較簡單,以下:

[java]  view plain  copy
  1. URL[] urls = new URL[] {new URL("file:libs/jar1.jar")};  
  2. URLClassLoader loader = new URLClassLoader(urls, parentLoader);  

表示加載 libs 下面的 jar1.jar,其中 parentLoader 就是上面1中的 parent,能夠爲當前的 ClassLoader。


ClassLoader 隔離問題:

你們以爲一個運行程序中有沒有可能同時存在兩個包名和類名徹底一致的類?
JVM 及 Dalvik 對類惟一的識別是 ClassLoader id + PackageName + ClassName,因此一個運行程序中是有可能存在兩個包名和類名徹底一致的類的。而且若是這兩個」類」不是由一個 ClassLoader 加載,是沒法將一個類的示例強轉爲另一個類的,這就是 ClassLoader 隔離。 如 Android 中碰到以下異常

[java]  view plain  copy
  1. android.support.v4.view.ViewPager can not be cast to android.support.v4.view.ViewPager  

當碰到這種問題時能夠經過 instance.getClass().getClassLoader(); 獲得 ClassLoader,看 ClassLoader 是否同樣。

 

加載不一樣 Jar 包中公共類:

如今 Host 工程包含了 common.jar, jar1.jar, jar2.jar,而且 jar1.jar 和 jar2.jar 都包含了 common.jar,咱們經過 ClassLoader 將 jar1, jar2 動態加載進來,這樣在 Host 中實際是存在三份 common.jar,以下圖:

https://farm4.staticflickr.com/3872/14301963930_2f0f0fe8aa_o.png

咱們怎麼保證 common.jar 只有一份而不會形成上面3中提到的 ClassLoader 隔離的問題呢,其實很簡單,在生成 jar1 和 jar2 時把 common.jar 去掉,只保留 host 中一份,以 host ClassLoader 爲 parentClassLoader 便可。

 

最後:

一道面試題

能不能本身寫個類叫java.lang.System?

答案:一般不能夠,但能夠採起另類方法達到這個需求。 
解釋:爲了避免讓咱們寫System類,類加載採用委託機制,這樣能夠保證爸爸們優先,爸爸們能找到的類,兒子就沒有機會加載。而System類是Bootstrap加載器加載的,就算本身重寫,也老是使用Java系統提供的System,本身寫的System類根本沒有機會獲得加載。

可是,咱們能夠本身定義一個類加載器來達到這個目的,爲了不雙親委託機制,這個類加載器也必須是特殊的。因爲系統自帶的三個類加載器都加載特定目錄下的類,若是咱們本身的類加載器放在一個特殊的目錄,那麼系統的加載器就沒法加載,也就是最終仍是由咱們本身的加載器加載。


本文系轉載,原文連接: http://blog.csdn.net/boyupeng/article/details/47951037