Java類載入機制(全套)

Jemon發表於2018-02-12

概述

在開始正文之前,我們先看兩張圖 。Java平臺的理解?Java最顯著的特性?Java是解釋執行? 先看一下java程式的執行流程圖

Java類載入機制(全套)
再看一下jvm的大致物理結構圖

Java類載入機制(全套)

本文是我在學習jvm類載入機制的時候對網上的一些資料的整理和總結。本文將研究一下問題:

什麼是類載入?類的載入過程(生命週期)?類什麼時候初始化?類初始化順序?類載入器、反射、位元組碼等一系列問題。

一、類載入機制概念

  • Java虛擬機器把描述類的資料從Class檔案載入到記憶體,並對資料進行校驗、轉換解析和初始化,最終形成可以被虛擬機器直接使用的Java型別,這就是虛擬機器的載入機制。* Class檔案由類裝載器裝載後,在JVM中將形成一份描述Class結構的元資訊物件,通過該元資訊物件可以獲知Class的結構資訊:如建構函式,屬性和方法等,Java允許使用者藉由這個Class相關的元資訊物件間接呼叫Class物件的功能,這裡就是我們經常能見到的Class類。

二、類載入過程

Java類載入機制(全套)

工作機制

類裝載器就是尋找類的位元組碼檔案,並構造出類在JVM內部表示的物件元件。在Java中,類裝載器把一個類裝入JVM中,要經過以下步驟:

  (1) 裝載:查詢和匯入Class檔案;

  (2) 連結:把類的二進位制資料合併到JRE中;

     (a)校驗:檢查載入Class檔案資料的正確性;

     (b)準備:給類的靜態變數分配儲存空間;

     (c)解析:將符號引用轉成直接引用;

  (3) 初始化:對類的靜態變數,靜態程式碼塊執行初始化操作
複製程式碼

Java程式可以動態擴充套件是由執行期動態載入和動態連結實現的;比如:如果編寫一個使用介面的應用程式,可以等到執行時再指定其實際的實現(多型),解析過程有時候還可以在初始化之後執行;比如:動態繫結(多型) 如上圖所示,載入、驗證、準備、初始化和解除安裝這五個階段的順序是確定的,類的載入過程必須按照這個順序來按部就班地開始,而解析階段則不一定,它在某些情況下可以在初始化階段後再開始。 類的生命週期的每一個階段通常都是互相交叉混合式進行的,通常會在一個階段執行的過程中呼叫或啟用另外一個階段。

在我參考別人的資料的時候,發現都是先介紹的類的初始化這個過程,但是我覺得這樣會造成一種誤解什麼事類的初始化,和它應該發生的時機。這裡我就按照類的載入生命週期順序介紹每一個過程了。

1. 裝載(載入)

類的裝載指的是將類的.class檔案中的二進位制資料讀入到記憶體中,將其放在執行時資料區的方法區內,然後在堆區建立一個java.lang.Class物件,用來封裝類在方法區內的資料結構。類的載入的最終產品是位於堆區中的Class物件,Class物件封裝了類在方法區內的資料結構,並且向Java程式設計師提供了訪問方法區內的資料結構的介面。

類載入器並不需要等到某個類被“首次主動使用”時再載入它,JVM規範允許類載入器在預料某個類將要被使用時就預先載入它,如果在預先載入的過程中遇到了.class檔案缺失或存在錯誤,類載入器必須在程式首次主動使用該類時才報告錯誤(LinkageError錯誤)如果這個類一直沒有被程式主動使用,那麼類載入器就不會報告錯誤。

載入.class檔案的方式有:

1). 從本地系統中直接載入 2). 通過網路下載.class檔案 3). 從zip,jar等歸檔檔案中載入.class檔案 4). 從專有資料庫中提取.class檔案 5). 將Java原始檔動態編譯為.class檔案

在瞭解了什麼是類的載入後,回頭來再看jvm進行類載入階段都做了什麼。虛擬機器需要完成以下三件事情:

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

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

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

