Android解析ClassLoader(一)Java中的ClassLoader

劉望舒發表於2017-09-27

相關文章
Java虛擬機器系列

前言

熱修復和外掛化是目前比較熱門的技術,要想更好的掌握它們需要了解ClassLoader,因此也就有了本系列的產生,這一篇我們先來學習Java中的ClassLoader。

1.ClassLoader的型別

Java虛擬機器(一)結構原理與執行時資料區域這篇文章中,我提到過類載入子系統,它的主要作用就是通過多種類載入器(ClassLoader)來查詢和載入Class檔案到 Java 虛擬機器中。
Java中的類載入器主要有兩種型別,系統類載入和自定義類載入器。其中系統類載入器包括3種,分別是Bootstrap ClassLoader、 Extensions ClassLoader和 App ClassLoader。

1.1 Bootstrap ClassLoader

用C/C++程式碼實現的載入器,用於載入Java虛擬機器執行時所需要的系統類,如java.lang.*、java.uti.*等這些系統類,它們預設在$JAVA_HOME/jre/lib目錄中,也可以通過啟動Java虛擬機器時指定-Xbootclasspath選項,來改變Bootstrap ClassLoader的載入目錄。
Java虛擬機器的啟動就是通過 Bootstrap ClassLoader建立一個初始類來完成的。由於Bootstrap ClassLoader是使用C/C++語言實現的, 所以該載入器不能被Java程式碼訪問到。需要注意的是Bootstrap ClassLoader並不繼承java.lang.ClassLoader。
我們可以通過如下程式碼來得出Bootstrap ClassLoader所載入的目錄:

public class ClassLoaderTest {
    public static void main(String[]args) {
        System.out.println(System.getProperty("sun.boot.class.path"));
    }
}複製程式碼

列印結果為:

C:\Program Files\Java\jdk1.8.0_102\jre\lib\resources.jar;
C:\Program Files\Java\jdk1.8.0_102\jre\lib\rt.jar;
C:\Program Files\Java\jdk1.8.0_102\jre\lib\sunrsasign.jar;
C:\Program Files\Java\jdk1.8.0_102\jre\lib\jsse.jar;
C:\Program Files\Java\jdk1.8.0_102\jre\lib\jce.jar;
C:\Program Files\Java\jdk1.8.0_102\jre\lib\charsets.jar;
C:\Program Files\Java\jdk1.8.0_102\jre\lib\jfr.jar;
C:\Program Files\Java\jdk1.8.0_102\jre\classes複製程式碼

可以發現幾乎都是$JAVA_HOME/jre/lib目錄中的jar包,包括rt.jar、resources.jar和charsets.jar等等。

1.2 Extensions ClassLoader

用於載入 Java 的擴充類 ,擴充類的jar包一般會放在$JAVA_HOME/jre/lib/ext目錄下,用來提供除了系統類之外的額外功能。也可以通過-Djava.ext.dirs選項新增和修改Extensions ClassLoader載入的路徑。
通過以下程式碼可以得到Extensions ClassLoader載入目錄:

System.out.println(System.getProperty("java.ext.dirs"));複製程式碼

列印結果為:

C:\Program Files\Java\jdk1.8.0_102\jre\lib\ext;
C:\Windows\Sun\Java\lib\ext複製程式碼

1.3 App ClassLoader

負責載入當前應用程式Classpath目錄下的所有jar和Class檔案。也可以載入通過-Djava.class.path選項所指定的目錄下的jar和Class檔案。

1.4 Custom ClassLoader

除了系統提供的類載入器,還可以自定義類載入器,自定義類載入器通過繼承java.lang.ClassLoader類的方式來實現自己的類載入器,除了 Bootstrap ClassLoader,Extensions ClassLoader和App ClassLoader也繼承了java.lang.ClassLoader類。關於自定義類載入器後面會進行介紹。

2.ClassLoader的繼承關係

執行一個Java程式需要用到幾種型別的類載入器呢?如下所示。

public class ClassLoaderTest {
    public static void main(String[] args) {
        ClassLoader loader = ClassLoaderTest.class.getClassLoader();
        while (loader != null) {
            System.out.println(loader);//1
            loader = loader.getParent();
        }
    }
}複製程式碼

首先我們得到當前類ClassLoaderTest的類載入器,並在註釋1處列印出來,接著列印出當前類的類載入器的父載入器,直到沒有父載入器終止迴圈。列印結果如下所示。

sun.misc.Launcher$AppClassLoader@75b84c92
sun.misc.Launcher$ExtClassLoader@1b6d3586複製程式碼

第1行說明載入ClassLoaderTest的類載入器是AppClassLoader,第2行說明AppClassLoader的父載入器為ExtClassLoader。至於為何沒有列印出ExtClassLoader的父載入器Bootstrap ClassLoader,這是因為Bootstrap ClassLoader是由C/C++編寫的,並不是一個Java類,因此我們無法在Java程式碼中獲取它的引用。

