類加載器及其加載原理

概述

在之前的文章"類的加載流程"講了一個Class文件從加載到卸載整個生命週期的過程,並且提到"非數組類在加載階段是可控性最強的"。而這個優點很大程度上都是類加載器所帶了的,因而本篇文章就着重講一下類加載器的加載機制與加載原理。

首先我們思考一個問題:什麼是類加載器?

簡單來說就是加載類的二進制字節流的工具,那它是如何找到所要加載類的具體位置呢?

答案就是通過類的全限定名

因而我們可以這樣說,類加載器就是用來完成“通過一個類的全限定名來獲取描述該類的二進制字節流”這一加載動作的代碼。

類加載器的作用

顧名思義,類加載器的作用是實現類的加載動作。但它的作用就僅限於此嗎?

答案必然是否定的。

我們首先看下邊這一段代碼:

public class ClassLoaderTest {

    public static void main(String[] args) throws Exception {
    	//創建自定義的類加載器並重寫loadClass方法
        ClassLoader myLoader = new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                try {
                    String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
                    InputStream is = getClass().getResourceAsStream(fileName);
                    if (is == null) {
                        return super.loadClass(name);
                    }
                    byte[] b = new byte[is.available()];
                    is.read(b);
                    return defineClass(name, b, 0, b.length);
                } catch (IOException e) {
                    throw new ClassNotFoundException(name);
                }
            }
        };
        //使用自定義類加載器加載對象
        Object obj = myLoader.loadClass("test.ClassLoaderTest").newInstance();

        System.out.println(obj.getClass());
        System.out.println(obj instanceof test.ClassLoaderTest);
    }
}

代碼運行結果如下:

class test.ClassLoaderTest
false

上述代碼基本上所做的事情就是,創建了一個自定義的類加載器,然後使用這個類加載器加載了ClassLoaderTest類,並以該類爲模板創建了對象。

輸出結果上看,新創建的對象obj確實是以test.ClassLoaderTest爲類模板創建的,但爲何在判斷是否是test.ClassLoaderTest的實例對象時結果是false呢?

這是因爲在Java中一個類的唯一性不僅和類本身相關而且和加載它的類加載器相關,也就是說:任何一個類都必須由加載它的類加載器這個類本身一起確定其在Java中的唯一性,每一個類加載器都有一個獨立的類命名空間。

換句話說,如果想要比較兩個類是否相等時,只有這兩個類是同一個類加載器的前提下才有意義,否則,即使這兩個類是同一個Class文件,被同一個Java虛擬機加載,只要加載它們的類加載器不同,這兩個類必然就不相等。

這裏類的相等與否到底有何影響呢?

這裏的相等包含了的Class對象的equals()方法、isAssignableForm()isInstance()方法返回結果,也包括了使用instanceof關鍵字進行從屬判斷的各種情況。

通過上面的解釋,我們應該就懂了爲何例子程序中,在使用instanceof判斷的時候返回結果是false。因爲在例子程序中,Java虛擬中同時存在兩個test.ClassLoaderTest類,一個是由虛擬機的"應用程序類加載器"加載的,另一個是由自定義加載器加載的,但在Java虛擬機中仍然是兩個獨立的類,因而在做類型檢查時返回結果是false。

類的加載機制

前邊我們說了類加載器是參與類唯一性的判斷的,並且我們的Java虛擬機是有多個類加載器的,因而這裏就會有一個問題:多個類加載器在加載類的時候是如何進行協調的?它是如何解決重複加載這一問題的?

說到這裏,我們不得不提一下,類加載器的一個加載機制--雙親委派機制。但在正式聊雙親委派機制之前,我們有必要了解一下類加載器的類別和它們之間的一個層次關係。

類加載器的種類與關係

類加載器的層次關係圖,如下所示:

層次關係

