java中雙親委派機制(+總結)


類加載器

加載類的開放性

類加載器(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關鍵字運算的結果的影響。

package com.jvm.classloading;

import java.io.IOException;
import java.io.InputStream;

/** * 類加載器在類相等判斷中的影響 * * instanceof關鍵字 * */

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(fileName);
                    }
                    byte[] b = new byte[is.available()];
                    is.read(b);
                    return defineClass(name, b, 0, b.length);   
                } catch (IOException e) {
                    throw new ClassNotFoundException();
                }
            }
        };

        // 使用ClassLoaderTest的類加載器加載本類
        Object obj1 = ClassLoaderTest.class.getClassLoader().loadClass("com.jvm.classloading.ClassLoaderTest").newInstance();
        System.out.println(obj1.getClass());
        System.out.println(obj1 instanceof com.jvm.classloading.ClassLoaderTest);

        // 使用自定義類加載器加載本類
        Object obj2 = myLoader.loadClass("com.jvm.classloading.ClassLoaderTest").newInstance();
        System.out.println(obj2.getClass());
        System.out.println(obj2 instanceof com.jvm.classloading.ClassLoaderTest);
    }
}

輸出結果:

class com.jvm.classloading.ClassLoaderTest
true
class com.jvm.classloading.ClassLoaderTest
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()方法進行加載。

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;
}

注意,雙親委派模型是Java設計者推薦給開發者的類加載器的實現方式,並不是強制規定的。大多數的類加載器都遵循這個模型,但是JDK中也有較大規模破壞雙親模型的情況,例如線程上下文類加載器(Thread Context ClassLoader)的出現,具體分析可以參見周志明著《深入理解Java虛擬機》。

自定義類加載器

若要實現自定義類加載器,只需要繼承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關鍵字的結果)。

類加載器雙親委派模型是從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也沒有完全遵循雙親委派模型。

  1. 周志明,深入理解Java虛擬機:JVM高級特性與最佳實踐,機械工業出版社
  2. Alexia(minmin)博客,http://www.cnblogs.com/lanxuezaipiao/p/4138511.html
  3. 【深入理解JVM】:類加載器與雙親委派模型
  4. Java類加載機制(全套)


免責聲明!

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



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