相對於類載入過程的其他階段,載入階段是開發期相對來說可控性比較強,該階段既可以使用系統提供的類載入器完成,也可以由使用者自定義的類載入器來完成,開發人員可以通過定義自己的類載入器去控制位元組流的獲取方式。關於這個過程的更多細節,我會在下一節細說,類的載入。 載入階段完成後,虛擬機器外部的 二進位制位元組流就按照虛擬機器所需的格式儲存在方法區之中,而且在Java堆中也建立一個java.lang.Class類的物件,這樣便可以通過該物件訪問方法區中的這些資料。

2. 驗證

驗證的目的是為了確保Class檔案中的位元組流包含的資訊符合當前虛擬機器的要求,而且不會危害虛擬機器自身的安全。不同的虛擬機器對類驗證的實現可能會有所不同,但大致都會完成以下四個階段的驗證:檔案格式的驗證、後設資料的驗證、位元組碼驗證和符號引用驗證。

1)檔案格式的驗證:驗證位元組流是否符合Class檔案格式的規範,並且能被當前版本的虛擬機器處理,該驗證的主要目的是保證輸入的位元組流能正確地解析並儲存於方法區之內。經過該階段的驗證後,位元組流才會進入記憶體的方法區中進行儲存,後面的三個驗證都是基於方法區的儲存結構進行的。

2)後設資料驗證:對類的後設資料資訊進行語義校驗(其實就是對類中的各資料型別進行語法校驗),保證不存在不符合Java語法規範的後設資料資訊。

3)位元組碼驗證:該階段驗證的主要工作是進行資料流和控制流分析,對類的方法體進行校驗分析,以保證被校驗的類的方法在執行時不會做出危害虛擬機器安全的行為。

4)符號引用驗證:這是最後一個階段的驗證,它發生在虛擬機器將符號引用轉化為直接引用的時候(解析階段中發生該轉化,後面會有講解),主要是對類自身以外的資訊(常量池中的各種符號引用)進行匹配性的校驗。

3. 準備

準備階段是正式為類變數分配記憶體並設定類變數初始值的階段,這些記憶體都將在方法區中進行分配。 注:

1)這時候進行記憶體分配的僅包括類變數(static),而不包括例項變數,例項變數會在物件例項化時隨著物件一塊分配在Java堆中。

2)這裡所設定的初始值通常情況下是資料型別預設的零值(如0、0L、null、false等),而不是被在Java程式碼中被顯式地賦予的值。

4. 解析

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

符號引用(Symbolic Reference):符號引用以一組符號來描述所引用的目標,符號引用可以是任何形式的字面量,符號引用與虛擬機器實現的記憶體佈局無關,引用的目標並不一定已經在記憶體中。

直接引用(Direct Reference):直接引用可以是直接指向目標的指標、相對偏移量或是一個能間接定位到目標的控制程式碼。直接引用是與虛擬機器實現的記憶體佈局相關的,同一個符號引用在不同的虛擬機器例項上翻譯出來的直接引用一般都不相同,如果有了直接引用,那引用的目標必定已經在記憶體中存在。

1)、類或介面的解析:判斷所要轉化成的直接引用是對陣列型別,還是普通的物件型別的引用,從而進行不同的解析。 2)、欄位解析:對欄位進行解析時,會先在本類中查詢是否包含有簡單名稱和欄位描述符都與目標相匹配的欄位,如果有,則查詢結束;如果沒有,則會按照繼承關係從上往下遞迴搜尋該類所實現的各個介面和它們的父介面,還沒有,則按照繼承關係從上往下遞迴搜尋其父類,直至查詢結束。

3)、類方法解析:對類方法的解析與對欄位解析的搜尋步驟差不多,只是多了判斷該方法所處的是類還是介面的步驟,而且對類方法的匹配搜尋,是先搜尋父類,再搜尋介面。

4)、介面方法解析:與類方法解析步驟類似,只是介面不會有父類,因此,只遞迴向上搜尋父介面就行了。

5. 初始化

