深入探討Java 類載入器

來源:互聯網
上載者:User

標籤:java類載入器   虛擬機器   

什麼是類載入器?

類載入器(class loader)用來載入 Java 類到 JAVA 虛擬機器中。一般來說,JAVA 虛擬機器使用 Java 類的方式如下:Java 來源程式(.java 檔案)在經過 Java 編譯器編譯之後就被轉換成 Java 位元組代碼(.class 檔案)。類載入器負責讀取 Java 位元組代碼,並轉換成 java.lang.Class類的一個執行個體。每個這樣的執行個體用來表示一個 Java 類。通過此執行個體的 newInstance()方法就可以建立出該類的一個對象。實際的情況可能更加複雜,比如 Java 位元組代碼可能是通過工具動態產生的,也可能是通過網路下載的。基本上所有的類載入器都是 java.lang.ClassLoader類的一個執行個體。

類載入器是 Java 語言的一個創新,也是 Java 語言流行的重要原因之一。它使得 Java 類可以被動態載入到 JAVA 虛擬機器中並執行。類載入器從 JDK 1.0 就出現了,最初是為了滿足 Java Applet 的需要而開發出來的。Java Applet 需要從遠程下載 Java 類檔案到瀏覽器中並執行。現在類載入器在 Web 容器和 OSGi 中得到了廣泛的使用。一般來說,Java 應用的開發人員不需要直接同類載入器進行互動。JAVA 虛擬機器預設的行為就已經足夠滿足大多數情況的需求了。不過如果遇到了需要與類載入器進行互動的情況,而對類載入器的機制又不是很瞭解的話,就很容易花大量的時間去調試ClassNotFoundException和 NoClassDefFoundError等異常。

類載入器的樹狀組織圖

從Java開發人員的角度來看,絕大部分的Java程式都會使用到以下3種系統提供的類載入器:

類載入器的代理模式

如果一個類載入器收到了類載入請求,它首先不會自己嘗試載入這個類,而是把這個請求委派給父類載入器去完成,每一個層次的類載入器都是如此,因此所有的載入請求最終都應該傳送到頂層的啟動類載入器中,只有當父載入器反饋自己無法完成這個載入請求(在它的搜尋範圍中沒有找到)時,子載入器才會嘗試自己去載入。

這種工作過程的模型叫做雙親委派模型,它要求除了頂層的啟動類載入器外,其餘的類載入器都應當有自己的父類載入器。這裡類載入器之間的父子關係一般不會以繼承的關係來實現,而是都使用組合關係來複用父載入器的代碼。

Java虛擬機器是如何判定兩個Java類是相同的?

JAVA 虛擬機器不僅要看類的全名是否相同,還要看載入此類的類載入器是否一樣。只有兩者都相同的情況,才認為兩個類是相同的。即便是同樣的位元組代碼,被不同的類載入器載入之後所得到的類,也是不同的。比如一個Java類Xlinsist,編譯之後產生了位元組代碼檔案 Xlinsist.class。兩個不同的類載入器 ClassLoaderA和 ClassLoaderB分別讀取了這個 Xlinsist.class檔案,並定義出兩個 java.lang.Class類的執行個體來表示這個類。這兩個執行個體是不相同的。對於 JAVA 虛擬機器來說,它們是不同的類。

下面的代碼將驗證這一點:

