1. 程式人生 > >深入JVM系列(三)之類載入、類載入器、雙親委派機制與常見問題

深入JVM系列(三)之類載入、類載入器、雙親委派機制與常見問題

一.概述

定義:虛擬機器把描述類的資料從Class檔案載入到記憶體,並對資料進行校驗、轉換解析和初始化,最終形成可以被虛擬機器直接使用的java型別。類載入和連線的過程都是在執行期間完成的。

二. 類的載入方式

1):本地編譯好的class中直接載入 2):網路載入:java.net.URLClassLoader可以載入url指定的類 3):從jar、zip等等壓縮檔案載入類,自動解析jar檔案找到class檔案去載入util類 4):從java原始碼檔案動態編譯成為class檔案

三.類載入的時機

1. 類載入的生命週期:載入(Loading)-->驗證(Verification)-->準備(Preparation)-->解析(Resolution)-->初始化(Initialization)-->使用(Using)-->解除安裝(Unloading)

2. 載入:這有虛擬機器自行決定。

3. 初始化階段:

a) 遇到new、getstatic、putstatic、invokestatic這4個位元組碼指令時,如果類沒有進行過初始化,出發初始化操作。

b) 使用java.lang.reflect包的方法對類進行反射呼叫時。

c) 當初始化一個類的時候,如果發現其父類還沒有執行初始化則進行初始化。

d) 虛擬機器啟動時使用者需要指定一個需要執行的主類,虛擬機器首先初始化這個主類。

注意:介面與類的初始化規則在第三點不同,介面不要氣所有的父介面都進行初始化。

四.類載入的過程

4.1. 載入

a) 載入階段的工作

i. 通過一個類的全限定名來獲取定義此類的二進位制位元組流。

ii. 將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構。

iii. 在java堆中生成一個代表這個類的java.lang.Class物件,做為方法區這些資料的訪問入口。

b) 載入階段完成之後二進位制位元組流就按照虛擬機器所需的格式儲存在方區去中。

4.2. 驗證

這一階段的目的是為了確保Class檔案的位元組流中包含的資訊符合當前虛擬機器的要求。

a) 檔案格式驗證:驗證位元組流是否符合Class檔案格式的規範,並且能被當前版本的虛擬機器處理。

b) 元資料驗證:對位元組碼描述的資訊進行語義分析,以確保其描述的資訊符合java語言規範的要求。

c) 位元組碼驗證:這個階段的主要工作是進行資料流和控制流的分析。任務是確保被驗證類的方法在執行時不會做出危害虛擬機器安全的行為。

d) 符號引用驗證:這一階段發生在虛擬機器將符號引用轉換為直接引用的時候(解析階段),主要是對類自身以外的資訊進行匹配性的校驗。目的是確保解析動作能夠正常執行。

4.3. 準備

準備階段是正式為變數分配記憶體並設定初始值,這些記憶體都將在方法區中進行分配,這裡的變數僅包括類標量不包括例項變數。

4.4. 解析

解析是虛擬機器將常量池的符號引用替換為直接引用的過程。

a) 符號引用:符號引用以一組符號來描述所引用的目標,符號可以是任意形式的字面量,只要使用時能無歧義地定位到目標即可。符號引用與虛擬機器實現的記憶體佈局無關,引用的目標並不一定已經載入到記憶體中。

b) 直接引用:直接引用可以是直接指向目標的指標,相對偏移量或是一個能間接定位到目標的控制代碼。直接飲用是與記憶體佈局相關的。

c) 類或介面的解析

d) 欄位的解析

e) 類方法解析

f) 介面方法解析

4.5. 初始化

是根據程式設計師制定的主觀計劃區初始化變數和其他資源,或者可以從另外一個角度來表達:初始化階段是執行類構造器<clinit>()方法的過程。

五. JVM三種預定義型別類載入器

當一個 JVM 啟動的時候,Java 預設開始使用如下三種類型類裝入器:

啟動(Bootstrap)類載入器:引導類裝入器是用原生代碼實現的類裝入器,它負責將 <Java_Runtime_Home>/lib 下面的類庫載入到記憶體中。由於引導類載入器涉及到虛擬機器本地實現細節,開發者無法直接獲取到啟動類載入器的引用,所以不允許直接通過引用進行操作。

標準擴充套件(Extension)類載入器:擴充套件類載入器是由 Sun 的 ExtClassLoader(sun.misc.Launcher$ExtClassLoader) 實現的。它負責將 

< Java_Runtime_Home >/lib/ext 或者由系統變數 java.ext.dir 指定位置中的類庫載入到記憶體中。開發者可以直接使用標準擴充套件類載入器。

系統(System)類載入器:系統類載入器是由 Sun 的 AppClassLoader(sun.misc.Launcher$AppClassLoader)實現的。它負責將系統類路徑(CLASSPATH)中指定的類庫載入到記憶體中。開發者可以直接使用系統類載入器。

除了以上列舉的三種類載入器,還有一種比較特殊的型別就是執行緒上下文類載入器,這個將在後面單獨介紹。

a. Bootstrap ClassLoader/啟動類載入器

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

b. Extension ClassLoader/擴充套件類載入器

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

c. System ClassLoader/系統類載入器

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

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

在程式執行期間, 通過java.lang.ClassLoader的子類動態載入class檔案, 體現java動態實時類裝入特性.