類初始化階段是類載入過程的最後一步,前面的類載入過程中,除了載入(Loading)階段使用者應用程式可以通過自定義類載入器參與之外,其餘動作完全由虛擬機器主導和控制。到了初始化階段,才真正開始執行類中定義的Java程式程式碼。 初始化,為類的靜態變數賦予正確的初始值,JVM負責對類進行初始化,主要對類變數進行初始化。在Java中對類變數進行初始值設定有兩種方式:

①宣告類變數時指定初始值

②使用靜態程式碼塊為類變數指定初始值

JVM初始化步驟

1)、假如這個類還沒有被載入和連線,則程式先載入並連線該類

2)、假如該類的直接父類還沒有被初始化,則先初始化其直接父類

3)、假如類中有初始化語句,則系統依次執行這些初始化語句

初始化階段時執行類構造器方法()的過程。

1)類構造器方法是由編譯器自動收集類中的所有類變數的賦值動作和靜態語句塊(static{}塊)中的語句合併產生的,編譯器收集的順序由語句在原始檔中出現的順序所決定。

2)類構造器方法與類的建構函式不同,它不需要顯式地呼叫父類構造器,虛擬機器會保證在子類的類構造器方法執行之前,父類的類構造器方法已經執行完畢,因此在虛擬機器中第一個執行的類構造器方法的類一定是java.lang.Object。

3)由於父類的類構造器方法方法先執行,也就意味著父類中定義的靜態語句塊要優先於子類的變數賦值操作。

4)類構造器方法對於類或者介面來說並不是必需的,如果一個類中沒有靜態語句塊也沒有對變數的賦值操作,那麼編譯器可以不為這個類生成類構造器方法。

5)介面中可能會有變數賦值操作,因此介面也會生成類構造器方法。但是介面與類不同,執行介面的類構造器方法不需要先執行父介面的類構造器方法。只有當父介面中定義的變數被使用時,父介面才會被初始化。另外,介面的實現類在初始化時也不會執行介面的類構造器方法。

6)虛擬機器會保證一個類的類構造器方法在多執行緒環境中被正確地加鎖和同步。如果有多個執行緒去同時初始化一個類,那麼只會有一個執行緒去執行這個類的類構造器方法,其它執行緒都需要阻塞等待,直到活動執行緒執行類構造器方法完畢。如果在一個類的類構造器方法中有耗時很長的操作,那麼就可能造成多個程式阻塞。

6.結束生命週期

在以下情況的時候,Java虛擬機器會結束生命週期 1). 執行了System.exit()方法 2). 程式正常執行結束 3). 程式在執行過程中遇到了異常或錯誤而異常終止 4). 由於作業系統出現錯誤而導致Java虛擬機器程式終止

三、 何時開始類的初始化

什麼情況下需要開始類載入過程的第一個階段:"載入"。虛擬機器規範中並沒強行約束,這點可以交給虛擬機器的的具體實現自由把握,但是對於初始化階段虛擬機器規範是嚴格規定了如下幾種情況,如果類未初始化會對類進行初始化。

1、建立類的例項