package hxl.insist.jvm;import java.io.IOException;import java.io.InputStream;public class LoadClassDemo {    public static void main(String[] args) throws Exception {        ClassLoader myloader = new ClassLoader() {            @Override            public Class<?> loadClass(String name) throws ClassNotFoundException {                try {                    String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";                    InputStream inputStream = this.getClass().getResourceAsStream(fileName);                    if (inputStream == null) {                        return super.loadClass(name);                    }                    byte[] classBytes = new byte[inputStream.available()];                    inputStream.read(classBytes);                    return super.defineClass(name, classBytes, 0, classBytes.length);                } catch (IOException e) {                    throw new ClassNotFoundException(name);                }            }        };        Object instance = myloader.loadClass("hxl.insist.jvm.LoadClassDemo").newInstance();        System.out.println(instance instanceof hxl.insist.jvm.LoadClassDemo);//false        System.out.println(new LoadClassDemo() instanceof hxl.insist.jvm.LoadClassDemo);//true    }}

之所以出現這樣的結果是由於虛擬機器中存在了兩個LoadClassDemo類,一個是由系統應用程式類載入器載入的,另外一個是由我們自訂的類載入器載入的,雖然都來自同一個Class檔案,但依然是兩個獨立的類。

載入類的過程

在前面介紹類載入器的代理模式的時候,提到過類載入器會首先代理給其它類載入器來嘗試載入某個類。這就意味著真正完成類的載入工作的類載入器和啟動這個載入過程的類載入器,有可能不是同一個。真正完成類的載入工作是通過調用 defineClass來實現的;而啟動類的載入過程是通過調用 loadClass來實現的。前者稱為一個類的定義載入器(defining loader),後者稱為初始載入器(initiating loader)。在 JAVA 虛擬機器判斷兩個類是否相同的時候,使用的是類的定義載入器。也就是說,哪個類載入器啟動類的載入過程並不重要,重要的是最終定義這個類的載入器。兩種類載入器的關聯之處在於:一個類的定義載入器是它引用的其它類的初始載入器。比如下面的代碼:

public class ClassLoaderTest {    javax.swing.JFrame f = new javax.swing.JFrame();}

如果應用程式類載入器是ClassLoaderTest的定義載入器,那麼應用程式類載入器就是javax.swing.JFrame的初始載入器。

類載入器在成功載入某個類之後,會把得到的 java.lang.Class類的執行個體緩衝起來。下次再請求載入該類的時候,類載入器會直接使用緩衝的類的執行個體,而不會嘗試再次載入。也就是說,對於一個類載入器執行個體來說,相同全名的類只載入一次,即 loadClass方法不會被重複調用。

瞭解java.lang.ClassLoader

java.lang.ClassLoader類的基本職責就是根據一個指定的類的名稱,找到或者產生其對應的位元組代碼,然後從這些位元組代碼中定義出一個 Java 類,即 java.lang.Class類的一個執行個體。除此之外,ClassLoader還負責載入 Java 應用所需的資源,像檔案和設定檔等。

Extension ClassLoader和App ClassLoader都是java.lang.ClassLoader類的執行個體,但是Bootstrap ClassLoader不繼承自ClassLoader,因為它不是一個普通的Java類,底層由C++編寫,已嵌入到了JVM核心當中,當JVM啟動後,Bootstrap ClassLoader也隨著啟動,負責載入完核心類庫後,並構造Extension ClassLoader和App ClassLoader類載入器。

使用者也可以根據需要定義自已的ClassLoader,而這些自訂的ClassLoader要是java.lang.ClassLoader的子類。

java.lang.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 類的位元組代碼,為了保證安全性,這些位元組代碼經過了加密處理。這個時候您就需要自己的類載入器來從某個網路地址上讀取加密後的位元組代碼,接著進行解密和驗證,最後定義出要在 JAVA 虛擬機器中啟動並執行類來。下面將通過具體的執行個體來建立自己的類載入器。

package hxl.insist.jvm;import java.io.ByteArrayOutputStream;import java.io.FileInputStream;import java.io.IOException;import java.io.InputStream;public class CustomizeClassLoader extends ClassLoader {    @Override    protected Class<?> findClass(String name) throws ClassNotFoundException {        byte[] classBytes = getClassData("E:\\TestDemo.class");        if (classBytes == null) {             throw new ClassNotFoundException();         } else {             return defineClass(name, classBytes, 0, classBytes.length);        }     }    /**     * 讀取硬碟中的Class檔案內容,將其轉變為位元組數組     * @param rootPath     * @return     */    private byte[] getClassData(String rootPath) {        try {            InputStream inputStream = new FileInputStream(rootPath);            ByteArrayOutputStream baos = new ByteArrayOutputStream();            byte[] buffer = new byte[1024];            int bytesNumRead = 0;            while ((bytesNumRead = inputStream.read(buffer)) != -1) {                baos.write(buffer, 0, bytesNumRead);            }            return baos.toByteArray();        } catch (IOException e) {            e.printStackTrace();        }        return null;    }}
線程上下文類載入器

線程上下文類載入器(context class loader)是從 JDK 1.2 開始引入的。類 java.lang.Thread中的方法 getContextClassLoader()和 setContextClassLoader(ClassLoader cl)用來擷取和設定線程的上下文類載入器。如果沒有通過 setContextClassLoader(ClassLoader cl)方法進行設定的話,線程將繼承其父線程的上下文類載入器。Java 應用啟動並執行初始線程的上下文類載入器是系統類別載入器。線上程中啟動並執行代碼可以通過此類載入器來載入類和資源。

前面提到的類載入器的代理模式並不能解決 Java 應用開發中會遇到的類載入器的全部問題。Java 提供了很多服務提供者介面(Service Provider 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 的實現所提供的。如在 Apache Xerces 中,實現的類是 org.apache.xerces.jaxp.DocumentBuilderFactoryImpl。而問題在於,SPI 的介面是 Java 核心庫的一部分,是由引導類載入器來載入的;SPI 實現的 Java 類一般是由系統類別載入器來載入的。引導類載入器是無法找到 SPI 的實作類別的,因為它只載入 Java 的核心庫。它也不能代理給系統類別載入器,因為它是系統類別載入器的祖先類載入器。也就是說,類載入器的代理模式無法解決這個問題。

線程上下文類載入器正好解決了這個問題。如果不做任何的設定,Java 應用的線程的上下文類載入器預設就是系統上下文類載入器。在 SPI 介面的代碼中使用線程上下文類載入器,就可以成功的載入到 SPI 實現的類。線程上下文類載入器在很多 SPI 的實現中都會用到。

另外一種載入類的方法: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()用來載入 Apache Derby 資料庫的驅動。

著作權聲明:本文為博主原創文章,未經博主允許不得轉載。

深入探討Java 類載入器

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.