相關文章
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檔案步驟如下:
- 自定義類載入器首先從快取中要查詢Class檔案是否已經載入,如果已經載入就返回該Class,如果沒載入則委託給父載入器也就是App ClassLoader。
- 按照上圖中紅色虛線的方向遞迴步驟1。
- 一直委託到Bootstrap ClassLoader,如果Bootstrap ClassLoader在快取中還沒有查詢到Class檔案,則在自己的規定路徑$JAVA_HOME/jre/libr中或者-Xbootclasspath選項指定路徑的jar包中進行查詢,如果找到則返回該Class,如果沒有則交給子載入器Extensions ClassLoader。
- Extensions ClassLoader查詢$JAVA_HOME/jre/lib/ext目錄下或者-Djava.ext.dirs選項指定目錄下的jar包,如果找到就返回,找不到則交給App ClassLoader。
- 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 雙親委託模式的好處
採取雙親委託模式主要有兩點好處:
- 避免重複載入,如果已經載入過一次Class,就不需要再次載入,而是先從快取中直接讀取。
- 更加安全,如果不使用雙親委託模式,就可以自定義一個String類來替代系統的String類,這顯然會造成安全隱患,採用雙親委託模式會使得系統的String類在Java虛擬機器啟動時就被載入,也就無法自定義String類來替代系統的String類,除非我們修改
類載入器搜尋類的預設演算法。還有一點,只有兩個類名一致並且被同一個類載入器載入的類,Java虛擬機器才會認為它們是同一個類,想要騙過Java虛擬機器顯然不會那麼容易。
4.自定義ClassLoader
系統提供的類載入器只能夠載入指定目錄下的jar包和Class檔案,如果想要載入網路上的或者是D盤某一檔案中的jar包和Class檔案則需要自定義ClassLoader。
實現自定義ClassLoader需要兩個步驟:
- 定義一個自定義ClassLoade並繼承抽象類ClassLoader。
- 複寫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相關原創技術乾貨盡在公眾號:劉望舒。