2、訪問類的靜態變數(除常量【被final修辭的靜態變數】原因:常量一種特殊的變數,因為編譯器把他們當作值(value)而不是域(field)來對待。如果你的程式碼中用到了常變數(constant variable),編譯器並不會生成位元組碼來從物件中載入域的值,而是直接把這個值插入到位元組碼中。這是一種很有用的優化,但是如果你需要改變final域的值那麼每一塊用到那個域的程式碼都需要重新編譯。

3、訪問類的靜態方法

4、反射如(Class.forName("my.xyz.Test"))

5、當初始化一個類時,發現其父類還未初始化,則先出發父類的初始化

6、虛擬機器啟動時,定義了main()方法的那個類先初始化

以上情況稱為稱對一個類進行“主動引用”,除此種情況之外,均不會觸發類的初始化,稱為“被動引用” 介面的載入過程與類的載入過程稍有不同。介面中不能使用static{}塊。當一個介面在初始化時,並不要求其父介面全部都完成了初始化,只有真正在使用到父介面時(例如引用介面中定義的常量)才會初始化。

被動引用例子

1、子類呼叫父類的靜態變數,子類不會被初始化。只有父類被初始化。。對於靜態欄位,只有直接定義這個欄位的類才會被初始化.

2、通過陣列定義來引用類,不會觸發類的初始化

3、 訪問類的常量,不會初始化類

class SuperClass {  
    static {  
        System.out.println("superclass init");  
    }  
    public static int value = 123;  
}  
  
class SubClass extends SuperClass {  
    static {  
        System.out.println("subclass init");  
    }  
}  
  
public class Test {  
    public static void main(String[] args) {  
        System.out.println(SubClass.value);// 被動應用1  
        SubClass[] sca = new SubClass[10];// 被動引用2  
    }  
}  
複製程式碼

程式執行輸出 superclass init 123 從上面的輸入結果證明了被動引用1與被動引用2

class ConstClass {  
    static {  
        System.out.println("ConstClass init");  
    }  
    public static final String HELLOWORLD = "hello world";  
}  
  
public class Test {  
    public static void main(String[] args) {  
        System.out.println(ConstClass.HELLOWORLD);// 呼叫類常量  
    }  
}  
複製程式碼

程式輸出結果 hello world 從上面的輸出結果證明了被動引用3

** 題目分析**

上面很詳細的介紹了類的載入時機和類的載入過程,通過上面的理論來分析本文開門見上的題目

class SingleTon {  
    private static SingleTon singleTon = new SingleTon();  
    public static int count1;  
    public static int count2 = 0;  
  
    private SingleTon() {  
        count1++;  
        count2++;  
    }  
  
    public static SingleTon getInstance() {  
        return singleTon;  
    }  
}  
  
public class Test {  
    public static void main(String[] args) {  
        SingleTon singleTon = SingleTon.getInstance();  
        System.out.println("count1=" + singleTon.count1);  
        System.out.println("count2=" + singleTon.count2);  
    }  
}  
複製程式碼

分析:

1:SingleTon singleTon = SingleTon.getInstance();呼叫了類的SingleTon呼叫了類的靜態方法,觸發類的初始化

2:類載入的時候在準備過程中為類的靜態變數分配記憶體並初始化預設值 singleton=null count1=0,count2=0

3:類初始化化,為類的靜態變數賦值和執行靜態程式碼快。singleton賦值為new SingleTon()呼叫類的構造方法

4:呼叫類的構造方法後count=1;count2=1

5:繼續為count1與count2賦值,此時count1沒有賦值操作,所有count1為1,但是count2執行賦值操作就變為0

四、類初始化順序

現在我們知道什麼時候觸發類的初始化了,他精確地寫在Java語言規範中。但瞭解清楚 域(fields,靜態的還是非靜態的)、塊(block靜態的還是非靜態的)、不同類(子類和超類)和不同的介面(子介面,實現類和超介面)的初始化順序也很重要類。事實上很多核心Java面試題和SCJP問題都是基於這些概念,下面是類初始化的一些規則:

1.類從頂至底的順序初始化,所以宣告在頂部的欄位的早於底部的欄位初始化
2.超類早於子類和衍生類的初始化
3.如果類的初始化是由於訪問靜態域而觸發,那麼只有宣告靜態域的類才被初始化,而不會觸發超類的初始化或者子類的4.初始化即使靜態域被子類或子介面或者它的實現類所引用。
5.介面初始化不會導致父介面的初始化。
6.靜態域的初始化是在類的靜態初始化期間,非靜態域的初始化時在類的例項建立期間。這意味這靜態域初始化在非靜態域之前。
7.非靜態域通過構造器初始化,子類在做任何初始化之前構造器會隱含地呼叫父類的構造器,他保證了非靜態或例項變數(父類)初始化早於子類
複製程式碼

原文連結:

類載入舉例:http://blog.csdn.net/mrzhoug/article/details/51581994

五、類載入器

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類中,我們可以檢視類載入實現過程的程式碼,具體原始碼如下:

protected synchronized Class loadClass(String name, boolean resolve)  
        throws ClassNotFoundException {  
    // 首先檢查該name指定的class是否有被載入  
    Class c = findLoadedClass(name);  
    if (c == null) {  
        try {  
            if (parent != null) {  
                // 如果parent不為null,則呼叫parent的loadClass進行載入  
                c = parent.loadClass(name, false);  
            } else {  
                // parent為null,則呼叫BootstrapClassLoader進行載入  
                c = findBootstrapClass0(name);  
            }  
        } catch (ClassNotFoundException e) {  
            // 如果仍然無法載入成功,則呼叫自身的findClass進行載入  
            c = findClass(name);  
        }  
    }  
    if (resolve) {  
        resolveClass(c);  
    }  
    return c;  
}  
複製程式碼

通過上面程式碼可以看出,雙親委派模型是通過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關鍵字的結果)。例子程式碼如下:

