深入分析ClassLoader

why?

ClassLoader,即java類加載器,主要作用是將class加載到JVM內,同時它還要考慮class由誰來加載。在說java的類加載機制之前,還是像前面的博客一樣,先說說爲什麼要知道java的類加載機制。個人認爲主要有以下幾個原因:

  • 按需加載。JVM啓動時不能確定我要加載哪些東西,或者有些類非常大,我只希望用到它時再加載,並非一次性加載所有的class,所以這時候瞭解了加載機制就可以按需加載了。
  • 類隔離。比如web容器中部署多個應用,應用之間互相可能會有衝突,所以希望儘量隔離,這裏可能就要分析各個應用加載的資源和加載順序之間的衝突,針對這些衝突再自己定些規則,讓它們能夠愉快地玩耍。
  • 資源回收。如果你不瞭解java是如何加載資源的,又怎麼理解java是如何回收資源的?

what?

一般說到java的類加載機制,都要說到“雙親委派模型”(其實個人很不理解爲什麼叫“雙親”,其實英文叫“parent”)。使用這種機制,可以避免重複加載,當父親已經加載了該類的時候,就沒有必要子ClassLoader再加載一次。JVM根據 類名+包名+ClassLoader實例ID 來判定兩個類是否相同,是否已經加載過(所以這裏可以略微擴展下,可以通過創建不同的classloader實例來實現類的熱部署)。
有個圖很形象(來源見參考資料)。

ClassLoader

注意:如果想形象地看到java的類加載順序,可以在運行java的時候加個啓動參數,即java –verbose

下面結合上圖來詳細介紹下java的類加載過程。

  • BootStrapClassLoader。它是最頂層的類加載器,是由C++編寫而成, 已經內嵌到JVM中了。在JVM啓動時會初始化該ClassLoader,它主要用來讀取Java的核心類庫JRE/lib/rt.jar中所有的class文件,這個jar文件中包含了java規範定義的所有接口及實現。
  • ExtensionClassLoader。它是用來讀取Java的一些擴展類庫,如讀取JRE/lib/ext/*.jar中的包等(這裏要注意,有些版本的是沒有ext這個目錄的)。
  • AppClassLoader。它是用來讀取CLASSPATH下指定的所有jar包或目錄的類文件,一般情況下這個就是程序中默認的類加載器。
  • CustomClassLoader。它是用戶自定義編寫的,它用來讀取指定類文件 。基於自定義的ClassLoader可用於加載非Classpath中(如從網絡上下載的jar或二進制)的jar及目錄、還可以在加載前對class文件優一些動作,如解密、編碼等。

很多資料和文章裏說,ExtClassLoader的父類加載器是BootStrapClassLoader,其實這裏省掉了一句話,容易造成很多新手(比如我)的迷惑。嚴格來說,ExtClassLoader的父類加載器是null,只不過在默認的ClassLoader 的 loadClass 方法中,當parent爲null時,是交給BootStrapClassLoader來處理的,而且ExtClassLoader 沒有重寫默認的loadClass方法,所以,ExtClassLoader也會調用BootStrapLoader類加載器來加載,這就導致“BootStrapClassLoader具備了ExtClassLoader父類加載器的功能”。看一下下面的代碼就很容易理解上面這一大段話了。

/**
 * 查看父類加載器
 */
private static void test1() {
    ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
    System.out.println("系統類裝載器:" + appClassLoader);
    ClassLoader extensionClassLoader = appClassLoader.getParent();
    System.out.println("系統類裝載器的父類加載器——擴展類加載器:" + extensionClassLoader);
    ClassLoader bootstrapClassLoader = extensionClassLoader.getParent();
    System.out.println("擴展類加載器的父類加載器——引導類加載器:" + bootstrapClassLoader);
}

可以看出ExtensionClassLoaderparentnull

三個重要的方法

查看classloader的源碼可以發現三個重要的方法:

  • loadClass。classloader加載類的入口,此方法負責加載指定名字的類,ClassLoader的實現方法爲先從已經加載的類中尋找,如沒有則繼續從父ClassLoader中尋找,如仍然沒找到,則從BootstrapClassLoader中尋找,最後再調用findClass方法來尋找,如要改變類的加載順序,則可覆蓋此方法,如加載順序相同,則可通過覆蓋findClass來做特殊的處理,例如解密、固定路徑尋找等,當通過整個尋找類的過程仍然未獲取到Class對象時,則拋出ClassNotFoundException。如類需要resolve,則調用resolveClass進行鏈接。
  • findClass。此方法直接拋出ClassNotFoundException,因此需要通過覆蓋loadClass或此方法來以自定義的方式加載相應的類。
  • defineClass。此方法負責將二進制的字節碼轉換爲Class對象,這個方法對於自定義加載類而言非常重要,如二進制的字節碼的格式不符合JVM Class文件的格式,拋出ClassFormatError;如需要生成的類名和二進制字節碼中的不同,則拋出NoClassDefFoundError;如需要加載的class是受保護的、採用不同簽名的或類名是以java.開頭的,則拋出SecurityException;如需加載的class在此ClassLoader中已加載,則拋出LinkageError。

好,可能有人看了上面會疑惑,爲什麼上面說自定義classloader是需要重寫findClass而不是loadClass或者其他方法。這裏建議查看源碼。

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

可以看到,JDK已經在loadClass方法中幫我們實現了ClassLoader搜索類的判斷方法,當在loadClass方法中搜索不到類時,loadClass方法就會調用findClass方法來搜索類,所以我們只需重寫該方法即可。

看完這一大串可能有人還是不理解,ok,那我們現在就動手寫一個自己的ClassLoader,儘量包含上面三個方法。

