Java虛擬機器類裝載的原理及實現

眼鏡333發表於2018-01-12
一、引言

    Java虛擬機器(JVM)的類裝載就是指將包含在類檔案中的位元組碼裝載到JVM中, 並使其成為JVM一部分的過程。JVM的類動態裝載技術能夠在執行時刻動態地載入或者替換系統的某些功能模組, 而不影響系統其他功能模組的正常執行。本文將分析JVM中的類裝載系統,探討JVM中類裝載的原理、實現以及應用。
二、Java虛擬機器的類裝載實現與應用

2.1 裝載過程簡介       

所謂裝載就是尋找一個類或是一個介面的二進位制形式並用該二進位制形式來構造代表這個類或是這個介面的class物件的過程,其中類或介面的名稱是給定了的。當然名稱也可以通過計算得到,但是更常見的是通過搜尋原始碼經過編譯器編譯後所得到的二進位制形式來構造。

        在Java中,類裝載器把一個類裝入Java虛擬機器中,要經過三個步驟來完成:裝載、連結和初始化,其中連結又可以分成校驗、準備和解析三步,除了解析外,其它步驟是嚴格按照順序完成的,各個步驟的主要工作如下:
裝載:查詢和匯入類或介面的二進位制資料;
  連結:執行下面的校驗、準備和解析步驟,其中解析步驟是可以選擇的;
       校驗:檢查匯入類或介面的二進位制資料的正確性;
       準備:給類的靜態變數分配並初始化儲存空間;
       解析:將符號引用轉成直接引用;
       初始化:啟用類的靜態變數的初始化Java程式碼和靜態Java程式碼塊。
       至於在類裝載和虛擬機器啟動的過程中的具體細節和可能會丟擲的錯誤,請參看《Java虛擬機器規範》以及《深入Java虛擬機器》。 由於本文的討論重點不在此就不再多敘述。

2.2 裝載的實現
         JVM中類的裝載是由ClassLoader和它的子類來實現的,Java ClassLoader 是一個重要的Java執行時系統元件。它負責在執行時查詢和裝入類檔案的類。
        在Java中,ClassLoader是一個抽象類,它在包java.lang中,可以這樣說,只要瞭解了在ClassLoader中的一些重要的方法,再結合上面所介紹的JVM中類裝載的具體的過程,對動態裝載類這項技術就有了一個比較大概的掌握,這些重要的方法包括以下幾個:
        ①loadCass方法    loadClass(String name ,boolean resolve)其中name引數指定了JVM需要的類的名稱,該名稱以包表示法表示,如Java.lang.Object;resolve引數告訴方法是否需要解析類,在初始化類之前,應考慮類解析,並不是所有的類都需要解析,如果JVM只需要知道該類是否存在或找出該類的超類,那麼就不需要解析。這個方法是ClassLoader 的入口點。
        ②defineClass方法    這個方法接受類檔案的位元組陣列並把它轉換成Class物件。位元組陣列可以是從本地檔案系統或網路裝入的資料。它把位元組碼分析成執行時資料結構、校驗有效性等等。
        ③findSystemClass方法    findSystemClass方法從本地檔案系統裝入檔案。它在本地檔案系統中尋找類檔案,如果存在,就使用defineClass將位元組陣列轉換成Class物件,以將該檔案轉換成類。當執行Java應用程式時,這是JVM 正常裝入類的預設機制。
        ④resolveClass方法    resolveClass(Class c)方法解析裝入的類,如果該類已經被解析過那麼將不做處理。當呼叫loadClass方法時,通過它的resolve 引數決定是否要進行解析。
        ⑤findLoadedClass方法    當呼叫loadClass方法裝入類時,呼叫findLoadedClass 方法來檢視ClassLoader是否已裝入這個類,如果已裝入,那麼返回Class物件,否則返回NULL。如果強行裝載已存在的類,將會丟擲連結錯誤。

2.3 裝載的應用
       一般來說,我們使用虛擬機器的類裝載時需要繼承抽象類java.lang.ClassLoader,其中必須實現的方法是loadClass(),對於這個方法需要實現如下操作:
    (1)確認類的名稱;
    (2)檢查請求要裝載的類是否已經被裝載;
    (3)檢查請求載入的類是否是系統類;
    (4)嘗試從類裝載器的儲存區獲取所請求的類;
    (5)在虛擬機器中定義所請求的類;
    (6)解析所請求的類;
    (7) 返回所請求的類。
    所有的Java 虛擬機器都包括一個內建的類裝載器,這個內建的類庫裝載器被稱為根裝載器(bootstrap ClassLoader)。根裝載器的特殊之處是它只能夠裝載在設計時刻已知的類,因此虛擬機器假定由根裝載器所裝載的類都是安全的、可信任的,可以不經過安全認證而直接執行。當應用程式需要載入並不是設計時就知道的類時,必須使用使用者自定義的裝載器(user-defined ClassLoader)。下面我們舉例說明它的應用。

[java] view plaincopy
public abstract class MultiClassLoader extends ClassLoader{ 
     ... 
     public synchronized Class loadClass(String s, boolean flag) throws ClassNotFoundException{ 
    /* 檢查類s是否已經在本地記憶體*/ 
    Class class1 = (Class)classes.get(s); 
 
    /* 類s已經在本地記憶體*/ 
    if(class1 != null) return class1;  
    try/*用預設的ClassLoader 裝入類*/ { 
        class1 = super.findSystemClass(s); 
        return class1; 
    }catch(ClassNotFoundException _ex) { 
        System.out.println(">> Not a system class."); 
    } 
 
    /* 取得類s的位元組陣列*/ 
    byte abyte0[] = loadClassBytes(s); 
    if(abyte0 == null) throw new ClassNotFoundException(); 
    /* 將類位元組陣列轉換為類*/ 
    class1 = defineClass(null, abyte0, 0, abyte0.length); 
    if(class1 == null) throw new ClassFormatError(); 
    if(flag) resolveClass(class1); /*解析類*/ 
    /* 將新載入的類放入本地記憶體*/ 
    classes.put(s, class1); 
    System.out.println(">> Returning newly loaded class."); 
 
    /* 返回已裝載、解析的類*/ 
    return class1; 
    } 
      ... 
    } 