從圖中可以看到,系統提供的類加載器主要有三個:

  1. BootstrapClassLoader(啓動類加載器) :用於加載系統類庫中的類,是最頂層的加載類,由C++實現,負責加載 %JAVA_HOME%/lib目錄下的jar包和類或者或被 -Xbootclasspath參數指定的路徑中的所有類。注意該加載器無法被Java程序直接引用,若在自定義加載器中需要委派給啓動類加載器加載,直接返回null即可。
  2. ExtensionClassLoader(擴展類加載器) :用於加載系統類庫擴展類,主要負責加載目錄 %JRE_HOME%/lib/ext 目錄下的jar包和類,或被 java.ext.dirs 系統變量所指定的路徑下的jar包。但在JDK9之後,這種擴展機制被模塊化的天然擴展能力所取代。
  3. AppClassLoader(應用程序類加載器) :面向我們用戶的加載器,負責加載當前應用classpath下的所有jar包和類。一般情況下,該加載器是默認加載器

最後是自定義類加載器(User Class Loader):這個加載器是需要在繼承ClassLoader的自定義加載器類UserClassLoader中重寫findClass()來實現的。

此處可能會有些疑問,既然應用程序類加載器都已經是默認的加載器了?那自定義類加載器還有什麼意義呢?

主要原因是應用程序加載器它只能加載在classpath下的所有類,但不在classpath下的class文件是無法被加載的,比如通過網絡遠程傳輸過來的class文件(遠程調用)或者我們在桌面上有一個class文件,希望在運行的過程中被加載和使用,在這兩種情況下,應用程序類加載器是無法加載的,此時如果想要加載必須靠自定義類。

爲了說明這一點,我們可以看一個例子:

首先我們定義一個待加載的普通類,放置在com.test包中:

package com.test;

public class Test {
    public void hello() {
        System.out.println("我是Test,由 " + getClass().getClassLoader().getClass()
                + " 加載進來的");
    }
}

將編譯生成後的class文件移動到其他位置,非當前項目的classpath下面,本例位置如下:

image.png

注意:

如果你是直接在當前項目裏面創建,待Test.java編譯後,請把Test.class文件拷貝走,再將Test.java刪除。因爲如果Test.class存放在當前項目中,根據雙親委派模型可知,會通過sun.misc.Launcher$AppClassLoader 類加載器加載。爲了讓我們自定義的類加載器加載,我們把Test.class文件放入到其他目錄。

如果此時我們想調用Test類,因爲該類不在項目的classpath下,因而無法通過系統加載器進行加載,只能通過用戶自定義加載器。

寫一個用戶自定義加載器,內容如下:

/**
 * 自定義類加載器,加載自定義位置下的class文件
 * @author vcjmhg
 *
 */
public class UserDefineClassLoader {
	 static class MyClassLoader extends ClassLoader {
	        private String classPath;

	        public MyClassLoader(String classPath) {
	            this.classPath = classPath;
	        }

	        private byte[] loadByte(String name) throws Exception {
	            name = name.replaceAll("\\.", "/");
	            FileInputStream fis = new FileInputStream(classPath + "/" + name
	                    + ".class");
	            int len = fis.available();
	            byte[] data = new byte[len];
	            fis.read(data);
	            fis.close();
	            return data;

	        }

	        protected Class<?> findClass(String name) throws ClassNotFoundException {
	            try {
	                byte[] data = loadByte(name);
	                return defineClass(name, data, 0, data.length);
	            } catch (Exception e) {
	                e.printStackTrace();
	                throw new ClassNotFoundException();
	            }
	        }

	    };

	    public static void main(String args[]) throws Exception {
	        MyClassLoader classLoader = new MyClassLoader("C:\\Users\\vcjmhg\\Desktop\\Test");
	        Class clazz = classLoader.loadClass("com.test.Test");
	        Object obj = clazz.newInstance();
	        Method helloMethod = clazz.getDeclaredMethod("hello", null);
	        helloMethod.invoke(obj, null);
	    }
}

上述代碼,基本意思就是:定義了一個類加載器MyClassLoader可以對指定文件夾下的class文件進行加載,在加載完成之後通過反射創建了一個obj對象並調用了其hello()方法。關於自定義加載器定義方法可以參考後續小節的"如何自定義類加載器?"

運行結果如下:

我是Test,由 class com.test.UserDefineClassLoader$MyClassLoader 加載進來的

上邊的例子,就解釋了自定義類加載器的作用:它可以按照用戶要求加載指定的class文件,無論class文件是通過網絡傳過來,還是本地的某個路徑,都可以實現加載。

雙親委派機制

其實從前邊那個類的層次關係圖中,,我們就可以簡單瞭解雙親委派模型加載機制:

