Java ClassLoader詳解 – JAVA編程語言程序開發技術文章

 類加載器是 Java 語言的一個創新,也是 Java 語言流行的重要原因之一。它使得 Java 類可以被動態加載到 Java 虛擬機中並執行。類加載器從 JDK 1.0 就出現瞭,最初是為瞭滿足 Java Applet 的需要而開發出來的。Java Applet 需要從遠程下載 Java 類文件到瀏覽器中並執行。現在類加載器在 Web 容器和 OSGi 中得到瞭廣泛的使用。一般來說,Java 應用的開發人員不需要直接同類加載器進行交互。Java 虛擬機默認的行為就已經足夠滿足大多數情況的需求瞭。不過如果遇到瞭需要與類加載器進行交互的情況,而對類加載器的機制又不是很瞭解的話,就很容易花大量的時間去調試 ClassNotFoundException 和 NoClassDefFoundError 等異常。本文將詳細介紹 Java 的類加載器,幫助讀者深刻理解 Java 語言中的這個重要概念。下面首先介紹一些相關的基本概念。


  類加載器基本概念


  顧名思義,類加載器(class loader)用來加載 Java 類到 Java 虛擬機中。一般來說,Java 虛擬機使用 Java 類的方式如下:Java 源程序(.java 文件)在經過 Java 編譯器編譯之後就被轉換成 Java 字節代碼(.class 文件)。類加載器負責讀取 Java 字節代碼,並轉換成 java.lang.Class 類的一個實例。每個這樣的實例用來表示一個 Java 類。通過此實例的 newInstance()方法就可以創建出該類的一個對象。實際的情況可能更加復雜,比如 Java 字節代碼可能是通過工具動態生成的,也可能是通過網絡下載的。


  基本上所有的類加載器都是 java.lang.ClassLoader 類的一個實例。下面詳細介紹這個 Java 類。


  java.lang.ClassLoader 類介紹


  java.lang.ClassLoader 類的基本職責就是根據一個指定的類的名稱,找到或者生成其對應的字節代碼,然後從這些字節代碼中定義出一個 Java 類,即 java.lang.Class 類的一個實例。除此之外,ClassLoader 還負責加載 Java 應用所需的資源,如圖像文件和配置文件等。不過本文隻討論其加載類的功能。為瞭完成加載類的這個職責,ClassLoader 提供瞭一系列的方法,比較重要的方法如 表 1 所示。關於這些方法的細節會在下面進行介紹。


  表 1. ClassLoader 中與加載類相關的方法


  方法 說明


  getParent() 返回該類加載器的父類加載器。


  loadClass(String name) 加載名稱為 name 的類,返回的結果是 java.lang.Class 類的實例。


  findClass(String name) 查找名稱為 name 的類,返回的結果是 java.lang.Class 類的實例。


  findLoadedClass(String name) 查找名稱為 name 的已經被加載過的類,返回的結果是 java.lang.Class 類的實例。


  defineClass(String name, byte[] b, int off, int len) 把字節數組 b 中的內容轉換成 Java 類,返回的結果是 java.lang.Class 類的實例。這個方法被聲明為 final 的。


  resolveClass(Class<?> c) 鏈接指定的 Java 類。


  對於 表 1 中給出的方法,表示類名稱的 name 參數的值是類的二進制名稱。需要註意的是內部類的表示,如 com.example.Sample$1 和 com.example.Sample$Inner 等表示方式。這些方法會在下面介紹類加載器的工作機制時,做進一步的說明。下面介紹類加載器的樹狀組織結構。


  類加載器的樹狀組織結構


  Java 中的類加載器大致可以分成兩類,一類是系統提供的,另外一類則是由 Java 應用開發人員編寫的。系統提供的類加載器主要有下面三個:


  引導類加載器(bootstrap class loader):它用來加載 Java 的核心庫,是用原生代碼來實現的,並不繼承自 java.lang.ClassLoader。


  擴展類加載器(extensions class loader):它用來加載 Java 的擴展庫。Java 虛擬機的實現會提供一個擴展庫目錄。該類加載器在此目錄裡面查找並加載 Java 類。


  系統類加載器(system class loader):它根據 Java 應用的類路徑(CLASSPATH)來加載 Java 類。一般來說,Java 應用的類都是由它來完成加載的。可以通過 ClassLoader.getSystemClassLoader() 來獲取它。


  除瞭系統提供的類加載器以外,開發人員可以通過繼承 java.lang.ClassLoader 類的方式實現自己的類加載器,以滿足一些特殊的需求。


  除瞭引導類加載器之外,所有的類加載器都有一個父類加載器。通過 表 1 中給出的 getParent() 方法可以得到。對於系統提供的類加載器來說,系統類加載器的父類加載器是擴展類加載器,而擴展類加載器的父類加載器是引導類加載器;對於開發人員編寫的類加載器來說,其父類加載器是加載此類加載器 Java 類的類加載器。因為類加載器 Java 類如同其它的 Java 類一樣,也是要由類加載器來加載的。一般來說,開發人員編寫的類加載器的父類加載器是系統類加載器。類加載器通過這種方式組織起來,形成樹狀結構。樹的根節點就是引導類加載器。圖 1 中給出瞭一個典型的類加載器樹狀組織結構示意圖,其中的箭頭指向的是父類加載器。


  圖 1. 類加載器樹狀組織結構示意圖


  代碼清單 1 演示瞭類加載器的樹狀組織結構。


  清單 1. 演示類加載器的樹狀組織結構


  public class ClassLoaderTree {


  public static void main(String[] args) {


  ClassLoader loader = ClassLoaderTree.class.getClassLoader();


  while (loader != null) {


  System.out.println(loader.toString());


  loader = loader.getParent();


  }


  }


  }


  每個 Java 類都維護著一個指向定義它的類加載器的引用,通過 getClassLoader() 方法就可以獲取到此引用。代碼清單 1 中通過遞歸調用 getParent() 方法來輸出全部的父類加載器。代碼清單 1 的運行結果如 代碼清單 2 所示。


  清單 2. 演示類加載器的樹狀組織結構的運行結果


  sun.misc.Launcher$AppClassLoader@9304b1


  sun.misc.Launcher$ExtClassLoader@190d11


  如 代碼清單 2 所示,第一個輸出的是 ClassLoaderTree 類的類加載器,即系統類加載器。它是 sun.misc.Launcher$AppClassLoader 類的實例;第二個輸出的是擴展類加載器,是 sun.misc.Launcher$ExtClassLoader 類的實例。需要註意的是這裡並沒有輸出引導類加載器,這是由於有些 JDK 的實現對於父類加載器是引導類加載器的情況,getParent() 方法返回 null。


  在瞭解瞭類加載器的樹狀組織結構之後,下面介紹類加載器的代理模式。


  類加載器的代理模式


  類加載器在嘗試自己去查找某個類的字節代碼並定義它時,會先代理給其父類加載器,由父類加載器先去嘗試加載這個類,依次類推。在介紹代理模式背後的動機之前,首先需要說明一下 Java 虛擬機是如何判定兩個 Java 類是相同的。Java 虛擬機不僅要看類的全名是否相同,還要看加載此類的類加載器是否一樣。隻有兩者都相同的情況,才認為兩個類是相同的。即便是同樣的字節代碼,被不同的類加載器加載之後所得到的類,也是不同的。比如一個 Java 類 com.example.Sample,編譯之後生成瞭字節代碼文件 Sample.class。兩個不同的類加載器 ClassLoaderA 和 ClassLoaderB 分別讀取瞭這個 Sample.class 文件,並定義出兩個 java.lang.Class 類的實例來表示這個類。這兩個實例是不相同的。對於 Java 虛擬機來說,它們是不同的類。試圖對這兩個類的對象進行相互賦值,會拋出運行時異常 ClassCastException。下面通過示例來具體說明。代碼清單 3 中給出瞭 Java 類 com.example.Sample。


  清單 3. com.example.Sample 類


  package com.example;


  public class Sample {


  private Sample instance;


  public void setSample(Object instance) {


  this.instance = (Sample) instance;


  }


  }


  如 代碼清單 3 所示,com.example.Sample 類的方法 setSample 接受一個 java.lang.Object 類型的參數,並且會把該參數強制轉換成 com.example.Sample 類型。測試 Java 類是否相同的代碼如 代碼清單 4 所示。


  清單 4. 測試 Java 類是否相同


  public void testClassIdentity() {


  String classDataRootPath = “C:\workspace\Classloader\classData”;


  FileSystemClassLoader fscl1 = new FileSystemClassLoader(classDataRootPath);


  FileSystemClassLoader fscl2 = new FileSystemClassLoader(classDataRootPath);


  String className = “com.example.Sample”;


  try {


  Class<?> class1 = fscl1.loadClass(className);


  Object obj1 = class1.newInstance();


  Class<?> class2 = fscl2.loadClass(className);


  Object obj2 = class2.newInstance();


  Method setSampleMethod = class1.getMethod(“setSample”, java.lang.Object.class);


  setSampleMethod.invoke(obj1, obj2);


  } catch (Exception e) {


  e.printStackTrace();


  }


  }


代碼清單 4 中使用瞭類 FileSystemClassLoader 的兩個不同實例來分別加載類 com.example.Sample,得到瞭兩個不同的 java.lang.Class 的實例,接著通過 newInstance() 方法分別生成瞭兩個類的對象 obj1 和 obj2,最後通過 Java 的反射 API 在對象 obj1 上調用方法 setSample,試圖把對象 obj2 賦值給 obj1 內部的 instance 對象。代碼清

發佈留言