1. 程式人生 > >JVM十:虛擬機器類載入機制(2)

JVM十:虛擬機器類載入機制(2)

1. 類載入器作用

          類載入器雖然只用於實現類的載入動作,但它在Java程式起到的作用卻遠大於類載入階段。對於任意一個類,都需要由載入它的類載入器和這個類本身一同確立其在Java虛擬機器中的唯一性,每一個類載入器,都擁有一個獨立的類名稱空間。通俗而言:比較兩個類是否“相等”,只有在這兩個類時由同一個類載入器載入的前提下才有意義,否則,即使這兩個類來源於同一個Class檔案,被同一個虛擬機器載入,只要載入它們的類載入器不同,那這兩個類就必定不相等


2. 類載入器 與 instanceof 關鍵字

     上文所說的“相等”,包括代表類的物件的 equals() 方法、isAssignableFrom()方法、isInstance() 方法的返回結果,也包括使用 instanceof 關鍵字做物件所屬關係判定等情況。

如果沒有注意到類載入器的影響,在某些情況下可能會產生具有迷惑性的結果,以下程式碼演示了不同的類載入器對 instanceof 關鍵字運算結果的影響:

/**
 * 類載入器與instanceof關鍵字演示
 * 
 * @author zzm
 */
public class ClassLoaderTest {

    public static void main(String[] args) throws Exception {

        ClassLoader myLoader = new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                try {
                    String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
                    InputStream is = getClass().getResourceAsStream(fileName);
                    if (is == null) {
                        return super.loadClass(name);
                    }
                    byte[] b = new byte[is.available()];
                    is.read(b);
                    return defineClass(name, b, 0, b.length);
                } catch (IOException e) {
                    throw new ClassNotFoundException(name);
                }
            }
        };

        Object obj = myLoader.loadClass("org.fenixsoft.classloading.ClassLoaderTest").newInstance();

        System.out.println(obj.getClass());
        System.out.println(obj instanceof org.fenixsoft.classloading.ClassLoaderTest);
    }
}

 

執行結果:

這裡寫圖片描述

分析

     以上程式碼中構造了一個簡單的類載入器,它可以載入與自己在同一路徑下的Class檔案。使用這個類載入器去載入一個名為“org.fenixsoft.classloading.ClassLoaderTest”的類,並例項化這個類的物件。

從輸出結果的第一行可以看出,此物件確實是類“org.fenixsoft.classloading.ClassLoaderTest”例項化出的物件,但從第二句看出,此物件與類“org.fenixsoft.classloading.ClassLoaderTest

”做所屬型別檢查的時候卻返回了false,這是因為虛擬機器中存在了兩個 ClassLoaderTest 類,一個是由系統應用程式類載入器載入的,另外一個是由我們自定義的類載入器載入的,雖然都來自同一個Class檔案,但依然是兩個獨立的類,做物件所屬型別檢查時結果自然返回 false




二. 雙親委派模式

1. 虛擬機器角度中的類載入器

虛擬機器的角度來講,只存在兩種不同的類載入器:

  • 啟動類載入器(Bootstrap ClassLoader):此類載入器使用C++實現,是虛擬機器自身的一部分。
  • 所有其他類載入器:由Java語言實現,獨立於虛擬機器外部,並且全都繼承自抽象類 java.lang.ClassLoader

2. Java開發人員角度中的類載入器

Java開發人員的角度來看,類載入器還可以劃分得更細緻一些,絕大部分Java程式都會使用到以下3種系統提供的類載入器:

(1) 啟動類載入器(Bootstrap ClassLoader)

       此類載入器負責將存放在 <JAVA_HOME>\lib 目錄中的,或者被 -Xbootclasspath 引數所指定的路徑中的,並且是虛擬機器識別的(僅按照檔名識別,如 rt.jar,名字不符合的類庫即使放在lib 目錄中也不會被載入)類庫載入到虛擬機器記憶體中。

