相關文章
Java虛擬機系列html
熱修復和插件化是目前比較熱門的技術,要想更好的掌握它們須要瞭解ClassLoader,所以也就有了本系列的產生,這一篇咱們先來學習Java中的ClassLoader。
java
在Java虛擬機(一)結構原理與運行時數據區域這篇文章中,我提到過類加載子系統,它的主要做用就是經過多種類加載器(ClassLoader)來查找和加載Class文件到 Java 虛擬機中。
Java中的類加載器主要有兩種類型,系統類加載和自定義類加載器。其中系統類加載器包括3種,分別是Bootstrap ClassLoader、 Extensions ClassLoader和 App 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等等。安全
用於加載 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複製代碼
負責加載當前應用程序Classpath目錄下的全部jar和Class文件。也能夠加載經過-Djava.class.path選項所指定的目錄下的jar和Class文件。 jvm
除了系統提供的類加載器,還能夠自定義類加載器,自定義類加載器經過繼承java.lang.ClassLoader類的方式來實現本身的類加載器,除了 Bootstrap ClassLoader,Extensions ClassLoader和App ClassLoader也繼承了java.lang.ClassLoader類。關於自定義類加載器後面會進行介紹。ide
運行一個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相關類,下面簡單對它們進行介紹:
類加載器查找Class所採用的是雙親委託模式,所謂雙親委託模式就是首先判斷該Class是否已經加載,若是沒有則不是自身去查找而是委託給父加載器進行查找,這樣依次的進行遞歸,直到委託到最頂層的Bootstrap ClassLoader,若是Bootstrap ClassLoader找到了該Class,就會直接返回,若是沒找到,則繼續依次向下查找,若是還沒找到則最後會交由自身去查找。
這樣講可能會有些抽象,來看下面的圖。
咱們知道類加載子系統用來查找和加載Class文件到 Java 虛擬機中,假設咱們要加載一個位於D盤的Class文件,這時系統所提供的類加載器不能知足條件,這時就須要咱們自定義類加載器繼承自java.lang.ClassLoader,並複寫它的findClass方法。加載D盤的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方法繼續向下進行查找。
採起雙親委託模式主要有兩點好處:
系統提供的類加載器只可以加載指定目錄下的jar包和Class文件,若是想要加載網絡上的或者是D盤某一文件中的jar包和Class文件則須要自定義ClassLoader。
實現自定義ClassLoader須要兩個步驟:
下面咱們就自定義一個ClassLoader用來加載位於D:\lib的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。
接下來編寫自定義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原理