/** 
     * 一、ClassLoader載入類的順序 
     *  1.呼叫 findLoadedClass(String) 來檢查是否已經載入類。 
     *  2.在父類載入器上呼叫 loadClass 方法。如果父類載入器為 null,則使用虛擬機器的內建類載入器。 
     *  3.呼叫 findClass(String) 方法查詢類。 
     * 二、實現自己的類載入器 
     *  1.獲取類的class檔案的位元組陣列 
     *  2.將位元組陣列轉換為Class類的例項 
     * @author lei 2011-9-1 
     */  
    public class ClassLoaderTest {  
        public static void main(String[] args) throws InstantiationException, IllegalAccessException, ClassNotFoundException {  
            //新建一個類載入器  
            MyClassLoader cl = new MyClassLoader("myClassLoader");  
            //載入類,得到Class物件  
            Class<?> clazz = cl.loadClass("classloader.Animal");  
            //得到類的例項  
            Animal animal=(Animal) clazz.newInstance();  
            animal.say();  
        }  
    }  
    class Animal{  
        public void say(){  
            System.out.println("hello world!");  
        }  
    }  
    class MyClassLoader extends ClassLoader {  
        //類載入器的名稱  
        private String name;  
        //類存放的路徑  
        private String path = "E:\\workspace\\Algorithm\\src";  
        MyClassLoader(String name) {  
            this.name = name;  
        }  
        MyClassLoader(ClassLoader parent, String name) {  
            super(parent);  
            this.name = name;  
        }  
        /** 
         * 重寫findClass方法 
         */  
        @Override  
        public Class<?> findClass(String name) {  
            byte[] data = loadClassData(name);  
            return this.defineClass(name, data, 0, data.length);  
        }  
        public byte[] loadClassData(String name) {  
            try {  
                name = name.replace(".", "//");  
                FileInputStream is = new FileInputStream(new File(path + name + ".class"));  
                ByteArrayOutputStream baos = new ByteArrayOutputStream();  
                int b = 0;  
                while ((b = is.read()) != -1) {  
                    baos.write(b);  
                }  
                return baos.toByteArray();  
            } catch (Exception e) {  
                e.printStackTrace();  
            }  
            return null;  
        }  
    }  
複製程式碼

類載入器雙親委派模型是從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 比較簡單,如下:

URL[] urls = new URL[] {new URL("file:libs/jar1.jar")};  
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

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類根本沒有機會得到載入。

但是,我們可以自己定義一個類載入器來達到這個目的,為了避免雙親委託機制,這個類載入器也必須是特殊的。由於系統自帶的三個類載入器都載入特定目錄下的類,如果我們自己的類載入器放在一個特殊的目錄,那麼系統的載入器就無法載入,也就是最終還是由我們自己的載入器載入。

六、反射

原文連結:https://www.cnblogs.com/lakeslove/p/5978382.html(18.3章節)

七、位元組碼

Java號稱是一門“一次編譯到處執行”的語言,但是我們對這句話的理解深度又有多少呢?從我們寫的java檔案到通過編譯器編譯成java位元組碼檔案(也就是.class檔案),這個過程是java編譯過程;而我們的java虛擬機器執行的就是位元組碼檔案。不論該位元組碼檔案來自何方,由哪種編譯器編譯,甚至是手寫位元組碼檔案,只要符合java虛擬機器的規範,那麼它就能夠執行該位元組碼檔案。

原文連結:http://www.importnew.com/24088.html

相關文章