自定義ClassLoader

先定義一個Person接口。

public interface Person {
    public void say();
}

再定一個高富帥類實現這個接口

public class HighRichHandsome implements Person {

    @Override
    public void say() {
        System.out.println("I don't care whether you are rich or not");
    }

}

好的,開胃菜結束,主角來了,MyClassLoader。

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;

public class MyClassLoader extends ClassLoader{
    /* 
     * 覆蓋了父類的findClass,實現自定義的classloader
     */
    @Override
    public Class<?> findClass(String name) {
        byte[] bt = loadClassData(name);
        return defineClass(name, bt, 0, bt.length);
    }

    private byte[] loadClassData(String className) {
        InputStream is = getClass().getClassLoader().getResourceAsStream(
                className.replace(".", "/") + ".class");
        ByteArrayOutputStream byteSt = new ByteArrayOutputStream();
        int len = 0;
        try {
            while ((len = is.read()) != -1) {
                byteSt.write(len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return byteSt.toByteArray();
    }
}

代碼很簡單,不解釋了,最後在測試類LoaderTest裏寫個測試方法。

/**
 * 父類classloader
 * @throws Exception
 */
private static void test2() throws Exception{
    MyClassLoader loader = new MyClassLoader();
    Class<?> c = loader.loadClass("com.alibaba.classload.HighRichHandsome");
    System.out.println("Loaded by :" + c.getClassLoader());

    Person p = (Person) c.newInstance();
    p.say();

    HighRichHandsome man = (HighRichHandsome) c.newInstance();
    man.say();    
}

main方法中調用這個方法即可。LoaderTest默認構造函數會設置AppClassLoaderparent, 測試時執行test2()方法會發現HighRichHandsome類是委託AppClassLoader加載的,所以AppClassLoader可以訪問到,不會出錯。

但是我們再想一下,如果我們直接加載,不委託給父類加載,會出現什麼情況?

/**
 * 自己的classloader加載
 * @throws Exception
 */
private static void test3() throws Exception{
    MyClassLoader loader = new MyClassLoader();
    Class<?> c = loader.findClass("com.alibaba.classload.HighRichHandsome");
    System.out.println("Loaded by :" + c.getClassLoader());

    Person p = (Person) c.newInstance();
    p.say();

    //註釋下面兩行則不報錯
    HighRichHandsome man = (HighRichHandsome) c.newInstance();
    man.say();    
}

可以看到,悲劇的報錯了。根據class loader命名空間規則,每個class loader都有自己唯一的命名空間,每個class loader 只能訪問自己命名空間中的類,如果一個類是委託parent加載的,那麼加載後,這個類就類似共享的,parent和child都可以訪問到這個類,因爲parent是不會委託child加載類的,所以child加載的類parent訪問不到。簡單來說,即子加載器的命名空間包含了parent加載的所有類,反過來則不成立。 本例中LoaderTest類是AppClassLoader加載的,所以其看不見由MyClassLoader加載的HighRichHandsome類,但Person接口是可以訪問的,所以賦給Person類型不會出錯。

一些小的知識點

1.Class.forName()

相信大家都寫過連接數據庫的例子,基本上就是加載驅動,建立連接,創建請求,寫prepareStatement,關閉連接之類的。在這裏,有一段代碼:

public DbTest() {
    try {
        Class.forName("com.mysql.jdbc.Driver");// 加載驅動
        conn = DriverManager.getConnection(url, "root", "");// 建立連接
        stm = conn.createStatement(); // 創建請求
    } catch (Exception e) {
        e.printStackTrace();
    }
}

我相信大家一開始的時候肯定都有些疑惑,就是Class.forName(“com.mysql.jdbc.Driver”),爲什麼加載驅動是Class.forName,而不是ClassLoader的loadClass?爲什麼這麼寫就可以加載驅動了呢?

其實Class.forName()是顯示加載類,作用是要求JVM查找並加載指定的類,也就是說JVM會執行該類的靜態代碼段。查看com.mysql.jdbc.Driver源碼可以發現裏面有個靜態代碼塊,在加載後,類裏面的靜態代碼塊就執行(主要目的是註冊驅動,把自己註冊進去),所以主要目的就是爲了觸發這個靜態方法。

2.Web容器加載機制

其實web容器的加載機制這一點我說不好,因爲沒看過諸如tomcat之類的源碼,但是根據自己使用感覺以及查了相關資料,可以簡單介紹一下。一般服務器端都要求類加載器能夠反轉委派原則,也就是先加載本地的類,如果加載不到,再到parent中加載。這個得細看,我這裏只知道這個概念,所以就不說了。JavaEE規範推薦每個模塊的類加載器先加載本類加載的內容,如果加載不到纔回到parent類加載器中嘗試加載。

3.重複加載與回收

一個class可以被不同的class loader重複加載,但同一個class只能被同一個class loader加載一次。見下列代碼:

/**
 * 對象只加載一次,返回true
 */
private static void test4() {
    ClassLoader c1 = LoaderTest.class.getClassLoader();
    LoaderTest loadtest = new LoaderTest();
    ClassLoader c2 = loadtest.getClass().getClassLoader();
    System.out.println("c1.equals(c2):"+c1.equals(c2));
}

類的回收。說到最開始的why,其實java的回收機制我前面有篇博客說的比較詳細,這裏就插一句廢話吧,當某個classloader加載的所有類實例化的所有對象都被回收了,則該classloader會被回收。

參考:http://blog.csdn.net/xyang81/article/details/7292380

ps:話說csdn的markdown寫出來的效果實在不敢恭維,小小的吐槽一下。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章