Java原始碼分析——Class類、ClassLoader類解析(三) 類載入器、實現自定義類載入器
在這個系列的第一篇章就講解了Class類的獲取以及載入過程,但是並沒有提及具體的載入過程,在java中,載入一個類是通過ClassLoader類來執行的,也就是類載入器完成。java中所有的類,都必須載入進jvm中才能執行,這個載入的意思是指將.class檔案載入進jvm中,返回一個Class類物件的過程。
在官方的定義中,類載入器的定義如下:
類載入器(class loader)是一個負責載入JAVA類(classes)的物件,ClassLoader類是一個抽象類,需要給出類的二進位制名稱,class loader嘗試定位或者產生一個class的資料,一個典型的策略是把二進位制名字轉換成檔名然後到檔案系統中找到該檔案。
接下來講解ClassLoader類裡幾個重要的方法,在類的抽象與獲取裡,簡單的講解了loadClass方法與forName方法的異同,現在來詳細的對它分析,它的原始碼如下:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
//同步,根據類名
synchronized (getClassLoadingLock(name)) {
// 檢查是否已經被載入完
Class<?> c = findLoadedClass(name);
//該類未被載入
if (c == null) {
//記錄開始時間
long t0 = System.nanoTime();
try {
//如果父載入器不為空,呼叫父載入器
if (parent != null) {
c = parent. loadClass(name, false);
} else {
//否則,呼叫jvm內建載入器載入
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//沒找到。丟擲異常
}
//兩者都沒找到
if (c == null) {
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;
}
}
在方法的首先,便呼叫getClassLoadingLock方法來獲取該類的鎖,實現同步,而讓該方法執行緒是安全的,接著呼叫findLoadedClass方法來判斷該類是否被載入過, 如載入過則直接返回該Class類物件。接著會判定該載入器的父載入器是否有,如果有則呼叫父載入器載入,沒有則直接呼叫jvm中的內建載入器載入。接著是判斷是否需要連線與該類相關的類,也就是resolveClass方法,該方法的作用是載入的不指定的類以及該類引用的所有其他類時。
而另外一個比較重要的方法是defineClass方法,該方法的作用是把.class檔案中的二進位制流資料載入進記憶體的,並返回Class類物件。也就是載入的實質性操作是這個方法在進行的,其實現都是在jvm中,也就是說是native方法,java提供了三種defineClass方法,其最後一種是採用了NIO流:
private native Class<?> defineClass0(String name, byte[] b, int off, int len,
ProtectionDomain pd);
private native Class<?> defineClass1(String name, byte[] b, int off, int len,
ProtectionDomain pd, String source);
private native Class<?> defineClass2(String name, java.nio.ByteBuffer b,
int off, int len, ProtectionDomain pd,
String source);
name是指定的類的完全限定名,b指的是該類的.class檔案的二進位制流,off指的是開始載入前的指標偏移量,len指的是.class檔案的資料大小,source指的是載入的資源,pd指的是許可權的檢查。
在java程式中,類有三種,系統類、擴充套件類、程式設計師自定義的類,所以類載入器也有對應的三種:
- BootstrapClassLoader載入器:該載入器是最頂層的載入器,在jvm中定義且執行,它負責載入%JRE_HOME%\lib目錄下rt.jar、resources.jar、charsets.jar等包中的類。
- Extention ClassLoader載入器:該載入器是載入拓展的類,載入%JRE_HOME%\lib\ext目錄下的類。
- Appclass Loader載入器:也稱為SystemAppClass載入器,載入當前應用的CLASSPATH目錄下的所有類。
那我們自定義的類由誰載入呢?是由AppclassLoader載入器載入的,因為我們自定義的類就位於CLASSPATH目錄下,而其他兩種類載入器是主要載入jre有關的類的。有了三種類載入器,那麼自然的就有了一個順序來執行不同的類載入器載入類,因為程式並不會識別你是哪一種類,最簡潔的方式就是都試一遍。在sun.misc.Launcher類裡面,也就是jvm的入口裡:
private static String bootClassPath =System.getProperty("sun.boot.class.path");
public static Launcher getLauncher() {
return launcher;
}
public Launcher() {
Launcher.ExtClassLoader var1;
try {
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
......
}
從程式碼中可以看出,首先一個靜態的方法獲取了BootstrapClassLoader載入器,接著獲取ExtClassLoader載入器,最後才獲取AppClassLoader載入器。也就是說BootstrapClassLoader載入器是最先被執行的,最後執行的是AppClassLoader載入器。
它們之間的關係可以用如下圖展示:
因為BootstrapClassLoader載入器是在jvm中定義的,所以這裡不展示了,下面展示一下獲取類的類載入器程式碼:
class Kt{
}
public class Test {
public static void main(String args[]) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
System.out.println("系統類載入器:");
System.out.println(ClassLoader.getSystemClassLoader());
System.out.println("自定義類載入器載入器:");
System.out.println(Kt.class.getClassLoader());
System.out.println("自定義類載入器的父載入器:");
System.out.println(Kt.class.getClassLoader().getParent());
System.out.println("自定義類載入器的父載入器的父載入器:");
System.out.println(Kt.class.getClassLoader().getParent().getParent());
}
}
從結果看出,自定義的類以及系統預設的載入器是AppClassLoader載入器,但是在這裡有個困惑,怎麼AppClassLoader載入器的父載入器是ExtClassLoader載入器呢?明明兩者都是直接在上圖中沒有繼承關係的。因為父載入器不是父類,在類載入器的協調中使用了委託模式。什麼意思呢?就是AppClassLoader載入器的父載入器只是它的委託者,當AppClassLoader載入器發現這個類不是它所載入的類的時候,就會委託給它的父載入器ExtClassLoader載入器,當ExtClassLoader載入器假如也發現這個類不是它所載入的類的時候,也會委託給它的父載入器,但是從上面的例子程式碼中可以看到,ExtClassLoader載入器的父載入器是null,但是別忘了BootstrapClassLoader載入器,它就是ExtClassLoader載入器的父載入器,因為BootstrapClassLoader載入器是有jvm來定義執行的,所以ExtClassLoader載入器的父載入器會顯示為空。當這三者都沒找到對應類的.class檔案時,就會丟擲對應.class找不到異常。也就是說,每個載入器都有父載入器。一個ClassLoader建立時如果沒有指定parent,那麼它的parent預設就是AppClassLoader。這種委託模式在loadClass方法就有體現:
那麼如何自定義一個類載入器呢?其實很簡單,繼承ClassLoader類,然後只要重寫findClass方法,並在findClass方法裡呼叫defineClass方法就可以了。在討論loadClass方法時,指出findClass就是個在所有載入器找不到的情況下才呼叫它,且丟擲異常的方法,這裡找不到的情況是指java把指定的幾個檔案下找了一遍都沒找到的情況。所以我們可以自定義一個類的存放檔案目錄,然後在findClass方法裡指定我們自定義的類存放資料夾,然後呼叫definClass來獲取Class類物件並建立例項。
- 建立一個測試類,並編譯形成.class檔案,將.class檔案放在自定義的目錄下,這裡我放在:
//目錄:/home/image
//測試類
public class Cat {
public void print(){
System.out.println("已經被呼叫啦");
}
}
- 建立一個類繼承ClassLoader並重寫findClass:
public class MyClassLoader extends ClassLoader{
private String root;
public MyClassLoader(String root){
this.root=root;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String filename=getFileName(name);
File file = new File(root,filename);
try {
//將資料從.class檔案中讀出來並存入byte陣列
FileInputStream is = new FileInputStream(file);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
int len = 0;
try {
while ((len = is.read()) != -1) {
bos.write(len);
}
} catch (IOException e) {
e.printStackTrace();
}
byte[] data = bos.toByteArray();
is.close();
bos.close();
//呼叫defineClass方法載入.class檔案
return defineClass(name,data,0,data.length);
} catch (IOException e) {
e.printStackTrace();
}
//失敗呼叫父類findClass方法
return super.findClass(name);
}
//獲取.class名
private String getFileName(String name) {
int index = name.lastIndexOf('.');
if(index == -1){
return name+".class";
}else{
return name.substring(index+1)+".class";
}
}
}
- 用反射的方式呼叫對應的方法測試:
public class Test {
public static void main(String args[]) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
MyClassLoader classLoader=new MyClassLoader("/home/image");
//這裡依舊需要載入類的相對路徑
Class cat=classLoader.loadClass("test.Cat");
Cat test=(Cat) cat.newInstance();
test.print();
}
}
//列印輸出為:已經被呼叫啦
可見確實成功的呼叫了。