Android解析ClassLoader(一)Java中的ClassLoader

相關文章
Java虛擬機系列html

前言

熱修復和插件化是目前比較熱門的技術,要想更好的掌握它們須要瞭解ClassLoader,所以也就有了本系列的產生,這一篇咱們先來學習Java中的ClassLoader。
java

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加載目錄:bash

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文件。 jvm

1.4 Custom ClassLoader

除了系統提供的類加載器,還能夠自定義類加載器,自定義類加載器經過繼承java.lang.ClassLoader類的方式來實現本身的類加載器,除了 Bootstrap ClassLoader,Extensions ClassLoader和App ClassLoader也繼承了java.lang.ClassLoader類。關於自定義類加載器後面會進行介紹。ide

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相關原創技術乾貨盡在公衆號:劉望舒。

相關文章
相關標籤/搜索