我們知道系統所提供的類載入器有3種型別,但是系統提供的ClassLoader相關類卻不只3個。另外,AppClassLoader的父類載入器為ExtClassLoader,並不代表AppClassLoader繼承自ExtClassLoader,ClassLoader的繼承關係如下所示。

可以看到上圖中共有5個ClassLoader相關類,下面簡單對它們進行介紹:

  • ClassLoader是一個抽象類,其中定義了ClassLoader的主要功能。
  • SecureClassLoader繼承了抽象類ClassLoader,但SecureClassLoader並不是ClassLoader的實現類,而是擴充了ClassLoader類加入了許可權方面的功能,加強了ClassLoader的安全性。
  • URLClassLoader繼承自SecureClassLoader,用來通過URl路徑從jar檔案和資料夾中載入類和資源。
  • ExtClassLoader和AppClassLoader都繼承自URLClassLoader,它們都是Launcher 的內部類,Launcher 是Java虛擬機器的入口應用,ExtClassLoader和AppClassLoader都是在Launcher中進行初始化的。

3 雙親委託模式

3.1 雙親委託模式的特點

類載入器查詢Class所採用的是雙親委託模式,所謂雙親委託模式就是首先判斷該Class是否已經載入,如果沒有則不是自身去查詢而是委託給父載入器進行查詢,這樣依次的進行遞迴,直到委託到最頂層的Bootstrap ClassLoader,如果Bootstrap ClassLoader找到了該Class,就會直接返回,如果沒找到,則繼續依次向下查詢,如果還沒找到則最後會交由自身去查詢。
這樣講可能會有些抽象,來看下面的圖。

我們知道類載入子系統用來查詢和載入Class檔案到 Java 虛擬機器中,假設我們要載入一個位於D盤的Class檔案,這時系統所提供的類載入器不能滿足條件,這時就需要我們自定義類載入器繼承自java.lang.ClassLoader,並複寫它的findClass方法。載入D盤的Class檔案步驟如下:

  1. 自定義類載入器首先從快取中要查詢Class檔案是否已經載入,如果已經載入就返回該Class,如果沒載入則委託給父載入器也就是App ClassLoader。
  2. 按照上圖中紅色虛線的方向遞迴步驟1。
  3. 一直委託到Bootstrap ClassLoader,如果Bootstrap ClassLoader在快取中還沒有查詢到Class檔案,則在自己的規定路徑$JAVA_HOME/jre/libr中或者-Xbootclasspath選項指定路徑的jar包中進行查詢,如果找到則返回該Class,如果沒有則交給子載入器Extensions ClassLoader。
  4. Extensions ClassLoader查詢$JAVA_HOME/jre/lib/ext目錄下或者-Djava.ext.dirs選項指定目錄下的jar包,如果找到就返回,找不到則交給App ClassLoader。
  5. App ClassLoade查詢Classpath目錄下或者-Djava.ext.dirs選項所指定的目錄下的jar包和Class檔案,如果找到就返回,找不到交給我們自定義的類載入器,如果還找不到則丟擲異常。

總的來說就是Class檔案載入到類載入子系統後,先沿著圖中紅色虛線的方向自下而上進行委託,再沿著黑色虛線的方向自上而下進行查詢,整個過程就是先上後下。