啟動類載入器無法被 Java 程式直接引用,使用者在編寫自定義類載入器時,如果需要把載入請求委派給引導類載入器,直接使用null代替即可。如下程式碼java.lang.ClassLoader.getClassLoader() 方法的程式碼片段:

    【ClassLoader.getClassLoader() 方法的程式碼片段】
   /**
     * Returns the class loader for the class.  Some implementations may use null to represent the bootstrap class loader. This method will return null in such implementations if this class was loaded by the bootstrap class loader.
     */
    public ClassLoader getClassLoader() {
        ClassLoader cl = getClassLoader0();
        if (cl == null)
            return null;
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            ClassLoader cc1 = ClassLoader.getCallerClassLoader();
            if(cc1 != null && cc1 != c1 && !c1.isAncestor(cc1)){
                sm.checkPermission(SecurityConstants.GET_CLASSLOADER_PERMISSION);
            }
        }
        return cl;
    }

(2)擴充套件類載入器(Extension ClassLoader)

ExtClassLoader(sun.misc.Launcher$ExtClassLoader)實現的。它負責將Java_Home /lib/ext或者由系統變數 java.ext.dir指定位置中的類庫載入到記憶體中,開發者可以直接使用標準擴充套件類載入器。

(3)應用程式類載入器(Application ClassLoader)

AppClassLoader(sun.misc.Launcher$AppClassLoader)實現的。由於這個類載入器是ClassLoader中的getSystemClassLoader()方法的返回值,因此一般稱為系統類載入器

它負責載入使用者類路徑(ClassPath)上所指定的類庫,開發者可以直接使用這個類載入器,如果應用程式中沒有自定義過自己的類載入器,一般情況下這個就是程式中預設的類載入器。


3. 雙親委派模型

(1)概念

我們的應用程式都是由這3 種類載入器互相配合進行載入的,如果有必要,還可以加入自己定義的類載入器。這些類載入器之間的關係如下圖:

這裡寫圖片描述

      上圖展示的類載入器之間的層次關係,稱為類載入器的雙親委派模型(Parents Delegation Model)。該模型要求除了頂層的啟動類載入器外,其餘的類載入器都應該有自己的父類載入器,而這種父子關係一般通過組合關係來實現,而不是通過繼承(程式碼中體現)。

(2)工作過程

        如果一個類載入器收到了類載入的請求,它首先不會自己去嘗試載入,而是把這個請求委派給父類載入器,每一個層次的載入器都是如此,依次遞迴,因此所有的載入請求最終都應該傳送到頂層的啟動類載入器中,只有當父載入器反饋自己無法完成此載入請求(它搜尋範圍中沒有找到所需類)時,子載入器才會嘗試自己載入。

(3)模式優點

      使用雙親委派模型來組織類載入器之間的關係,好處在於Java類隨著它的類載入器一起具備了一種帶有優先順序的層次關係。

例如類java.lang.Object,它存在在rt.jar中,無論哪一個類載入器要載入這個類,最終都是委派給處於模型最頂端的啟動類載入器進行載入,因此Object類在程式的各種類載入器環境中都是同一個類。相反,如果沒有雙親委派模型而是由各個類載入器自行載入的話,使用者編寫了一個java.lang.Object的類,並放在程式的ClassPath中,那系統中將會出現多個不同的Object類,程式將混亂。因此,如果開發者嘗試編寫一個與rt.jar類庫中已有類重名的Java類,將會發現可以正常編譯,但是永遠無法被載入執行。

(4)雙親委派模型的系統實現

雙親委派模型對於保證Java程式的穩定運作很重要,但它的實現卻很簡單,實現集中在java.lang.ClassLoader的loadClass()
方法中,在其方法中,主要判斷邏輯如下:先檢查是否已經被載入過,

  • 若沒有被載入過,則接著判斷父載入器是否為空。
    • 若不為空,則呼叫父類載入器loadClass()方法。
    • 若父載入器為空,則預設使用啟動類載入器作為父載入器(遞迴呼叫)。
  • 如果父載入失敗,則丟擲ClassNotFoundException異常後,再呼叫自己的findClass()方法進行載入。

這裡寫圖片描述

【雙親委派模型的實現】

protected synchronized Class<?> loadClass(String name,boolean resolve)throws ClassNotFoundException{
    //check the class has been loaded or not
    Class c = findLoadedClass(name);
    if(c == null){
        try{
            if(parent != null){
                c = parent.loadClass(name,false);
            }else{
                c = findBootstrapClassOrNull(name);
            }
        }catch(ClassNotFoundException e){
            //if throws the exception ,the father can not complete the load
        }
        if(c == null){
            c = findClass(name);
        }
    }
    if(resolve){
        resolveClass(c);
    }
    return c;
}

