java se基礎-類載入器
十二.類載入器
1.類的載入過程
JVM將類載入過程分為三個步驟:裝載(Load),連結(Link)和初始化(Initialize)連結又分為三個步驟,如下圖所示:
1) 裝載:查詢並載入類的二進位制資料;
2)連結:
驗證:確保被載入類的正確性;
準備:為類的靜態變數分配記憶體,並將其初始化為預設值;
解析:把類中的符號引用轉換為直接引用;
3)初始化:為類的靜態變數賦予正確的初始值;
那為什麼我要有驗證這一步驟呢?首先如果由編譯器生成的class檔案,它肯定是符合JVM位元組碼格式的,但是萬一有高手自己寫一個class檔案,讓JVM載入並執行,用於惡意用途,就不妙了,因此這個class檔案要先過驗證這一關,不符合的話不會讓它繼續執行的,也是為了安全考慮吧。
準備階段和初始化階段看似有點牟盾,其實是不牟盾的,如果類中有語句:private staticint a = 10,它的執行過程是這樣的,首先位元組碼檔案被載入到記憶體後,先進行連結的驗證這一步驟,驗證通過後準備階段,給a分配記憶體,因為變數a是static的,所以此時a等於int型別的預設初始值0,即a=0,然後到解析(後面在說),到初始化這一步驟時,才把a的真正的值10賦給a,此時a=10。
2. 類的初始化
類什麼時候才被初始化:
1)建立類的例項,也就是new一個物件
2)訪問某個類或介面的靜態變數,或者對該靜態變數賦值
3)呼叫類的靜態方法
4)反射(Class.forName("com.lyj.load")
5)初始化一個類的子類(會首先初始化子類的父類)
6)JVM啟動時標明的啟動類,即檔名和類名相同的那個類
只有這6中情況才會導致類的類的初始化。
類的初始化步驟:
1)如果這個類還沒有被載入和連結,那先進行載入和連結
2)假如這個類存在直接父類,並且這個類還沒有被初始化(注意:在一個類載入器中,類只能初始化一次),那就初始化直接的父類(不適用於介面)
3)加入類中存在初始化語句(如static變數和static塊),那就依次執行這些初始化語句。
3.類的載入
類的載入指的是將類的.class檔案中的二進位制資料讀入到記憶體中,將其放在執行時資料區的方法區內,然後在堆區建立一個這個類的
類的載入的最終產品是位於堆區中的Class物件
Class物件封裝了類在方法區內的資料結構,並且向Java程式設計師提供了訪問方法區內的資料結構的介面
載入類的方式有以下幾種:
1)從本地系統直接載入
2)通過網路下載.class檔案
3)從zip,jar等歸檔檔案中載入.class檔案
4)從專有資料庫中提取.class檔案
5)將Java原始檔動態編譯為.class檔案(伺服器)
4.載入器
JVM的類載入是通過ClassLoader及其子類來完成的,類的層次關係和載入順序可以由下圖來描述:
1)BootstrapClassLoader
負責載入$JAVA_HOME中jre/lib/rt.jar裡所有的class,由C++實現,不是ClassLoader子類
2)ExtensionClassLoader
負責載入java平臺中擴充套件功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目錄下的jar包
3)AppClassLoader
負責記載classpath中指定的jar包及目錄中class
4)CustomClassLoader
屬於應用程式根據自身需要自定義的ClassLoader,如tomcat、jboss都會根據j2ee規範自行實現ClassLoader
載入過程中會先檢查類是否被已載入,檢查順序是自底向上,從CustomClassLoader到BootStrapClassLoader逐層檢查,只要某個classloader已載入就視為已載入此類,保證此類只所有ClassLoader載入一次。而載入的順序是自頂向下,也就是由上層來逐層嘗試載入此類。
十三.深入探討 Java 類載入器
類載入器(class loader)是 Java™中的一個很重要的概念。類載入器負責載入 Java類的位元組程式碼到 Java 虛擬機器中。本文首先詳細介紹了 Java 類載入器的基本概念,包括代理模式、載入類的具體過程和執行緒上下文類載入器等,接著介紹如何開發自己的類載入器,最後介紹了類載入器在 Web 容器和 OSGi™中的應用。
類載入器是Java 語言的一個創新,也是Java 語言流行的重要原因之一。它使得Java 類可以被動態載入到Java 虛擬機器中並執行。類載入器從JDK 1.0 就出現了,最初是為了滿足Java Applet 的需要而開發出來的。JavaApplet 需要從遠端下載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 中與載入類相關的方法
方法 | 說明 |
| 返回該類載入器的父類載入器。 |
| 載入名稱為 |
| 查詢名稱為 |
| 查詢名稱為 |
| 把位元組陣列 |
| 連結指定的 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. 演示類載入器的樹狀組織結構的執行結果
[email protected]
[email protected]
如程式碼清單 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
物件。程式碼清單 4的執行結果如程式碼清單 5所示。
清單 5. 測試 Java 類是否相同的執行結果
java.lang.reflect.InvocationTargetException
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at classloader.ClassIdentity.testClassIdentity(ClassIdentity.java:26)
at classloader.ClassIdentity.main(ClassIdentity.java:9)
Caused by: java.lang.ClassCastException: com.example.Sample
cannot be cast to com.example.Sample
at com.example.Sample.setSample(Sample.java:7)
... 6 more
從程式碼清單 5給出的執行結果可以看到,執行時丟擲了java.lang.ClassCastException
異常。雖然兩個物件obj1
和obj2
的類的名字相同,但是這兩個類是由不同的類載入器例項來載入的,因此不被Java 虛擬機器認為是相同的。
瞭解了這一點之後,就可以理解代理模式的設計動機了。代理模式是為了保證Java 核心庫的型別安全。所有Java 應用都至少需要引用java.lang.Object
類,也就是說在執行的時候,java.lang.Object
這個類需要被載入到 Java 虛擬機器中。如果這個載入過程由Java 應用自己的類載入器來完成的話,很可能就存在多個版本的java.lang.Object
類,而且這些類之間是不相容的。通過代理模式,對於Java 核心庫的類的載入工作由引導類載入器來統一完成,保證了Java 應用所使用的都是同一個版本的Java 核心庫的類,是互相相容的。
不同的類載入器為相同名稱的類建立了額外的名稱空間。相同名稱的類可以並存在Java 虛擬機器中,只需要用不同的類載入器來載入它們即可。不同類載入器載入的類之間是不相容的,這就相當於在Java 虛擬機器內部建立了一個個相互隔離的Java 類空間。這種技術在許多框架中都被用到,後面會詳細介紹。
下面具體介紹類載入器載入類的詳細過程。
載入類的過程
在前面介紹類載入器的代理模式的時候,提到過類載入器會首先代理給其它類載入器來嘗試載入某個類。這就意味著真正完成類的載入工作的類載入器和啟動這個載入過程的類載入器,有可能不是同一個。真正完成類的載入工作是通過呼叫defineClass
來實現的;而啟動類的載入過程是通過呼叫loadClass
來實現的。前者稱為一個類的定義載入器(definingloader),後者稱為初始載入器(initiatingloader)。在Java 虛擬機器判斷兩個類是否相同的時候,使用的是類的定義載入器。也就是說,哪個類載入器啟動類的載入過程並不重要,重要的是最終定義這個類的載入器。兩種類載入器的關聯之處在於:一個類的定義載入器是它引用的其它類的初始載入器。如類com.example.Outer
引用了類com.example.Inner
,則由類com.example.Outer
的定義載入器負責啟動類com.example.Inner
的載入過程。
方法loadClass()
丟擲的是java.lang.ClassNotFoundException
異常;方法defineClass()
丟擲的是java.lang.NoClassDefFoundError
異常。
類載入器在成功載入某個類之後,會把得到的java.lang.Class
類的例項快取起來。下次再請求載入該類的時候,類載入器會直接使用快取的類的例項,而不會嘗試再次載入。也就是說,對於一個類載入器例項來說,相同全名的類只加載一次,即loadClass
方法不會被重複呼叫。
下面討論另外一種類載入器:執行緒上下文類載入器。
執行緒上下文類載入器
執行緒上下文類載入器(context class loader)是從 JDK 1.2 開始引入的。類java.lang.Thread
中的方法getContextClassLoader()
和setContextClassLoader(ClassLoadercl)
用來獲取和設定執行緒的上下文類載入器。如果沒有通過setContextClassLoader(ClassLoadercl)
方法進行設定的話,執行緒將繼承其父執行緒的上下文類載入器。Java應用執行的初始執行緒的上下文類載入器是系統類載入器。線上程中執行的程式碼可以通過此類載入器來載入類和資源。
前面提到的類載入器的代理模式並不能解決Java 應用開發中會遇到的類載入器的全部問題。Java提供了很多服務提供者介面(ServiceProvider Interface,SPI),允許第三方為這些介面提供實現。常見的SPI 有JDBC、JCE、JNDI、JAXP 和 JBI 等。這些 SPI 的介面由 Java 核心庫來提供,如 JAXP 的 SPI 介面定義包含在javax.xml.parsers
包中。這些 SPI 的實現程式碼很可能是作為 Java 應用所依賴的 jar 包被包含進來,可以通過類路徑(CLASSPATH)來找到,如實現了 JAXP SPI的Apache Xerces所包含的 jar 包。SPI 介面中的程式碼經常需要載入具體的實現類。如JAXP 中的javax.xml.parsers.DocumentBuilderFactory
類中的newInstance()
方法用來生成一個新的DocumentBuilderFactory
的例項。這裡的例項的真正的類是繼承自javax.xml.parsers.DocumentBuilderFactory
,由 SPI 的實現所提供的。如在 ApacheXerces 中,實現的類是org.apache.xerces.jaxp.DocumentBuilderFactoryImpl
。而問題在於,SPI 的介面是 Java 核心庫的一部分,是由引導類載入器來載入的;SPI實現的 Java 類一般是由系統類載入器來載入的。引導類載入器是無法找到SPI 的實現類的,因為它只加載Java 的核心庫。它也不能代理給系統類載入器,因為它是系統類載入器的祖先類載入器。也就是說,類載入器的代理模式無法解決這個問題。
執行緒上下文類載入器正好解決了這個問題。如果不做任何的設定,Java應用的執行緒的上下文類載入器預設就是系統上下文類載入器。在SPI 介面的程式碼中使用執行緒上下文類載入器,就可以成功的載入到SPI 實現的類。執行緒上下文類載入器在很多SPI 的實現中都會用到。
下面介紹另外一種載入類的方法:Class.forName
。
Class.forName
Class.forName
是一個靜態方法,同樣可以用來載入類。該方法有兩種形式:Class.forName(String name,boolean initialize, ClassLoader loader)
和Class.forName(String className)
。第一種形式的引數name
表示的是類的全名;initialize
表示是否初始化類;loader
表示載入時使用的類載入器。第二種形式則相當於設定了引數initialize
的值為true
,loader
的值為當前類的類載入器。Class.forName
的一個很常見的用法是在載入資料庫驅動的時候。如Class.forName("org.apache.derby.jdbc.EmbeddedDriver").newInstance()
用來載入 ApacheDerby 資料庫的驅動。
在介紹完類載入器相關的基本概念之後,下面介紹如何開發自己的類載入器。
開發自己的類載入器
雖然在絕大多數情況下,系統預設提供的類載入器實現已經可以滿足需求。但是在某些情況下,您還是需要為應用開發出自己的類載入器。比如您的應用通過網路來傳輸Java 類的位元組程式碼,為了保證安全性,這些位元組程式碼經過了加密處理。這個時候您就需要自己的類載入器來從某個網路地址上讀取加密後的位元組程式碼,接著進行解密和驗證,最後定義出要在Java 虛擬機器中執行的類來。下面將通過兩個具體的例項來說明類載入器的開發。
檔案系統類載入器
第一個類載入器用來載入儲存在檔案系統上的 Java 位元組程式碼。完整的實現如程式碼清單 6所示。
清單 6. 檔案系統類載入器
public class FileSystemClassLoader extends ClassLoader {
private String rootDir;
public FileSystemClassLoader(String rootDir) {
this.rootDir = rootDir;
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
}
else {
return defineClass(name, classData, 0, classData.length);
}
}
private byte[] getClassData(String className) {
String path = classNameToPath(className);
try {
InputStream ins = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesNumRead = 0;
while ((bytesNumRead = ins.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
private String classNameToPath(String className) {
return rootDir + File.separatorChar
+ className.replace('.', File.separatorChar) + ".class";
}
}
如程式碼清單 6所示,類FileSystemClassLoader
繼承自類java.lang.ClassLoader
。在表 1中列出的java.lang.ClassLoader
類的常用方法中,一般來說,自己開發的類載入器只需要覆寫findClass(String name)
方法即可。java.lang.ClassLoader
類的方法loadClass()
封裝了前面提到的代理模式的實現。該方法會首先呼叫findLoadedClass()
方法來檢查該類是否已經被載入過;如果沒有載入過的話,會呼叫父類載入器的loadClass()
方法來嘗試載入該類;如果父類載入器無法載入該類的話,就呼叫findClass()
方法來查詢該類。因此,為了保證類載入器都正確實現代理模式,在開發自己的類載入器時,最好不要覆寫loadClass()
方法,而是覆寫findClass()
方法。
類FileSystemClassLoader
的findClass()
方法首先根據類的全名在硬碟上查詢類的位元組程式碼檔案(.class檔案),然後讀取該檔案內容,最後通過defineClass()
方法來把這些位元組程式碼轉換成java.lang.Class
類的例項。
網路類載入器
下面將通過一個網路類載入器來說明如何通過類載入器來實現元件的動態更新。即基本的場景是:Java位元組程式碼(.class)檔案存放在伺服器上,客戶端通過網路的方式獲取位元組程式碼並執行。當有版本更新的時候,只需要替換掉伺服器上儲存的檔案即可。通過類載入器可以比較簡單的實現這種需求。
類NetworkClassLoader
負責通過網路下載 Java 類位元組程式碼並定義出 Java 類。它的實現與FileSystemClassLoader
類似。在通過NetworkClassLoader
載入了某個版本的類之後,一般有兩種做法來使