當一個類加載器收到類加載請求時,首先不會自己嘗試加載這個類,而是把這個請求委派父類加載器去完成,每一層的類加載器都是如此,因此所有的加載請求最終都會傳送到頂層的啓動類加載器中,只有父加載器無法完成這個加載請求(它的搜索範圍內,找不到所需的類)時,子加載器纔會嘗試自己去完成加載。

結合自定義加載器,整個類的加載流程如下圖所示:

image.png

  1. 當我們的自定義類加載器要加載一個類的時候,會首先判斷給定的類是否被加載過,如果已經被加載過則不再加載,可以直接使用;如果沒被加載,它也不會直接加載而是把加載任務委派給父加載器也就是AppClassLoader 加載器。
  2. AppClassLoader在收到委派的加載任務後,也不直接加載,也會做一個自己是否加載過的判斷,,如果沒有將將加載任務委派給ExtClassLoader
  3. ExtClassLoader收到委派的任務後,在自己沒有加載過該類的情況下,會將加載任務委派給BootstrapClassLoader,由於BootstrapClassLoader是頂層加載器沒有父加載器,因而BootstrapClassLoader會開始嘗試自己加載,如果說需要加載的類位於其加載範圍(比如-Xbootclasspath參數指定的加載類),則直接返回加載結果。否則下沉到子類加載器進行加載,直到底層的自定義類加載器
  4. 要注意,如果所有的類加載器最終都無法加載,會拋出一個ClassNotFoundException

到這裏可能就會有小夥伴有疑問了:這玩意有什麼用?爲什麼要這樣設計呢?

雙親委派機制有什麼用?

這種加載機制一個顯而易見的好處就是Java中的類隨着它的它的類加載器一起具備了具有優先級的層級結構。比如類java.lang.Object,它存放在rt.jar需要被頂層啓動類加載器所加載,因而Object類在程序的各種類加載器的環境中都能保證是同一個類,避免了重複加載的情況發生,而這也避免了危險代碼植入的風險(比如惡意替換java.lang.Object類)。

雙親委派模型對於Java程序的穩定性極其重要,但其實現卻異常簡單。

雙親委派機制如何實現的?

用以實現雙親委派的代碼只有短短十多行,全部集中在java.lang.ClassLoaderloadClass()方法之中,具體實現代碼如下:

//name:被加載類的全限定名
// resolove:是否連接了已加載的類
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // 首先檢查類是否已經被加載了
        Class<?> c = findLoadedClass(name);
	//未被加載的情況下,嘗試用父類加載器進行加載
        if (c == null) {
            try {
		//存在父類加載器的情況下繼續向上委託
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
		//使用頂層加載器BootstrapClassLoader進行加載
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // 父類加載器無法加載的情況下拋出ClassNotFoundException異常
                // 說明父類加載器無法完成加載請求
            }

            if (c == null) {
                // 父類加載器無法加載的情況下,調用本加載器的findClass()方法着手進行加載
                c = findClass(name);
            }
        }
	//如果resolve標記爲true,則在加載操作完成後執行鏈接操作
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

這段代碼邏輯非常簡單:先檢查類是否被加載,如果沒有被加載則委託父類加載器進行加載,若父類加載器也無法加載則調用findClass()方法進行進行加載。

如何自定義類加載器?

實現一個自定義類加載器有兩種情況:

第一種 不破壞“雙親委派機制”

loadClass()的代碼中可以看到,類加載的最後一步就是調用findClass()方法,因而如果要實現一個自定義類加載器需要首先繼承ClassLoader類,然後重寫其findClass()方法。

我們可以首先看下findClass()的默認實現:

protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
}

可以看出,抽象類ClassLoaderfindClass()函數默認只是拋出異常的,因此要自定義類加載器必須要重寫findClass()方法,根據傳入的字符串(指定類文件路徑)生成對應的Class對象

那如何生成一個Class對象呢?

很簡單,Java提供了defineClass()方法,通過這個方法,我們可以把一個字節數組轉爲Class對象。

defineClass()方法的默認實現如下:

protected final Class<?> defineClass(String name, byte[] b, int off, int len)
        throws ClassFormatError  {
        return defineClass(name, b, off, len, null);
}