類載入的步驟在JDK8的原始碼中也得到了體現,來檢視抽象類的ClassLoader方法,如下所示。

 protected Class<?> More ...loadClass(String name, boolean resolve)
         throws ClassNotFoundException
     {
         synchronized (getClassLoadingLock(name)) {
             Class<?> c = findLoadedClass(name);//1
             if (c == null) {
                 long t0 = System.nanoTime();
                 try {
                     if (parent != null) {
                         c = parent.loadClass(name, false);//2
                     } else {
                         c = findBootstrapClassOrNull(name);//3
                     }
                 } catch (ClassNotFoundException e) {            
                 }
                 if (c == null) {
                     // If still not found, then invoke findClass in order
                     // to find the class.
                     long t1 = System.nanoTime();
                     c = findClass(name);//4
                     // 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;
         }
     }複製程式碼

註釋1處用來檢查類是否已經載入,如果已經載入則後面的程式碼不會執行,最後會返回該類。沒有載入則會接著向下執行。
註釋2處,如果父類載入器不為null,則呼叫父類載入器的loadClass方法。如果父類載入器為null則呼叫註釋3處的findBootstrapClassOrNull方法,這個方法內部呼叫了Native方法findLoadedClass0,findLoadedClass0方法中最終會用Bootstrap Classloader來查詢類。如果Bootstrap Classloader仍沒有找到該類,也就說明向上委託沒有找到該類,則呼叫註釋4處的findClass方法繼續向下進行查詢。

3.2 雙親委託模式的好處

採取雙親委託模式主要有兩點好處:

  1. 避免重複載入,如果已經載入過一次Class,就不需要再次載入,而是先從快取中直接讀取。
  2. 更加安全,如果不使用雙親委託模式,就可以自定義一個String類來替代系統的String類,這顯然會造成安全隱患,採用雙親委託模式會使得系統的String類在Java虛擬機器啟動時就被載入,也就無法自定義String類來替代系統的String類,除非我們修改
    類載入器搜尋類的預設演算法。還有一點,只有兩個類名一致並且被同一個類載入器載入的類,Java虛擬機器才會認為它們是同一個類,想要騙過Java虛擬機器顯然不會那麼容易。

4.自定義ClassLoader

系統提供的類載入器只能夠載入指定目錄下的jar包和Class檔案,如果想要載入網路上的或者是D盤某一檔案中的jar包和Class檔案則需要自定義ClassLoader。
實現自定義ClassLoader需要兩個步驟:

  1. 定義一個自定義ClassLoade並繼承抽象類ClassLoader。
  2. 複寫findClass方法,並在findClass方法中呼叫defineClass方法。

下面我們就自定義一個ClassLoader用來載入位於D:\lib的Class檔案。

4.1 編寫測試Class檔案

首先編寫測試類並生成Class檔案,如下所示。

package com.example;
public class Jobs {
    public void say() {
        System.out.println("One more thing");
    }
}複製程式碼

將這個Jobs.java放入到D:\lib中,使用cmd命令進入D:\lib目錄中,執行Javac Jobs.java對該java檔案進行編譯,這時會在D:\lib中生成Jobs.class。

4.2 編寫自定義ClassLoader

接下來編寫自定義ClassLoader,如下所示。

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
public class DiskClassLoader extends ClassLoader {
    private String path;
    public DiskClassLoader(String path) {
        this.path = path;
    }
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        Class clazz = null;
        byte[] classData = loadClassData(name);//1
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            clazz= defineClass(name, classData, 0, classData.length);//2
        }
        return clazz;
    }
    private byte[] loadClassData(String name) {
        String fileName = getFileName(name);
        File file = new File(path,fileName);
        InputStream in=null;
        ByteArrayOutputStream out=null;
        try {
             in = new FileInputStream(file);
             out = new ByteArrayOutputStream();
            byte[] buffer = new byte[1024];
            int length=0;
            while ((length = in.read(buffer)) != -1) {
                out.write(buffer, 0, length);
            }
            return out.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                if(in!=null) {
                    in.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            try{
                if(out!=null) {
                    out.close();
                }
            }catch (IOException e){
                e.printStackTrace();
            }
        }
        return null;
    }
    private String getFileName(String name) {
        int index = name.lastIndexOf('.');
        if(index == -1){//如果沒有找到'.'則直接在末尾新增.class
            return name+".class";
        }else{
            return name.substring(index+1)+".class";
        }
    }
}複製程式碼

這段程式碼有幾點需要注意的,註釋1處的loadClassData方法會獲得class檔案的位元組碼陣列,並在註釋2處呼叫defineClass方法將class檔案的位元組碼陣列轉為Class類的例項。loadClassData方法中需要對流進行操作,關閉流的操作要放在finally語句塊中,並且要對in和out分別採用try語句,如果in和out共同在一個try語句中,那麼如果in.close()發生異常,則無法執行 out.close()

最後我們來驗證DiskClassLoader是否可用,程式碼如下所示。

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class ClassLoaderTest {
    public static void main(String[] args) {
        DiskClassLoader diskClassLoader = new DiskClassLoader("D:\\lib");//1
        try {
            Class c = diskClassLoader.loadClass("com.example.Jobs");//2
            if (c != null) {
                try {
                    Object obj = c.newInstance();
                    System.out.println(obj.getClass().getClassLoader());
                    Method method = c.getDeclaredMethod("say", null);
                    method.invoke(obj, null);//3
                } catch (InstantiationException | IllegalAccessException
                        | NoSuchMethodException
                        | SecurityException |
                        IllegalArgumentException |
                        InvocationTargetException e) {
                    e.printStackTrace();
                }
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}複製程式碼

註釋1出建立DiskClassLoader並傳入要載入類的路徑,註釋2處載入Class檔案,需要注意的是,不要在專案工程中存在名為com.example.Jobs的Java檔案,否則就不會使用DiskClassLoader來載入,而是AppClassLoader來負責載入,這樣我們定義DiskClassLoader就變得毫無意義。接下來在註釋3通過反射來呼叫Jobs的say方法,列印結果如下:

com.example.DiskClassLoader@4554617c
One more thing複製程式碼

使用了DiskClassLoader來載入Class檔案,say方法也正確執行,顯然我們的目的達到了。

後記

這一篇文章我們學習了Java中的ClassLoader,包括ClassLoader的型別、雙親委託模式、ClassLoader繼承關係以及自定義ClassLoader,為的是就是更好的理解下一篇所要講解的Android中的ClassLoader。

參考資料
一看你就懂,超詳細java中的ClassLoader詳解
深入分析Java ClassLoader原理


我的新書《Android進階之光》已出版,更多成體系的Android相關原創技術乾貨盡在公眾號:劉望舒。

相關文章