(5)注意(findClass方法)

在檢視學習以上ClassLoader的實現後,注意一個地方,即“如果父載入失敗,則丟擲ClassNotFoundException異常後,再呼叫自己的findClass()方法進行載入”這一步邏輯,進一步檢視findClass()方法:

protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
    }

    沒錯!此方法沒有具體實現,只是拋了一個異常,而且訪問許可權是protected這充分證明了:這個方法就是給開發者重寫用的,即自定義類載入器時需實現此方法!

三. 例項驗證雙親委派模式

在瞭解了以上多種類載入器和雙親委派模式的理論實現後,對這個“雙親委派模式”運作,自我感覺還是有點迷糊,所以接下來自定義一個類載入器來親自測試驗證“雙親委派模式”的執行實現,從實踐角度出發來一探究竟。

在瞭解完以上雙親委派模型破壞雙親委派模型後,我們得知自定義類載入器有以下兩種方式:

  • 採用雙親委派模型:只需要重寫ClassLoaderfindClass()方法即可

  • 破壞雙親委派模型:重寫ClassLoader的整個loadClass()方法(因為雙親委派模型的邏輯主要實現就在此方法中,若我們重寫即可破壞掉。)

不過此次實踐就是為了來探究驗證雙親委派模型,多以我們當然是採取第一種方法來自定義類載入器。


1. 自定義類載入器

首先第一步我們需要自定義一個簡單實現的類載入器,通過第二大點最後的講解後對自定義類載入器的過程稍有了解,構建重點:自定義的MyClassLoader繼承自java.lang.ClassLoader,就像上面說的,只需實現findClass()方法即可。

注意:此類裡面主要是一些IO和NIO操作,其中defineClass方法可以把二進位制流位元組組成的檔案轉換為一個java.lang.Class,只要二進位制位元組流的內容符合Class檔案規範即可。

/*
 * 自定義類載入器
 */
public class MyClassLoader extends ClassLoader{  

    public MyClassLoader(){   
    }

    public MyClassLoader(ClassLoader parent){
        super(parent);
    }

     @Override  
        public Class<?> findClass(String name) throws ClassNotFoundException {  
            //列印日誌,表示使用的是自定義的類載入器
            System.out.println("Use myclassloader findClass method.");  
            //獲取的fileName為: EasyTest.class
            String fileName = name.substring(name.lastIndexOf(".")+1)+".class";  

            byte[] bytes = loadClassData("E:\\test_eclipse\\JvmProject\\"+fileName); 
            return defineClass(name, bytes, 0, bytes.length);  
        }  


        public byte[] loadClassData(String name) { 
            //這裡引數name路徑為:E:\test_eclipse\JvmProject\EasyTest.class  
            FileInputStream fileInput = null;  
            ByteArrayOutputStream bytesOutput = null;  
            try {  
                fileInput = new FileInputStream(new File(name));  
                bytesOutput = new ByteArrayOutputStream();  
                int b = 0;  
                while ((b = fileInput.read()) != -1) {  
                    bytesOutput.write(b);  
                }  
                return bytesOutput.toByteArray();  
            } catch (Exception e) {  
                e.printStackTrace();  
            } finally {  
                try {  
                    if(fileInput != null)  
                        fileInput.close();  
                } catch (IOException e) {  
                    e.printStackTrace();  
                }  
            }  
            return null;  
        }  
}

 