六. 類載入雙親委派機制介紹和分析

       在這裡,需要著重說明的是,JVM在載入類時預設採用的是雙親委派機制。通俗的講,就是某個特定的類載入器在接到載入類的請求時,首先將載入任務委託給父類載入器,依次遞迴,如果父類載入器可以完成類載入任務,就成功返回;只有父類載入器無法完成此載入任務時,才自己去載入。關於虛擬機器預設的雙親委派機制,我們可以從系統類載入器和標準擴充套件類載入器為例作簡單分析。


       圖一 標準擴充套件類載入器繼承層次圖


                   
        圖二 系統類載入器繼承層次圖

       通過圖一和圖二我們可以看出,類載入器均是繼承自java.lang.ClassLoader抽象類。我們下面我們就看簡要介紹一下java.lang.ClassLoader中幾個最重要的方法:

//載入指定名稱(包括包名)的二進位制型別,供使用者呼叫的介面
public Class<?> loadClass(String name) throws ClassNotFoundException{//…}
//載入指定名稱(包括包名)的二進位制型別,同時指定是否解析(但是,這裡的resolve引數不一定真正能達到解析的效果~_~),供繼承用
protectedsynchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{//…}
//findClass方法一般被loadClass方法呼叫去載入指定名稱類,供繼承用
protected Class<?> findClass(String name) throws ClassNotFoundException {//…}
//定義型別,一般在findClass方法中讀取到對應位元組碼後呼叫,可以看出不可繼承(說明:JVM已經實現了對應的具體功能,解析對應的位元組碼,產生對應的內部資料結構放置到方法區,所以無需覆寫,直接呼叫就可以了)
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
throws ClassFormatError{//…}


       通過進一步分析標準擴充套件類載入器(sun.misc.Launcher$ExtClassLoader)和系統類載入器(sun.misc.Launcher$AppClassLoader)的程式碼以及其公共父類(java.net.URLClassLoader和java.security.SecureClassLoader)的程式碼可以看出,都沒有覆寫java.lang.ClassLoader中預設的載入委派規則---loadClass(…)方法。既然這樣,我們就可以通過分析java.lang.ClassLoader中的loadClass(String name)方法的程式碼就可以分析出虛擬機器預設採用的雙親委派機制到底是什麼模樣:

public Class<?> loadClass(String name)throws ClassNotFoundException {
       return loadClass(name,false);
}
protectedsynchronized Class<?> loadClass(String name,boolean resolve)
           throws ClassNotFoundException {
       //首先判斷該型別是否已經被載入
        Class c = findLoadedClass(name);
       if (c ==null) {
           //如果沒有被載入,就委託給父類載入或者委派給啟動類載入器載入
           try {
               if (parent !=null) {
//如果存在父類載入器,就委派給父類載入器載入
                    c = parent.loadClass(name,false);
                }else {
//如果不存在父類載入器,就檢查是否是由啟動類載入器載入的類,通過呼叫本地方法native Class findBootstrapClass(String name)
                    c = findBootstrapClass0(name);
                }
            }catch (ClassNotFoundException e) {
       //如果父類載入器和啟動類載入器都不能完成載入任務,才呼叫自身的載入功能
                c = findClass(name);
            }
        }
       if (resolve) {
            resolveClass(c);
        }
       return c;
    }


   通過上面的程式碼分析,我們可以對JVM採用的雙親委派類載入機制有了更感性的認識,下面我們就接著分析一下啟動類載入器、標準擴充套件類載入器和系統類載入器三者之間的關係。可能大家已經從各種資料上面看到了如下類似的一幅圖片:


                    
         圖三 類載入器預設委派關係圖

上面圖片給人的直觀印象是系統類載入器的父類載入器是標準擴充套件類載入器,標準擴充套件類載入器的父類載入器是啟動類載入器,下面我們就用程式碼具體測試一下:

示例程式碼:

public static void main(String[] args) {
   try {
     System.out.println(ClassLoader.getSystemClassLoader());
     System.out.println(ClassLoader.getSystemClassLoader().getParent();
     System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());
   } catch (Exception e) {
       e.printStackTrace();
   }
}


說明:通過java.lang.ClassLoader.getSystemClassLoader()可以直接獲取到系統類載入器。

程式碼輸出如下:
[email protected]
[email protected]
null


   通過以上的程式碼輸出,我們可以判定系統類載入器的父載入器是標準擴充套件類載入器,但是我們試圖獲取標準擴充套件類載入器的父類載入器時確得到了null,就是說標準擴充套件類載入器本身強制設定父類載入器為null。我們還是藉助於程式碼分析一下:

     我們首先看一下java.lang.ClassLoader抽象類中預設實現的兩個建構函式:

protected ClassLoader() {
        SecurityManager security = System.getSecurityManager();
       if (security !=null) {
            security.checkCreateClassLoader();
        }
       //預設將父類載入器設定為系統類載入器,getSystemClassLoader()獲取系統類載入器
       this.parent = getSystemClassLoader();
        initialized =true;
    }
   protected ClassLoader(ClassLoader parent) {
        SecurityManager security = System.getSecurityManager();
       if (security !=null) {
            security.checkCreateClassLoader();
        }
       //強制設定父類載入器
       this.parent = parent;
        initialized =true;
    }

   我們再看一下ClassLoader抽象類中parent成員的宣告:

      // The parent class loader for delegation
private ClassLoaderparent;

宣告為私有變數的同時並沒有對外提供可供派生類訪問的public或者protected設定器介面(對應的setter方法),結合前面的測試程式碼的輸出,我們可以推斷出

1.系統類載入器(AppClassLoader)呼叫ClassLoader(ClassLoader parent)建構函式將父類載入器設定為標準擴充套件類載入器(ExtClassLoader)。(因為如果不強制設定,預設會通過呼叫getSystemClassLoader()方法獲取並設定成系統類載入器,這顯然和測試輸出結果不符。)

2.擴充套件類載入器(ExtClassLoader)呼叫ClassLoader(ClassLoader parent)建構函式將父類載入器設定為null。(因為如果不強制設定,預設會通過呼叫getSystemClassLoader()方法獲取並設定成系統類載入器,這顯然和測試輸出結果不符。)

     現在我們可能會有這樣的疑問:擴充套件類載入器(ExtClassLoader)的父類載入器被強制設定為null了,那麼擴充套件類載入器為什麼還能將載入任務委派給啟動類載入器呢?

  

    圖四 標準擴充套件類載入器和系統類載入器成員大綱檢視

                  
 
    圖五擴充套件類載入器和系統類載入器公共父類成員大綱檢視

    通過圖四和圖五可以看出,標準擴充套件類載入器和系統類載入器及其父類(java.net.URLClassLoader和java.security.SecureClassLoader)都沒有覆寫java.lang.ClassLoader中預設的載入委派規則---loadClass(…)方法。有關java.lang.ClassLoader中預設的載入委派規則前面已經分析過,如果父載入器為null,則會呼叫本地方法進行啟動類載入嘗試。所以,圖三中,啟動類載入器、標準擴充套件類載入器和系統類載入器之間的委派關係事實上是仍就成立的。(在後面的使用者自定義類載入器部分,還會做更深入的分析)。

七. 類載入雙親委派示例

以上已經簡要介紹了虛擬機器預設使用的啟動類載入器、標準擴充套件類載入器和系統類載入器,並以三者為例結合JDK程式碼對JVM預設使用的雙親委派類載入機制做了分析。下面我們就來看一個綜合的例子。首先在eclipse中建立一個簡單的java應用工程,然後寫一個簡單的JavaBean如下:

package classloader.test.bean;
   publicclass TestBean {
       public TestBean() {}
}


在現有當前工程中另外建立一測試類(ClassLoaderTest.java)內容如下:

測試一:

publicclass ClassLoaderTest {
   publicstaticvoid main(String[] args) {
       try {
           //檢視當前系統類路徑中包含的路徑條目
            System.out.println(System.getProperty("java.class.path"));
//呼叫載入當前類的類載入器(這裡即為系統類載入器)載入TestBean
Class typeLoaded = Class.forName("classloader.test.bean.TestBean");
//檢視被載入的TestBean型別是被那個類載入器載入的
            System.out.println(typeLoaded.getClassLoader());
        }catch (Exception e) {
            e.printStackTrace();
        }
    }
}


對應的輸出如下:

D:"DEMO"dev"Study"ClassLoaderTest"bin
[email protected]


(說明:當前類路徑預設的含有的一個條目就是工程的輸出目錄)

測試二:

將當前工程輸出目錄下的…/classloader/test/bean/TestBean.class打包進test.jar剪貼到< Java_Runtime_Home >/lib/ext目錄下(現在工程輸出目錄下和JRE擴充套件目錄下都有待載入型別的class檔案)。再執行測試測試程式碼,結果如下:

D:"DEMO"dev"Study"ClassLoaderTest"bin
[email protected]


對比測試一和測試二,我們明顯可以驗證前面說的雙親委派機制,系統類載入器在接到載入classloader.test.bean.TestBean型別的請求時,首先將請求委派給父類載入器(標準擴充套件類載入器),標準擴充套件類載入器搶先完成了載入請求。

測試三:

test.jar拷貝一份到< Java_Runtime_Home >/lib下,執行測試程式碼,輸出如下:

D:"DEMO"dev"Study"ClassLoaderTest"bin
[email protected]


  測試三和測試二輸出結果一致。那就是說,放置到< Java_Runtime_Home >/lib目錄下的TestBean對應的class位元組碼並沒有被載入,這其實和前面講的雙親委派機制並不矛盾。虛擬機器出於安全等因素考慮,不會載入< Java_Runtime_Home >/lib存在的陌生類開發者通過將要載入的非JDK自身的類放置到此目錄下期待啟動類載入器載入是不可能的。做個進一步驗證,刪除< Java_Runtime_Home >/lib/ext目錄下和工程輸出目錄下的TestBean對應的class檔案,然後再執行測試程式碼,則將會有ClassNotFoundException異常丟擲。有關這個問題,大家可以在java.lang.ClassLoader中的loadClass(String name, boolean resolve)方法中設定相應斷點執行測試三進行除錯,會發現findBootstrapClass0()會丟擲異常,然後在下面的findClass方法中被載入,當前執行的類載入器正是擴充套件類載入器(sun.misc.Launcher$ExtClassLoader),這一點可以通過JDT中變數檢視檢視驗證。

八. 程式動態擴充套件方式

Java的連線模型允許使用者執行時擴充套件引用程式,既可以通過當前虛擬機器中預定義的載入器載入編譯時已知的類或者介面,又允許使用者自行定義類裝載器,在執行時動態擴充套件使用者的程式。通過使用者自定義的類裝載器,你的程式可以裝載在編譯時並不知道或者尚未存在的類或者介面,並動態連線它們並進行有選擇的解析。

執行時動態擴充套件java應用程式有如下兩個途徑:

8.1.呼叫java.lang.Class.forName(…)

這個方法其實在前面已經討論過,在後面的問題2解答中說明了該方法呼叫會觸發那個類載入器開始載入任務。這裡需要說明的是多引數版本的forName(…)方法:

public static Class<?> forName(String name, boolean initialize, ClassLoader loader) throws ClassNotFoundException


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

8.2.使用者自定義類載入器

通過前面的分析,我們可以看出,除了和本地實現密切相關的啟動類載入器之外,包括標準擴充套件類載入器和系統類載入器在內的所有其他類載入器我們都可以當做自定義類載入器來對待,唯一區別是是否被虛擬機器預設使用。前面的內容中已經對java.lang.ClassLoader抽象類中的幾個重要的方法做了介紹,這裡就簡要敘述一下一般使用者自定義類載入器的工作流程吧(可以結合後面問題解答一起看):

1、首先檢查請求的型別是否已經被這個類裝載器裝載到名稱空間中了,如果已經裝載,直接返回;否則轉入步驟2

2、委派類載入請求給父類載入器(更準確的說應該是雙親類載入器,真個虛擬機器中各種類載入器最終會呈現樹狀結構),如果父類載入器能夠完成,則返回父類載入器載入的Class例項;否則轉入步驟3

3、呼叫本類載入器的findClass(…)方法,試圖獲取對應的位元組碼,如果獲取的到,則呼叫defineClass(…)匯入型別到方法區;如果獲取不到對應的位元組碼或者其他原因失敗,返回異常給loadClass(…), loadClass(…)轉拋異常,終止載入過程(注意:這裡的異常種類不止一種)。

       (說明:這裡說的自定義類載入器是指JDK 1.2以後版本的寫法,即不覆寫改變java.lang.loadClass(…)已有委派邏輯情況下)

九. 常見問題分析

9.1.由不同的類載入器載入的指定型別還是相同的型別嗎?

在Java中,一個類用其完全匹配類名(fully qualified class name)作為標識,這裡指的完全匹配類名包括包名和類名。但在JVM中一個類用其全名和一個載入類ClassLoader的例項作為唯一標識,不同類載入器載入的類將被置於不同的名稱空間.我們可以用兩個自定義類載入器去載入某自定義型別(注意,不要將自定義型別的位元組碼放置到系統路徑或者擴充套件路徑中,否則會被系統類載入器或擴充套件類載入器搶先載入),然後用獲取到的兩個Class例項進行java.lang.Object.equals(…)判斷,將會得到不相等的結果。這個大家可以寫兩個自定義的類載入器去載入相同的自定義型別,然後做個判斷;同時,可以測試載入java.*型別,然後再對比測試一下測試結果。

9.2.在程式碼中直接呼叫Class.forName(String name)方法,到底會觸發那個類載入器進行類載入行為?

Class.forName(String name)預設會使用呼叫類的類載入器來進行類載入。我們直接來分析一下對應的jdk的程式碼:

//java.lang.Class.java
       publicstatic Class<?>forName(String className)throws ClassNotFoundException {
return forName0(className,true, ClassLoader.getCallerClassLoader());
}
//java.lang.ClassLoader.java
// Returns the invoker's class loader, or null if none.
static ClassLoader getCallerClassLoader() {
              // 獲取呼叫類(caller)的型別
        Class caller = Reflection.getCallerClass(3);
              // This can be null if the VM is requesting it
       if (caller ==null) {
           returnnull;
        }
       //呼叫java.lang.Class中本地方法獲取載入該呼叫類(caller)的ClassLoader
       return caller.getClassLoader0();
}
//java.lang.Class.java
//虛擬機器本地實現,獲取當前類的類載入器,前面介紹的Class的getClassLoader()也使用此方法
native ClassLoader getClassLoader0();


9.3.在編寫自定義類載入器時,如果沒有設定父載入器,那麼父載入器是?

前面講過,在不指定父類載入器的情況下,預設採用系統類載入器。可能有人覺得不明白,現在我們來看一下JDK對應的程式碼實現。眾所周知,我們編寫自定義的類載入器直接或者間接繼承自java.lang.ClassLoader抽象類,對應的無參預設建構函式實現如下:

//摘自java.lang.ClassLoader.java
protected ClassLoader() {
           SecurityManager security = System.getSecurityManager();
          if (security !=null) {
               security.checkCreateClassLoader();
           }
          this.parent = getSystemClassLoader();
           initialized =true;
}


我們再來看一下對應的getSystemClassLoader()方法的實現:

privatestaticsynchronizedvoid initSystemClassLoader() {
           //...
           sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
           scl = l.getClassLoader();
           //...
}


我們可以寫簡單的測試程式碼來測試一下:

System.out.println(sun.misc.Launcher.getLauncher().getClassLoader());


本機對應輸出如下:

[email protected]


所以,我們現在可以相信當自定義類載入器沒有指定父類載入器的情況下,預設的父類載入器即為系統類載入器。同時,我們可以得出如下結論:

即時使用者自定義類載入器不指定父類載入器,那麼,同樣可以載入如下三個地方的類:

1.   <Java_Runtime_Home>/lib下的類

2.   < Java_Runtime_Home >/lib/ext下或者由系統變數java.ext.dir指定位置中的類

3.   當前工程類路徑下或者由系統變數java.class.path指定位置中的類

9.4.在編寫自定義類載入器時,如果將父類載入器強制設定為null,那麼會有什麼影響?如果自定義的類載入器不能載入指定類,就肯定會載入失敗嗎?

JVM規範中規定如果使用者自定義的類載入器將父類載入器強制設定為null,那麼會自動將啟動類載入器設定為當前使用者自定義類載入器的父類載入器(這個問題前面已經分析過了)。同時,我們可以得出如下結論:

即時使用者自定義類載入器不指定父類載入器,那麼,同樣可以載入到<Java_Runtime_Home>/lib下的類,但此時就不能夠載入<Java_Runtime_Home>/lib/ext目錄下的類了。

   說明:問題3和問題4的推斷結論是基於使用者自定義的類載入器本身延續了java.lang.ClassLoader.loadClass(…)預設委派邏輯,如果使用者對這一預設委派邏輯進行了改變,以上推斷結論就不一定成立了,詳見問題5。

9.5.編寫自定義類載入器時,一般有哪些注意點?

9.5.1.一般儘量不要覆寫已有的loadClass(…)方法中的委派邏輯

一般在JDK 1.2之前的版本才這樣做,而且事實證明,這樣做極有可能引起系統預設的類載入器不能正常工作。在JVM規範和JDK文件中(1.2或者以後版本中),都沒有建議使用者覆寫loadClass(…)方法,相比而言,明確提示開發者在開發自定義的類載入器時覆寫findClass(…)邏輯。舉一個例子來驗證該問題:

//使用者自定義類載入器WrongClassLoader.Java(覆寫loadClass邏輯)
publicclassWrongClassLoaderextends ClassLoader {
       public Class<?> loadClass(String name)throws ClassNotFoundException {
           returnthis.findClass(name);
        }
       protected Class<?> findClass(String name)throws ClassNotFoundException {
           //假設此處只是到工程以外的特定目錄D:/library下去載入類
           具體實現程式碼省略
        }
}


   通過前面的分析我們已經知道,使用者自定義類載入器(WrongClassLoader)的默

       認的類載入器是系統類載入器,但是現在問題4種的結論就不成立了。大家可以簡

       單測試一下,現在<Java_Runtime_Home>/lib、< Java_Runtime_Home >/lib/ext和工

       程類路徑上的類都載入不上了。

//問題5測試程式碼一
publicclass WrongClassLoaderTest {
       publicstaticvoid main(String[] args) {
          try {
               WrongClassLoader loader =new WrongClassLoader();
               Class classLoaded = loader.loadClass("beans.Account");
               System.out.println(classLoaded.getName());
               System.out.println(classLoaded.getClassLoader());
           }catch (Exception e) {
               e.printStackTrace();
           }
        }
}

(說明:D:"classes"beans"Account.class物理存在的)

輸出結果:

java.io.FileNotFoundException: D:"classes"java"lang"Object.class (系統找不到指定的路徑。)
    at java.io.FileInputStream.open(Native Method)
    at java.io.FileInputStream.<init>(FileInputStream.java:106)
    at WrongClassLoader.findClass(WrongClassLoader.java:40)
    at WrongClassLoader.loadClass(WrongClassLoader.java:29)
    at java.lang.ClassLoader.loadClassInternal(ClassLoader.java:319)
    at java.lang.ClassLoader.defineClass1(Native Method)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:620)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:400)
    at WrongClassLoader.findClass(WrongClassLoader.java:43)
    at WrongClassLoader.loadClass(WrongClassLoader.java:29)
    at WrongClassLoaderTest.main(WrongClassLoaderTest.java:27)
Exception in thread "main" java.lang.NoClassDefFoundError: java/lang/Object
    at java.lang.ClassLoader.defineClass1(Native Method)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:620)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:400)
    at WrongClassLoader.findClass(WrongClassLoader.java:43)
    at WrongClassLoader.loadClass(WrongClassLoader.java:29)
    at WrongClassLoaderTest.main(WrongClassLoaderTest.java:27)


這說明,連要載入的型別的超型別java.lang.Object都載入不到了。這裡列舉的由於覆寫loadClass(…)引起的邏輯錯誤明顯是比較簡單的,實際引起的邏輯錯誤可能複雜的多。

//問題5測試二
//使用者自定義類載入器WrongClassLoader.Java(不覆寫loadClass邏輯)
publicclassWrongClassLoaderextends ClassLoader {
       protected Class<?> findClass(String name)throws ClassNotFoundException {
           //假設此處只是到工程以外的特定目錄D:/library下去載入類
           具體實現程式碼省略
        }
}


將自定義類載入器程式碼WrongClassLoader.Java做以上修改後,再執行測試程式碼,輸出結果如下:

beans.Account
[email protected]


這說明,beans.Account載入成功,且是由自定義類載入器WrongClassLoader載入。

這其中的原因分析,我想這裡就不必解釋了,大家應該可以分析的出來了。

9.5.2.正確設定父類載入器

通過上面問題4和問題5的分析我們應該已經理解,個人覺得這是自定義使用者類載入器時最重要的一點,但常常被忽略或者輕易帶過。有了前面JDK程式碼的分析作為基礎,我想現在大家都可以隨便舉出例子了。

9.5.3.保證findClass(String)方法的邏輯正確性

事先儘量準確理解待定義的類載入器要完成的載入任務,確保最大程度上能夠獲取到對應的位元組碼內容。

9.6.如何在執行時判斷系統類載入器能載入哪些路徑下的類?

一是可以直接呼叫ClassLoader.getSystemClassLoader()或者其他方式獲取到系統類載入器(系統類載入器和擴充套件類載入器本身都派生自URLClassLoader),呼叫URLClassLoader中的getURLs()方法可以獲取到;

二是可以直接通過獲取系統屬性java.class.path 來檢視當前類路徑上的條目資訊 , System.getProperty("java.class.path")

9.7.如何在執行時判斷標準擴充套件類載入器能載入哪些路徑下的類?

方法之一:

try {
               URL[] extURLs = ((URLClassLoader)ClassLoader.getSystemClassLoader().getParent()).getURLs();
              for (int i = 0; i < extURLs.length; i++) {
                     System.out.println(extURLs[i]);
              }
       } catch (Exception e) {//…}


       本機對應輸出如下:

file:/D:/DEMO/jdk1.5.0_09/jre/lib/ext/dnsns.jar
file:/D:/DEMO/jdk1.5.0_09/jre/lib/ext/localedata.jar
file:/D:/DEMO/jdk1.5.0_09/jre/lib/ext/sunjce_provider.jar
file:/D:/DEMO/jdk1.5.0_09/jre/lib/ext/sunpkcs11.jar


十、再分析類載入

10.1.類載入器的特性

1, 每個ClassLoader都維護了一份自己的名稱空間, 同一個名稱空間裡不能出現兩個同名的類。
2, 為了實現java安全沙箱模型頂層的類載入器安全機制, java預設採用了 ” 雙親委派的載入鏈 ” 結構.


如下圖:


Class Diagram:


類圖中, BootstrapClassLoader是一個單獨的java類, 其實在這裡, 不應該叫他是一個java類。
因為, 它已經完全不用java實現了。

它是在jvm啟動時, 就被構造起來的, 負責java平臺核心庫。(具體上面已經有介紹)

10.2.自定義類載入器載入一個類的步驟

 

ClassLoader 類載入邏輯分析, 以下邏輯是除 BootstrapClassLoader 外的類載入器載入流程:

 // 檢查類是否已被裝載過  
   Class c = findLoadedClass(name);  
   if (c == null ) {  
        // 指定類未被裝載過  
        try {  
            if (parent != null ) {  
                // 如果父類載入器不為空, 則委派給父類載入  
                c = parent.loadClass(name, false );  
            } else {  
               // 如果父類載入器為空, 則委派給啟動類載入載入  
               c = findBootstrapClass0(name);  
           }  
       } catch (ClassNotFoundException e) {  
           // 啟動類載入器或父類載入器丟擲異常後, 當前類載入器將其  
           // 捕獲, 並通過findClass方法, 由自身載入  
           c = findClass(name);  
       }  
  }  


10.3.用Class.forName載入類

Class.forName使用的是被呼叫者的類載入器來載入類的.
這種特性, 證明了java類載入器中的名稱空間是唯一的, 不會相互干擾.

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

public static Class forName(String className)  
        throws ClassNotFoundException {  
        return forName0(className, true , ClassLoader.getCallerClassLoader());  
   }  
      
   /** Called after security checks have been made. */  
   private static native Class forName0(String name, boolean initialize,  
   ClassLoader loader)  
        throws ClassNotFoundException;  

上圖中 ClassLoader.getCallerClassLoader 就是得到呼叫當前forName方法的類的類載入器

10.4.執行緒上下文類載入器

java預設的執行緒上下文類載入器是 系統類載入器(AppClassLoader).
// Now create the class loader to use to launch the application  
   try {  
       loader = AppClassLoader.getAppClassLoader(extcl);  
   } catch (IOException e) {  
       throw new InternalError(  
   "Could not create application class loader" );  
   }  
      
   // Also set the context class loader for the primordial thread.  
  Thread.currentThread().setContextClassLoader(loader);  

以上程式碼摘自sun.misc.Launch的無參建構函式Launch()。

使用執行緒上下文類載入器, 可以在執行執行緒中, 拋棄雙親委派載入鏈模式, 使用執行緒上下文裡的類載入器載入類.

典型的例子有, 通過執行緒上下文來載入第三方庫jndi實現, 而不依賴於雙親委派.

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


還有一些採用 hotswap 特性的框架, 也使用了執行緒上下文類載入器, 比如 seasar (full stack framework in japenese).

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

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

隨著多核時代的來臨, 相信多執行緒開發將會越來越多地進入程式設計師的實際編碼過程中. 因此,
在編寫基礎設施時, 通過使用執行緒上下文來載入類, 應該是一個很好的選擇.

當然, 好東西都有利弊. 使用執行緒上下文載入類, 也要注意, 保證多根需要通訊的執行緒間的類載入器應該是同一個,
防止因為不同的類載入器, 導致型別轉換異常(ClassCastException).

10.5.自定義的類載入器實現

defineClass(String name, byte[] b, int off, int len,ProtectionDomain protectionDomain)
是java.lang.Classloader提供給開發人員, 用來自定義載入class的介面.

使用該介面, 可以動態的載入class檔案.

例如,
在jdk中, URLClassLoader是配合findClass方法來使用defineClass, 可以從網路或硬碟上載入class.

而使用類載入介面, 並加上自己的實現邏輯, 還可以定製出更多的高階特性.

比如,

一個簡單的hot swap 類載入器實現:

import java.io.File;  
   import java.io.FileInputStream;  
   import java.lang.reflect.Method;  
   import java.net.URL;  
   import java.net.URLClassLoader;  
      
   /** 
   * 可以重新載入同名類的類載入器實現 
   * 
    
  * 放棄了雙親委派的載入鏈模式. 
  * 需要外部維護過載後的類的成員變數狀態. 
  * 
  * @author ken.wu 
  * @mail [email protected] 
  * 2007-9-28 下午01:37:43 
  */  
  public class HotSwapClassLoader extends URLClassLoader {  
     
      public HotSwapClassLoader(URL[] urls) {  
          super (urls);  
      }  
     
      public HotSwapClassLoader(URL[] urls, ClassLoader parent) {  
          super (urls, parent);  
      }  
     
      public Class load(String name)  
            throws ClassNotFoundException {  
          return load(name, false );  
      }  
     
      public Class load(String name, boolean resolve)  
            throws ClassNotFoundException {  
          if ( null != super .findLoadedClass(name))  
              return reload(name, resolve);  
     
          Class clazz = super .findClass(name);  
     
          if (resolve)  
              super .resolveClass(clazz);  
     
          return clazz;  
      }  
     
      public Class reload(String name, boolean resolve)  
            throws ClassNotFoundException {  
          return new HotSwapClassLoader( super .getURLs(), super .getParent()).load(  
              name, resolve);  
      }  
  }  
     
  public class A {  
      private B b;  
     
      public void setB(B b) {  
           this .b = b;  
      }  
     
      public B getB() {  
           return b;  
      }  
  }  
     
  public class B {}  

這個類的作用是可以重新載入同名的類, 但是, 為了實現hotswap, 老的物件狀態
需要通過其他方式拷貝到過載過的類生成的全新例項中來。(A類中的b例項)

而新例項所依賴的B類如果與老物件不是同一個類載入器載入的, 將會丟擲型別轉換異常(ClassCastException).

為了解決這種問題, HotSwapClassLoader自定義了load方法. 即當前類是由自身classLoader載入的, 而內部依賴的類

還是老物件的classLoader載入的.

public class TestHotSwap {  
   public static void main(String args[]) {  
       A a = new A();  
       B b = new B();  
       a.setB(b);  
      
       System.out.printf("A classLoader is %s n" , a.getClass().getClassLoader());  
       System.out.printf("B classLoader is %s n" , b.getClass().getClassLoader());  
       System.out.printf("A.b classLoader is %s n" ,   a.getB().getClass().getClassLoader());  
     
      HotSwapClassLoader c1 = new HotSwapClassLoader( new URL[]{ new URL( "file:\e:\test\")} , a.getClass().getClassLoader());  
      Class clazz = c1.load(" test.hotswap.A ");  
      Object aInstance = clazz.newInstance();  
     
      Method method1 = clazz.getMethod(" setB ", B.class);  
      method1.invoke(aInstance, b);  
     
      Method method2 = clazz.getMethod(" getB ", null);  
      Object bInstance = method2.invoke(aInstance, null);  
     
      System.out.printf(" reloaded A.b classLoader is %s n", bInstance.getClass().getClassLoader());  
  }  
  }  

輸出

A classLoader is [email protected]
B classLoader is [email protected]
A.b classLoader is [email protected]
reloaded A.b classLoader is [email protected]


相關推薦

深入JVM系列之類載入載入雙親委派機制常見問題

一.概述 定義:虛擬機器把描述類的資料從Class檔案載入到記憶體,並對資料進行校驗、轉換解析和初始化,最終形成可以被虛擬機器直接使用的java型別。類載入和連線的過程都是在執行期間完成的。 二.

深入JVM系列之GC機制收集GC調優

一、回顧JVM記憶體分配 1.1、記憶體分配: 1、物件優先在EDEN分配 2、大物件直接進入老年代  3、長期存活的物件將進入老年代  4、適齡物件也可能進入老年代:動態物件年齡判斷 動態物件年齡判斷: 虛擬機器並不總是要求物件的年齡必須達到MaxTenuring

深入JVM系列之記憶體模型記憶體分配

Java 方法棧也是執行緒私有的,每個 Java 方法棧都是由一個個棧幀組成的,每個棧幀是一個方法執行期的基礎資料結構,它儲存著區域性變量表、運算元棧、動態連結、方法出口等資訊。當執行緒呼叫呼叫了一個 Java 方法時,一個棧幀就被壓入(push)到相應的 Java 方法棧。當執行緒從一個 Java 方法

縮放系列:一個可以手勢縮放拖拽旋轉的layout

弄了一個下午,終於搞出來了,PowerfulLayout 下面是一個功能強大的改造的例子: 可以實現以下需求: 1.兩個手指進行縮放佈局 2.所有子控制元件也隨著縮放, 3.子控制元件該有的功能不能丟失(像button有可被點選的功能,縮放後不能丟失該功能)

PyTorch基礎系列——深入理解autograd:Variable屬性方法【最新已經和tensor合併為一類】

torch.autograd.backward(variables, grad_variables, retain_variables=False) 當前Variable對leaf variable求偏導。 計算圖可以通過鏈式法則求導。如果Variable是 非標量(non-scalar

深入理解JVM:虛擬機器載入機制

虛擬機器把描述類的資料從Class檔案載入到記憶體,並對資料進行校驗、轉換解析和初始化,最終形成可以被虛擬機器直接使用的Java型別,這就是虛擬機器的類載入機制。 在Java中,型別的載入、連線和初始化過程都是程式在執行期間完成的,這種策略雖然會令類載入時稍微增

深入理解Java併發框架AQS系列:獨佔鎖Exclusive Lock

[深入理解Java併發框架AQS系列(一):執行緒](https://www.cnblogs.com/xijiu/p/14396061.html) [深入理解Java併發框架AQS系列(二):AQS框架簡介及鎖概念](https://www.cnblogs.com/xijiu/p/14522224.html)

分布式緩存技術redis學習系列——redis高級應用主從事務持久化

master ica not ood www working can 出了 owin 上文《詳細講解redis數據結構(內存模型)以及常用命令》介紹了redis的數據類型以及常用命令,本文我們來學習下redis的一些高級特性。 回到頂部 安全性設置 設置客戶端操作秘密

數據結構系列線性表

復雜 -o -type 復雜度 順序結構 之前 包含 替換 鏈式存儲結構 線性表是什麽 零個或多個數據元素的有序序列 線性存儲結構 例如 java中的數組,每次都申請固定長度內存空間,並且長度不可變 而arraylist則是長度可變的數組,這是java在底層對數組

【原創】源碼角度分析Android的消息機制系列——ThreadLocal的工作原理

沒有 cit gen 管理 pre 靜態 bsp 允許 clas ι 版權聲明:本文為博主原創文章,未經博主允許不得轉載。 先看Android源碼(API24)中對ThreadLocal的定義: public class ThreadLocal<T>

源碼分析系列x264_deblocking_dataflow

像素 色度 結構 inf blank 水平 frame 垂直 左右 http://www.cnblogs.com/xkfz007/articles/2616157.html 去塊濾波(Deblocking)部分關鍵函數 3.1 deblocking_filter_ed

Windows Server 2012 AD 站點和域部署系列創建站點子網及鏈接

子網 站點 windows server 2012 配置域 本章博文開始在根域ds01 端創建BJ、SH、GZ站點 ,配置子網及相關站點間的鏈接 。創建站點:1、重命名默認站點:登陸ds01,打開“Active Directory 站點和服務”,右鍵點擊默認的站點Default-First-Sit

PHP系列PHP數組數據結構

php數組與數據結構 PHP數組與數據結構數組是把若幹變量按有序的形式組織起來的一種形式。這些數據元素的集數組分為一維二維三維、索引數組(數組索引是整數)和關聯數組 (1)數組的聲明1、一個數組中存的多個內容、數組中的內容叫作“元素”2、每個元素都是由健和值組成的Key/

Linux系統運維常見面試簡答題系列9題

connect 切換 -a ip) 整理 程序 strong ack 自己 1. 寫一個sed命令,修改/tmp/input.txt文件的內容,要求:(1) 刪除所有空行;(2) 一行中,如果包含”11111″,則在”11111″前面插入”AAA”,在”11111″後面插入

Python操作rabbitmq系列:多個接收端消費消息

name 連接 logs http clas header 消費者 exclusive pub 接著上一章。這一章,我們要將同一個消息發給多個客戶端。這就是發布訂閱模式。直接看代碼: 發送端: import pikaimport sysconnection = pika.B

Flask 學習系列---Jinjia2使用過濾器

ide 數位 指定字段 模板 小數 else capital 12px 效果圖 再Jinjia2中過濾器是一種轉變變量輸出內容的技術。··過濾器通過管道符號“|與變量鏈接,並且可以通過圓括號傳遞參數” 。舉例說明: {{my_variable|default(‘my_var

Docker系列容器管理

mozilla http 格式 file tor centos determine dia 進程 3.1 新建容器並啟動所需要的命令主要為docker run [root@localhost ~]# docker run centos /bin/echo "syavi

wifi認證Portal開發系列:portal協議

tro spa size http log ron 認證 gin auto 中國移動WLAN業務PORTAL協議規範介紹 wifi認證Portal開發系列(三):portal協議

詳解YUV系列----YUV420

roc 根據 系列 watermark 存儲方式 圖片 images src fff 前兩講詳細講解了YUV444以及YUV422兩種格式,實際中這兩種格式使用的相對較少,使用比較多的便是本節要梳理的YUV420格式嘍,同樣,老辦法,老套路嘍。 一、文字描述:YUV

C# 多線程系列

job row 空閑 最好 方式 不同的 運行時 作業 tun 線程池 創建線程需要時間,如果有不同的小任務要完成,就可以事先創建許多線程,在應完成這些任務時發出請求。這個線程數最好在需要更多線程時增加,在需要釋放資源時減少。 不需要自己創建這樣的一個列表。該列表由T