三、Java虛擬機器的類裝載原理

前面我們已經知道,一個Java應用程式使用兩種型別的類裝載器:根裝載器(bootstrap)和使用者定義的裝載器(user-defined)。
根裝載器是Java虛擬機器實現的一部分,舉個例子來說,如果一個Java虛擬機器是在現在已經存在並且正在被使用的作業系統的頂部用C程式來實現的,那麼根裝載器將是那些C程式的一部分。根裝載器以某種預設的方式將類裝入,包括那些Java API的類。
在執行期間一個Java程式能安裝使用者自己定義的類裝載器。根裝載器是虛擬機器固有的一部分,而使用者定義的類裝載器則不是,它是用Java語言寫的,被編譯成class檔案之後然後再被裝入到虛擬機器,並像其它的任何物件一樣可以被例項化。

Java類裝載器的體系結構如下所示:


圖1 Java的類裝載的體系結構

Java的類裝載模型是一種代理(delegation)模型。當JVM 要求類裝載器CL(ClassLoader)裝載一個類時,CL首先將這個類裝載請求轉發給他的父裝載器。只有當父裝載器沒有裝載並無法裝載這個類時,CL才獲得裝載這個類的機會。這樣, 所有類裝載器的代理關係構成了一種樹狀的關係。
樹的根是類的根裝載器(bootstrap ClassLoader) , 在JVM 中它以"null"表示。除根裝載器以外的類裝載器有且僅有一個父裝載器。在建立一個裝載器時, 如果沒有顯式地給出父裝載器, 那麼JVM將預設系統裝載器為其父裝載器。Java的基本類裝載器代理結構如圖2所示:


圖2 Java類裝載的代理結構

下面針對各種類裝載器分別進行詳細的說明。
根(Bootstrap) 裝載器:該裝載器沒有父裝載器,它是JVM實現的一部分,從sun.boot.class.path裝載執行時庫的核心程式碼。
擴充套件(Extension) 裝載器:繼承的父裝載器為根裝載器,不像根裝載器可能與執行時的作業系統有關,這個類裝載器是用純Java程式碼實現的,它從java.ext.dirs (擴充套件目錄)中裝載程式碼。
系統(System or Application) 裝載器:裝載器為擴充套件裝載器,我們都知道在安裝JDK的時候要設定環境變數(CLASSPATH ),這個類裝載器就是從java.class.path(CLASSPATH 環境變數)中裝載程式碼的,它也是用純Java程式碼實現的,同時還是使用者自定義類裝載器的預設父裝載器。
應用程式(Applet) 裝載器:裝載器為系統裝載器,它從使用者指定的網路上的特定目錄裝載小應用程式程式碼。

在設計一個類裝載器的時候,應該滿足以下兩個條件:
對於相同的類名,類裝載器所返回的物件應該是同一個類物件
如果類裝載器CL1將裝載類C的請求轉給類裝載器CL2,那麼對於以下的類或介面,CL1和CL2應該返回同一個類物件:a)S為C的直接超類;b)S為C的直接超介面;c)S為C的成員變數的型別;d)S為C的成員方法或構建器的引數型別;e)S為C的成員方法的返回型別。


每個已經裝載到JVM中的類都隱式含有裝載它的類裝載器的資訊。

類方法getClassLoader 可以得到裝載這個類的類裝載器。一個類裝載器認識的類包括它的父裝載器認識的類和它自己裝載的類,可見類裝載器認識的類是它自己裝載的類的超集。注意我們可以得到類裝載器的有關的資訊,但是已經裝載到JVM中的類是不能更改它的類裝載器的。

Java中的類的裝載過程也就是代理裝載的過程。比如:Web瀏覽器中的JVM需要裝載一個小應用程式TestApplet。JVM呼叫小應用程式裝載器ACL(Applet ClassLoader)來完成裝載。

ACL首先請求它的父裝載器, 即系統裝載器裝載TestApplet是否裝載了這個類, 由於TestApplet不在系統裝載器的裝載路徑中, 所以系統裝載器沒有找到這個類, 也就沒有裝載成功。

接著ACL自己裝載TestApplet。ACL通過網路成功地找到了TestApplet.class 檔案並將它匯入到了JVM中。在裝載過程中, JVM發現TestAppet是從超類java.applet.Applet繼承的。

所以JVM再次呼叫ACL來裝載java.applet.Applet類。ACL又再次按上面的順序裝載Applet類,結果ACL發現他的父裝載器已經裝載了這個類, 所以ACL就直接將這個已經裝載的類返回給了JVM , 完成了Applet類的裝載。接下來,Applet類的超類也一樣處理。最後, TestApplet及所有有關的類都裝載到了JVM中。

四、結論
類的動態裝載機制是JVM的一項核心技術,也是容易被忽視而引起很多誤解的地方。本文介紹了JVM中類裝載的原理、實現以及應用,尤其分析了ClassLoader的結構、用途以及如何利用自定義的ClassLoader裝載並執行Java類,希望能使讀者對JVM中的類裝載有一個比較深入的理解。

相關文章