當重寫findClass()首先要獲取到Class文件的字節流數據,然後將字節流數據傳遞給defineClass()方法最終獲取到一個Class對象,至此在不破壞雙親委派機制的情況下,就完成了自定義類加載器。具體實現可以參考前邊的"例子",該自定義類加載器的基本思路就是重寫了findClass()方法。

第二種 “破壞雙親委託機制”

可能在某些場景下,需要使用自定義加載器加載一些特殊的類文件,比如位於classpath路徑下的一些類文件,如果直接重寫findClass()方法,由於雙親委派機制該類必然會被AppClassLoader所加載。因而若要實現這樣的類加載器,必須要重寫loadClass()方法。

如何破壞雙親委託?

因爲雙親委派機制並不是一個強制性約束的模型,而是Java設計者推薦給開發者們的類加載器實現方式,因而在"模塊化"出現之前,雙親委託模型主要出現了3次被較大規模“被破壞”的情況:

第一次:爲了保證兼容性

由於雙親委派模型是在JDK1.2之後才被引入的,而類加載器這一概念java.lang.ClasssLoader這一概念在Java第一個版本中便存在了。因而爲了保證向前兼容性,兼用JDK1.2之前已經存在的自定義類加載器代碼,Java設計者在引入雙親委派模型的時候做了一些妥協,不直接以技術手段避免loadClass()被覆蓋的可能性,而是將雙親委派模型的邏輯代碼寫在loadClass()方法中。並且引導用戶在編寫自定義類加載器時,儘量重寫新添加的findClass()方法,而不是覆蓋loadClass()方法。

第二次:爲了實現SPI技術

某些情況下,基礎類型需要調用用戶的代碼,比如JNDI技術(對資源幾種查找和管理的技術),它需要調用其他廠商實現並部署在應用程序的ClassPath下的JNDI服務提供者接口SPI的代碼,但是啓動類絕不可能認識和加載這些代碼,那該怎麼辦呢?

爲了解決該問題,Java設計團隊設計了一個線程上下文加載器(Thread Context ClassLoader)。這個類加載器可以通過java.lang.Thread類的setContextLoader()方法進行設計,如果創建線程時,沒有設置它將從父線程中繼承一個,如果在應用程序的全局範圍內都沒有設置過的話,那麼這個類加載器默認就是應用程序類加載器。

有了這些上下文類加載器之後,JNDI服務使用這個線程上下文加載器去加載所需要的SPI代碼,這是一種父加載器請求自類加載器的類加載行爲。

第三次:用戶對應用程序動態性的追求所導致的

爲了追求應用程序的動態性,IBM在2008年提出了OSGI技術,用來實現模塊化的熱部署。

其實現熱部署的原理如下:

OSGI實現模塊化熱部署的關鍵是它自定義的類加載器機制的實現,每一個程序模塊(Bundle)都有一個自己的類加載器,當需要替換一個Bundle時,就把Bundle連同類加載器一起替換掉以實現代碼的熱替換。在OSGI環境下,類加載器不再是雙親委派模型推薦的樹狀結構,而逐步發展成了網狀結構,當收到請求加載時,OSGI將按照如下順序進行類搜索:

  1. 將以java.*開頭的類,委派給父類加載器加載
  2. 否則,將委派列表名單的類,委派給父加載器進行加載
  3. 否則,將Import列表中的類委派給Export這個類的Bundle的類加載器進行加載
  4. 否則,查找當前Bundle的ClassPath,使用自己的類加載器進行加載
  5. 否則,查找類是否在自己的Fragment Bundle中,如果在,則委派給Fragment Bundle的類加載器進行加載
  6. 否則,查找Dynamic Import列表的Bundle,委派給對應Bundle的類加載器加載
  7. 否則,類查找失敗

從上邊流程中我們可以看到,只有前兩條符合雙親委派模型,其他的均不符合。

總結

本文主要講了常用的類加載器,比如啓動類加載器、擴展類加載器、應用類加載器以及自定義類加載器,詳細介紹了類加載器在加載一個類時的原理以及加載所使用的雙親委派機制。以及使用雙親委派機制的好處以及破壞該機制的一些情況。

引用

  1. Java自定義類加載器與雙親委派模型
  2. 深入理解jvm虛擬機 第三版
  3. 類加載器
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章