ClassLoader 是 Java 虛擬機 (JVM) 的類裝載子系統,它負責將 Java 字節碼裝載到 JVM 中, 并使其成為 JVM 一部分。 JVM 的類動態裝載技術能夠在運行時刻動態地加載或者替換系統的某些功能模塊 , 而不影響系統其他功能模塊的正常運行。本文將分析 JVM 中的類裝載系統,探討 JVM 中類裝載的原理、實現以及應用。 ?????????? 裝載過程簡介類裝載就是尋找一個類或是一個接口的字節碼文件并通過解析該字節碼來構造代表這個類或是這個接口的 class 對象的過程。在 Java 中,類裝載器把一個類裝入 Java 虛擬機中,要經過三個步驟來完成:裝載、鏈接和初始化,其中鏈接又可以分成校驗、準備和解析三步,除了解析外,其它步驟是嚴格按照順序完成的,各個步驟的主要工作如下: 1.??????? 裝載:查找和導入類或接口的字節碼; 2.??????? 鏈接:執行下面的校驗、準備和解析步驟,其中解析步驟是可以選擇的; l??????? 校驗:檢查導入類或接口的二進制數據的正確性; l??????? 準備:給類的靜態變量分配并初始化存儲空間; l??????? 解析:將符號引用轉成直接引用; 3.??????? 初始化:激活類的靜態變量的初始化 Java 代碼和靜態 Java 代碼塊。 至于在類裝載和虛擬機啟動的過程中的具體細節和可能會拋出的錯誤,請參看《 Java 虛擬機規范》以及《深入 Java 虛擬機》。 由于本文的討論重點不在此就不再多敘述。 ?????????? 裝載的實現JVM 中類的裝載是由 ClassLoader 和它的子類來實現的。 Java ClassLoader 是一個重要的 Java 運行時系統組件,它負責在運行時查找和裝入 Java 字節碼。 在 Java 中, ClassLoader 是一個抽象類,它在包 java.lang 中。可以這樣說,只要了解了 ClassLoader 中的一些重要的方法,再結合上面所介紹的 JVM 中類裝載的具體的過程,對動態裝載類這項技術就有了一個比較大概的掌握,這些重要的方法包括以下幾個: ? |
?
1.??????? loadCass 方法: loadClass(String name ,boolean resolve) 其中 name 參數指定了 JVM 需要的類的名稱 , 該名稱以類的全限定名表示,如 Java.lang.Object ; resolve 參數告訴方法是否需要解析類,在初始化類之前,應考慮類解析,并不是所有的類都需要解析,如果 JVM 只需要知道該類是否存在或找出該類的超類,那么就不需要解析。這個方法是 ClassLoader 的入口點。
2.??????? defineClass 方法 ? 這個方法接受類文件的字節數組并把它轉換成 Class 對象。字節數組可以是從本地文件系統或網絡裝入的數據。它把字節碼分析成運行時數據結構、校驗有效性等等。
3.??????? findSystemClass 方法 ? findSystemClass 方法從本地文件系統裝入 Java 字節碼。它在本地文件系統中尋找類文件,如果存在,就使用 defineClass 將字節數組轉換成 Class 對象。當運行 Java 應用程序時 , 這是 JVM 正常裝入類的缺省機制。
4.??????? resolveClass 方法 resolveClass(Class c) 方法解析裝入的類,如果該類已經被解析過那么將不做處理。當調用 loadClass 方法時 , 通過它的 resolve 參數決定是否要進行解析。
5.??????? findLoadedClass 方法 ? 當調用 loadClass 方法裝入類時 , 調用 findLoadedClass 方法來查看 ClassLoader 是否已裝入這個類 , 如果已裝入 , 那么返回 Class 對象 , 否則返回 NULL 。如果強行裝載已存在的類 , 將會拋出鏈接錯誤。
?????????? 裝載的應用
一般來說,我們實現自定義的 ClassLoader 需要繼承抽象類 java.lang.ClassLoader ,其中必須實現的方法是 loadClass(String name) ,對于這個方法需要實現如下操作:
(1) 確認類的名稱;
(2) 檢查請求要裝載的類是否已經被裝載;
(3) 檢查請求加載的類是否是系統類;
(4) 嘗試從類裝載器的存儲區獲取所請求的類;
(5) 在虛擬機中定義所請求的類;
(6) 解析所請求的類;
(7)
返回所請求的類。
所有的 Java 虛擬機都包括一個內置的類裝載器,這個內置的類庫裝載器被稱為根裝載器 (bootstrap ClassLoader) 。根裝載器的特殊之處是它只能夠裝載基本的 Java 類,如 rt.jar 中的 class 。當應用程序可以使用用戶自定義的 ClassLoader 來加載特定 ClassPath 下的類。下面的例子是 JDK5.0 的 URLClassLoader 的實現。
public class URLClassLoader extends SecureClassLoader {
……
protected Class<?> findClass(final String name)
??????
?throws ClassNotFoundException
??? {
?????? try {
??????
??? return (Class)
????????????? AccessController.doPrivileged(new PrivilegedExceptionAction() {
?????????????
??? public Object run() throws ClassNotFoundException {
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?//
由類的全限定名得到物理路徑
???????????????????? String path = name.replace('.', '/').concat(".class");
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?//
從
URLs ClassPath
中取得相應的字節碼
???????????????????? Resource res = ucp.getResource(path, false);
???????????????????? if (res != null) {
????????????????????
??? try {
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?//
由
Java
字節碼創建一個
Class
對象
??????????????????????????? return defineClass(name, res);
????????????????????
??? } catch (IOException e) {
??????????????????????????? throw new ClassNotFoundException(name, e);
????????????????????
??? }
???????????????????? } else {
????????????????????
??? throw new ClassNotFoundException(name);
???????????????????? }
?????????????
??? }
????????????? }, acc);
?????? } catch (java.security.PrivilegedActionException pae) {
??????
??? throw (ClassNotFoundException) pae.getException();
?????? }
??? }
……
} |
????????? Java 虛擬機的類裝載原理
前面我們已經知道,一個 Java 應用程序使用兩種類型的類裝載器:根裝載器 (bootstrap) 和用戶定義的裝載器 (user-defined) 。根裝載器是 Java 虛擬機實現的一部分。根裝載器以某種默認的方式將類裝入,包括 Java API 的類。在運行期間,一個 Java 程序能使用用戶自己定義的類裝載器。根裝載器是虛擬機固有的一部分,而用戶定義的類裝載器則不是,它是用 Java 語言寫的,被編譯成 class 文件之后然后再被裝入到虛擬機,并像其它的任何對象一樣可以被實例化。 Java 類裝載器的體系結構如下所示:
Java 的類裝載的體系結構
Java 的類裝載模型是一種代理 (delegation) 模型。當 JVM 要求類裝載器 CL(ClassLoader) 裝載一個類時 ,CL 首先將這個類裝載請求轉發給他的父裝載器。只有當父裝載器沒有裝載并無法裝載這個類時 ,CL 才獲得裝載這個類的機會。這樣 , 所有類裝載器的代理關系構成了一種樹狀的關系。樹的根是類的根裝載器 (bootstrap ClassLoader) , 在 JVM 中它以 "null" 表示。除根裝載器以外的類裝載器有且僅有一個父裝載器。在創建一個裝載器時 , 如果沒有顯式地給出父裝載器 , 那么 JVM 將默認系統裝載器為其父裝載器。 Java 的基本類裝載器代理結構如圖 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
中的類對象都含有裝載它的類裝載器的信息。
Class
類的方法
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中的類裝載有一個比較深入的理解。