注意:此時是使用MyClassLoader 自定義載入器來載入 EasyTest類(空實現,只是一個空殼),需要將該類編譯後生成的EasyTest.class檔案放到E:\test_eclipse\JvmProject\ 路徑中,而測試的main 方法如下:

    /*
     * 測試自定義載入器的Main方法
     */
    public static void main(String[] args){  
        MyClassLoader myClassLoader = new MyClassLoader();  
        try {  
            Class<? extends Object> testClass = myClassLoader.loadClass("org.fenixsoft.classloading.EasyTest");  
            Object obj = testClass.newInstance();  
            System.out.println(obj.getClass().getClassLoader().toString());  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
    }  

輸出結果:

這裡寫圖片描述

結果分析

檢視上圖紅框顯示結果可得,雖然程式的確使用自定義類載入器載入的,可是顯示並非是 MyClassLoader而是應用程式類載入器 AppClassLoader 載入的,而這種結果的造成原因正是因為 雙親委派模式。後面通過例項來探究雙親委派模式的原始碼實現。


2. 原始碼探究

回顧一下在講解雙親委派模式中的相關知識點及層次圖,啟動類載入器只是JVM的一個類載入工具,處於層次圖的最上層,嚴格來說它並無遵守此模式,所以我們從它的下層 —— 擴充套件類載入器應用程式類載入器開始分析。

檢視其輸出結果可知,其中涉及到了 sun.misc.Launcher類,檢視此類結構圖:

這裡寫圖片描述

(1)sun.misc.Launcher類構造方法

點進去 sun.misc.Launcher類進行檢視,你會發現在它的構造方法中建立了擴充套件類載入器 ExtClassLoader的例項,並用該例項建立應用程式類載入器 AppClassLoader的例項:

public Launcher() {  
   // 建立擴充套件類載入器  
   ClassLoader extcl;  
   try {  
       extcl = ExtClassLoader.getExtClassLoader();  
   } catch (IOException e) {  
       throw new InternalError("Could not create extension class loader");  
   }  

   // 通過擴充套件類載入器來建立應用程式類載入器  
   try {  
       loader = AppClassLoader.getAppClassLoader(extcl);  
   } catch (IOException e) {  
       throw new InternalError("Could not create application class loader");  
   }
   ...... 
  }

(2)擴充套件類載入器的例項化

由上可知,在Launcher類構造方法中先例項化了擴充套件類載入器,檢視其實現過程:

static class ExtClassLoader extends URLClassLoader {  
    public static ExtClassLoader getExtClassLoader() throws IOException{  
        final File[] dirs = getExtDirs();  
        ......  
        return new ExtClassLoader(dirs);  
        ......  
    }  

    private static File[] getExtDirs() {  
        // 載入路徑  
        String s = System.getProperty("java.ext.dirs");  
        ......  
    }  
    public ExtClassLoader(File[] dirs) throws IOException {  
        // 這裡的第二個引數含義是指定上級類載入器
        super(getExtURLs(dirs), null, factory);  
        this.dirs = dirs;  
    }  
    ......  
}  

注意:檢視ExtClassLoader 的構造方法中,呼叫了父類構造方法,其中傳入的第二個引數為null,代表擴充套件類載入器沒有上級類載入器!

(3)應用程式類載入器的例項化

緊接著來看應用程式類載入器的例項化過程:

static class AppClassLoader extends URLClassLoader {  
    // extcl是ExtClassLoader類的例項  
    public static ClassLoader getAppClassLoader(final ClassLoader extcl)  
            throws IOException{  
        final String s = System.getProperty("java.class.path");  
        final File[] path = (s == null) ? new File[0] : getClassPath(s);  
        ......  
        return new AppClassLoader(urls, extcl);  
        ......  
    }  

    AppClassLoader(URL[] urls, ClassLoader parent) {  
        // 應用程式類載入器的上級是擴充套件類載入器  
        super(urls, parent, factory);  
    }  
    ......  
}  

注意:檢視AppClassLoader 的構造方法中,呼叫了父類構造方法,其中傳入的第二個引數為parent,也就是擴充套件類載入器extcl,直接從程式碼的角度證明 擴充套件類載入器是應用程式載入器的上級!

擴充套件類載入器是應用程式載入器的上級(已證明)

(4)ClassLoader的構造方法

在我們建立自定義類載入器時,繼承了ClassLoader類,所以程式在執行時會先呼叫父類——ClassLoader的建構函式,來檢視其實現:

private ClassLoader parent;  
private static ClassLoader scl;  

protected ClassLoader() {  
    this(checkCreateClassLoader(), getSystemClassLoader());  
}  

private ClassLoader(Void unused, ClassLoader parent) {  
    this.parent = parent;  
}  

public static ClassLoader getSystemClassLoader() {  
    // scl在改方法中建立  
    initSystemClassLoader();  
    .....,  
    return scl;  
}  

private static synchronized void initSystemClassLoader() {  
    if (!sclSet) {  
        if (scl != null)  
            throw new IllegalStateException("recursive invocation");  
        // 獲取Launcher類的例項  
        sun.misc.Launcher l = sun.misc.Launcher.getLauncher();  
        if (l != null) {  
            Throwable oops = null;  
            // 參照前面Launcher類的建構函式,scl就是應用程式類載入器  
            scl = l.getClassLoader();  
            ......  
            sclSet = true;  
        }  
    }  
}  

 

由以上可證明一個級別關係:

應用程式類載入器是自定義類載入器的上級。

我們在之前介紹過雙親委派模式的工作原理,通過前面一系列的分析後,再次敘述一遍,感受更深:如果一個類載入器收到了載入類的請求,它首先不會自己去嘗試載入這個類,而是把這個請求委託給上級類載入器去完成,每一個層次的類載入器都是如此,所有載入器的載入請求最終都應該傳送至最頂層的類載入器中(擴充套件類載入器),只有當上級類載入器反饋自己無法完成這個載入請求(它的類載入範圍中沒有找到所需的類)時,下級類載入器才會去嘗試自己載入這個類。

由以上一系列原始碼探究可窺得這雙親委派模式的工作原理,並且清楚了為何最終載入 EasyTest類的是應用程式類載入器而並非是我們自定義的類載入器


3. 使用自定義類載入器 載入

如今以上的道理算是理解了,可現在偏偏需要使用自定義的類載入器載入,應該如何修改呢?

在測試main() 方法中建立自定義類載入器的程式碼:

MyClassLoader myClassLoader = new MyClassLoader();  

修改成:

MyClassLoader myClassLoader = new MyClassLoader(ClassLoader.getSystemClassLoader().getParent());

即把自定義ClassLoader的父載入器設定為擴充套件類載入器,這樣父載入器載入不到EasyTest.class檔案,就交由子載入器MyClassLoader來載入了(別忘了在自定義類載入器中要寫對應的構造方法)。

執行結果如下:

這裡寫圖片描述


4. .class和getClass()

這兩者看起來類似,但其實有很大區別,如下:

  • .class用於類名,getClass()是一個final native的方法,因此用於類例項。
  • .class在編譯期間就確定了一個類的java.lang.Class物件,但是getClass()方法在執行期間確定一個類例項的java.lang.Class物件

5. ClassLoader.getResourceAsStream(String name)

       不知道細心的朋友有沒有注意到此方法,在本博文的第一個例子中出現過。在我們自定義的類載入器中所佔篇幅最大的就是一個loadClassData 方法,將檔案資料轉換為位元組陣列,但是還有第二種方法,就是採用系統提供的 ClassLoader.getResourceAsStream(String name) 方法,根據此方法獲取到資料輸入流,通過此輸入流獲取位元組陣列,最終傳入defineClass 方法即可,程式碼如下

【自定義類載入器中的 findClass方法】
@Override  
        public Class<?> findClass(String name) throws ClassNotFoundException {  
         try {
             String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
             InputStream is = getClass().getResourceAsStream(fileName);
             if (is == null) {
                 return super.loadClass(name);
             }
             byte[] b = new byte[is.available()];
             is.read(b);
             return defineClass(name, b, 0, b.length);
         } catch (IOException e) {
             throw new ClassNotFoundException(name);
         }
        }  

 

此方法是用來讀入指定的資源的輸入流,並將該輸入流返回給使用者用的,資源可以是影象、聲音、.properties檔案等,資源名稱是以”/”分隔的標識資源名稱的路徑名稱。有興趣可檢視其原始碼,實現也很簡單。


四. 總結

文章總結

本篇博文的內容並不少,首先第一點初步介紹了類載入器的概念、作用,用一個簡單實現的類載入器來證明兩個類之間的“相等”比較。接著在第二點中詳細介紹了三種不同的類載入器,並學習了雙親委派模式,將啟動、擴充套件、應用程式類載入器的層次歸納到雙親委派模型中,理解了此模型的優點、工作原理、原始碼邏輯等。

如果說以上兩點比較偏理論的話,那麼在第三點中採用自定義類載入器例項來探究雙親委派模式的工作原理,從原始碼的角度來分析ClassLoader的實現及類載入情況,此部分尤為重要。