1. 程式人生 > >Java類載入器深入探索

Java類載入器深入探索

          林炳文Evankaka原創作品。轉載請註明出處http://blog.csdn.net/evankaka

什麼是.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