Java類載入器深入探索
什麼是.class檔案?
class檔案全名稱為Java class檔案,主要在平臺無關性和網路移動性方面使Java更適合網路。它在平臺無關性方面的任務是:為Java程式提供獨立於底層主機平臺的二進位制形式的服務。class檔案徑打破了C或者C++等語言所遵循的傳統,使用這些傳統語言寫的程式通常首先被編譯,然後被連線成單獨的、專門支援特定硬體平臺和作業系統的二進位制檔案。通常情況下,一個平臺上的二進位制可執行檔案不能在其他平臺上工作。而Java class檔案是可以執行在任何支援Java虛擬機器的硬體平臺和作業系統上的二進位制檔案。而這也是Java宣稱的“一次編譯,到處執行”的真正原因,因為各個系統上的Java檔案都是被編譯成.class檔案,然後通過虛擬機器來載入執行的。
什麼是類載入器?
類載入器是一個用來載入類檔案的類。Java原始碼通過javac編譯器編譯成類檔案。然後JVM來執行類檔案中的位元組碼來執行程式。類載入器負責載入檔案系統、網路或其他來源的類檔案。有三種預設使用的類載入器:Bootstrap類載入器、Extension類載入器和System類載入器(或者叫作Application類載入器)。每種類載入器都有設定好從哪裡載入類。
生成一個物件例項發生了什麼事?
生成一個例項,程式主要會把對應的類的java檔案使用編譯器生成位元組碼檔案,然後等此類被呼叫靜態變數或方法或生成例項時,虛擬機器自動去相應目錄查詢位元組碼檔案,並載入到虛擬機器當中,然後生成對應的例項物件。每一個位元組碼檔案只會被載入一次。其過程如下:
類載入的方式
Java提供兩種方法來達成動態行,一種是隱式的,另一種是顯式的。這兩種方式底層用到的機制完全相同,差異只有程式程式碼不同。隱式的就是當用到new這個Java關鍵字時,會讓類載入器依需求載入所需的類。顯式的又分為兩種方法:一種是借用java.lang.Class裡的forName()方法,另一種則是借用java.lang.ClassLoader裡的loadClass()方法。
類載入器的樹狀組織結構及載入檔案目錄
Java 中的類載入器大致可以分成兩類,一類是系統提供的,另外一類則是由 Java 應用開發人員編寫的。系統提供的類載入器主要有下面三個:
(1) Bootstrap ClassLoader(引導類載入器) : 它用來載入 Java 的核心庫,是用原生程式碼來實現的,並不繼承自 java.lang.ClassLoader。將存放於<JAVA_HOME>\lib目錄中的,或者被-Xbootclasspath引數所指定的路徑中的,並且是虛擬機器識別的(僅按照檔名識別,如 rt.jar 名字不符合的類庫即使放在lib目錄中也不會被載入)類庫載入到虛擬機器記憶體中。啟動類載入器無法被Java程式直接引用
(2) Extension ClassLoader(擴充套件類載入器) : 它用來載入 Java 的擴充套件庫。Java 虛擬機器的實現會提供一個擴充套件庫目錄。該類載入器在此目錄裡面查詢並載入 Java 類。將<JAVA_HOME>\lib\ext目錄下的,或者被java.ext.dirs系統變數所指定的路徑中的所有類庫載入。開發者可以直接使用擴充套件類載入器。
(3) Application ClassLoader或叫System Classloader (系統類載入器): 負責載入使用者類路徑(ClassPath)上所指定的類庫,開發者可直接使用。它根據 Java 應用的類路徑(CLASSPATH)來載入 Java 類。一般來說,Java 應用的類都是由它來完成載入的。可以通過 ClassLoader.getSystemClassLoader()來獲取它。
以下有兩種方式來取得類載入器的組織結構:
package com.lin;
public class ClassLoadTest1 {
public static void main(String[] args) {
ClassLoader loader = ClassLoadTest1.class.getClassLoader();
ClassLoader loader1 = ClassLoader.getSystemClassLoader();
//從子到父取得載入器
while (loader != null) {
System.out.println(loader.toString());
loader = loader.getParent();
}
while (loader1 != null) {
System.out.println(loader1.toString());
loader1 = loader1.getParent();
}
}
}
輸出結果:
可以看到,兩種方法都是先取得 Application ClassLoader,然後再取得Extension ClassLoader。
表 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 類。
除了系統提供的類載入器以外,開發人員可以通過繼承 java.lang.ClassLoader類的方式實現自己的類載入器,以滿足一些特殊的需求。除了引導類載入器之外,所有的類載入器都有一個父類載入器。通過 表 1中給出的 getParent()方法可以得到。對於系統提供的類載入器來說,系統類載入器的父類載入器是擴充套件類載入器,而擴充套件類載入器的父類載入器是引導類載入器;對於開發人員編寫的類載入器來說,其父類載入器是載入此類載入器 Java 類的類載入器。因為類載入器 Java 類如同其它的 Java 類一樣,也是要由類載入器來載入的。一般來說,開發人員編寫的類載入器的父類載入器是系統類載入器。類載入器通過這種方式組織起來,形成樹狀結構。樹的根節點就是引導類載入器。下圖 中給出了一個典型的類載入器樹狀組織結構示意圖,其中的箭頭指向的是父類載入器。
每次載入的具體的過程:
類載入器工作過程
類裝載器就是尋找類的位元組碼檔案,並構造出類在JVM內部表示的物件元件。在Java中,類裝載器把一個類裝入JVM中,要經過以下步驟:
(1) 裝載:查詢和匯入Class檔案;
(2) 連結:把類的二進位制資料合併到JRE中;
(a)校驗:檢查載入Class檔案資料的正確性;
(b)準備:給類的靜態變數分配儲存空間;
(c)解析:將符號引用轉成直接引用;
(3) 初始化:對類的靜態變數,靜態程式碼塊執行初始化操作
類載入器的工作原理
(1)委託機制
當一個類載入和初始化的時候,類僅在有需要載入的時候被載入。假設你有一個應用需要的類叫作Abc.class,首先載入這個類的請求由Application類載入器委託給它的父類載入器Extension類載入器,然後再委託給Bootstrap類載入器。Bootstrap類載入器會先看看rt.jar中有沒有這個類,因為並沒有這個類,所以這個請求由回到Extension類載入器,它會檢視jre/lib/ext目錄下有沒有這個類,如果這個類被Extension類載入器找到了,那麼它將被載入,而Application類載入器不會載入這個類;而如果這個類沒有被Extension類載入器找到,那麼再由Application類載入器從classpath中尋找。記住classpath定義的是類檔案的載入目錄,而PATH是定義的是可執行程式如javac,java等的執行路徑。
工作過程:如果一個類載入器接收到了類載入的請求,它首先把這個請求委託給他的父類載入器去完成,每個層次的類載入器都是如此,因此所有的載入請求都應該傳送到頂層的啟動類載入器中,只有當父載入器反饋自己無法完成這個載入請求(它在搜尋範圍中沒有找到所需的類)時,子載入器才會嘗試自己去載入。
好處:java類隨著它的類載入器一起具備了一種帶有優先順序的層次關係。例如類java.lang.Object,它存放在rt.jar中,無論哪個類載入器要載入這個類,最終都會委派給啟動類載入器進行載入,因此Object類在程式的各種類載入器環境中都是同一個類。相反,如果使用者自己寫了一個名為java.lang.Object的類,並放在程式的Classpath中,那系統中將會出現多個不同的Object類,java型別體系中最基礎的行為也無法保證,應用程式也會變得一片混亂。
首先需要說明一下 Java 虛擬機器是如何判定兩個 Java 類是相同的。Java 虛擬機器不僅要看類的全名是否相同,還要看載入此類的類載入器是否一樣。只有兩者都相同的情況,才認為兩個類是相同的。即便是同樣的位元組程式碼,被不同的類載入器載入之後所得到的類,也是不同的。比如一個 Java 類 com.example.Sample,編譯之後生成了位元組程式碼檔案 Sample.class。兩個不同的類載入器 ClassLoaderA和 ClassLoaderB分別讀取了這個 Sample.class檔案,並定義出兩個 java.lang.Class類的例項來表示這個類。這兩個例項是不相同的。對於 Java 虛擬機器來說,它們是不同的類。試圖對這兩個類的物件進行相互賦值,會丟擲執行時異常 ClassCastException。下面通過示例來具體說明。
package com.lin;
public class Sample {
private Sample instance;
public void setSample(Object instance) {
this.instance = (Sample) instance;
}
public void say(){
System.out.println("Hello LinBingwen");
}
}
然後是使用:
package com.lin;
import java.net.*;
import java.lang.reflect.*;
public class ClassLoadTest4{
public static void main(String[] args) throws ClassNotFoundException, MalformedURLException, IllegalAccessException, NoSuchMethodException, InstantiationException, InvocationTargetException{
ClassLoader pClassLoader = ClassLoader.getSystemClassLoader(); // 以System ClassLoader作為父類載入器
URL[] baseUrls = {new URL("file:/E:/workspace/Eclipse/ClassLoadTest")}; // 搜尋類庫的目錄
final String binaryName = "com.lin.Sample"; // 需要載入的類的二進位制名稱
ClassLoader userClassLoader1 = new URLClassLoader(baseUrls, pClassLoader);
ClassLoader userClassLoader2 = new URLClassLoader(baseUrls, pClassLoader);
Class clazz1 = userClassLoader1.loadClass(binaryName);
Class clazz2 = userClassLoader2.loadClass(binaryName);
Object instance1 = clazz1.newInstance();
Object instance2 = clazz2.newInstance();
// 呼叫say方法
clazz1.getMethod("say").invoke(instance1);
clazz2.getMethod("say").invoke(instance2);
// 輸出類的二進位制名稱
System.out.println(clazz1.toString());
System.out.println(clazz2.toString());
// 比較兩個類的地址是否相同
System.out.println(clazz1 == clazz2);
// 比較兩個類是否相同或是否為繼承關係
System.out.println(clazz1.isAssignableFrom(clazz2));
// 檢視型別轉換是否成功
boolean ret = true;
try{
Method setSampleMethod = clazz1.getMethod("setSample", java.lang.Object.class);
setSampleMethod.invoke(instance1, instance2);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(ret);
}
}
輸出結果:因為都是從 ClassLoader.getSystemClassLoader(); // 以System ClassLoader作為父類載入器,所以兩個載入器其實是一樣的。(2)可見性機制
根據可見性機制,子類載入器可以看到父類載入器載入的類,而反之則不行。所以下面的例子中,當Abc.class已經被Application類載入器載入過了,然後如果想要使用Extension類載入器載入這個類,將會丟擲java.lang.ClassNotFoundException異常。
package com.lin;
import java.util.logging.Level;
import java.util.logging.Logger;
public class ClassLoadTest2 {
public static void main(String[] args) {
try {
//列印當前的類載入器
System.out.println("ClassLoadTest2.getClass().getClassLoader() : "
+ ClassLoadTest2.class.getClassLoader());
//使用擴充套件類載入器再次載入子類載入器載入過的
Class.forName(" com.lin.ClassLoadTest1", true
, ClassLoadTest2.class.getClassLoader().getParent());
} catch (ClassNotFoundException ex) {
Logger.getLogger(ClassLoadTest2.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
(3)單一性機制
根據這個機制,父載入器載入過的類不能被子載入器載入第二次。雖然重寫違反委託和單一性機制的類載入器是可能的,但這樣做並不可取。你寫自己的類載入器的時候應該嚴格遵守這三條機制。
參考文章:
1、https://www.ibm.com/developerworks/cn/java/j-lo-classloader/
2、http://www.cnblogs.com/ITtangtang/p/3978102.html
3、http://www.cnblogs.com/rason2008/archive/2012/01/01/2309718.html
4、http://www.importnew.com/6581.html