1. 程式人生 > >Java的自定義類載入器及JVM自帶的類載入器之間的互動關係

Java的自定義類載入器及JVM自帶的類載入器之間的互動關係

JVM自帶的類載入器:
這裡寫圖片描述
其關係如下:
這裡寫圖片描述
其中,類載入器在載入類的時候是使用了所謂的“父委託”機制。其中,除了根類載入器以外,其他的類載入器都有且只有一個父類載入器。
關於父委託機制的說明:
這裡寫圖片描述
當生成 一個自定義的類載入器例項時,如果沒有指定它的父載入器,那麼系統類載入器將成為該類載入器的父類載入器
下面,自定義類載入器。自定義的類載入器必須繼承java.lang.ClassLoader類

import java.io.*;
public class MyClassLoader extends ClassLoader {
    private String name;   //類載入器的名字
private String path; //載入類的路徑 private final String fileType = ".class"; //class檔案的副檔名 public MyClassLoader(String name){ super(); //讓系統類載入器成為該類載入器的父 類載入器,該句可省略不寫 this.name = name; } public MyClassLoader(ClassLoader parent,String name){ super(parent); //顯示指定該類載入器的父 類載入器
this.name = name; } @Override public String toString() { return this.name; } public String getPath() { return path; } public void setPath(String path) { this.path = path; } //實現自定義的類載入器必須重寫findClass方法,否則ClassLoader類中的findClass()方法是丟擲了異常
@Override public Class findClass(String name)throws ClassNotFoundException{ byte[] data = this.loadClassData(name); return this.defineClass(name,data,0,data.length); } private byte[] loadClassData(String name){ InputStream is = null; byte[] data = null; ByteArrayOutputStream baos = null; try { this.name = this.name.replace(".","\\"); //com.dream.it---->com\dream\it is = new FileInputStream(new File(path + name + fileType)); int ch; while(-1 != (ch = is.read())){ baos.write(ch); //將資料寫入到位元組陣列輸出流物件中去 } data = baos.toByteArray(); } catch (Exception e) { e.printStackTrace(); }finally { try { is.close(); baos.close(); } catch (IOException e) { e.printStackTrace(); } } return data; } public static void main(String[] args) throws Exception { MyClassLoader loader1 = new MyClassLoader("loader1"); loader1.setPath("d:/myapp/serverlib/"); MyClassLoader loader2 = new MyClassLoader(loader1,"loader2"); //loader1作為loader2的父 類載入器 loader2.setPath("d:/myapp/clientlib"); MyClassLoader loader3 = new MyClassLoader(null,"loader3");//父類載入器為null,表明其父類載入器為根類載入器 loader3.setPath("d:/myapp/otherlib"); test(loader2); test(loader3); } public static void test(ClassLoader cl) throws Exception { Class clazz = cl.loadClass("Sample"); Object object = clazz.newInstance(); } }

附上findClass()方法的JDK說明

protected Class<?> findClass(String name) throws ClassNotFoundException
Finds the class with the specified binary name. 
This method should be overridden by class loader
implementations that follow the delegation model 
for loading classes, and will be invoked by the 
loadClass method after checking the parent class
loader for the requested class. The default 
implementation throws a ClassNotFoundException. 
大致說明一下意思:通過指定的name來查詢類。該方法應該被類
載入器的實現類重寫,從而能夠保證在載入類的時候可以遵循委託
機制模型。在loadClass()方法(該方法是由JVM呼叫的)中,
檢查其父類載入器之後,該方法再被呼叫去載入請求的類。預設
該方法的實現是丟擲了一個ClassNotFoundException異常。

其實,所謂的載入類,無非就是讀取.class檔案到記憶體中,所以在findClass()方法中,loadClassData()方法用於讀取.class檔案的資料,並返回一個位元組陣列。然後利用ClassLoader類的defineClass()方法將位元組陣列轉換為Class物件。
上述自定義的類載入器loader1,loader2,loader3及JVM自帶的類載入器之間的關係如下:
這裡寫圖片描述
對於各個類載入器,系統的類載入器是從環境變數classpath中讀取.class檔案實現類的載入;loader1是從目錄d:/myapp/serverlib/下讀取.class檔案;loader2是從目錄d:/myapp/clientlib/下讀取.class檔案,loader3是從目錄d:/myapp/otherlib/下讀取.class檔案
執行結果:
這裡寫圖片描述
此處我們分析一下出現這種執行結果的原因:
當執行loader2.loadClass(“Sample”)時先由它上層的所有父類載入器嘗試載入Sample類。loader1從D:\myapp\serverliv目錄下成功載入了Sample類,所以loader1是Sample類的定義類載入器,loader1和loader2是Sample類的初始類載入器。
當執行loader3.loadClass(“Sample”)時,先由它上層的所有父類載入器嘗試載入Sample類。loader3的父載入器為根類載入器,它無法載入Sample類,接著loader3從D:\myapp\otherlib目錄下成功載入Sample類,所以loader3是Sample類的定義類載入器及初始類載入器。
在Sample類中主動使用了Dog類(new Dog()),當執行Sample類的構造方法中的new Dog()語句時,JVM需要先載入Dog類,到底用哪個類載入器家在呢?從上述的列印結果中可以看出,載入Sample類的loader1還載入了Dog類,JVM會用Sample類的定義類載入器去載入Dog類,載入過程中也同樣採用了父親委託機制。為了驗證這一點,可以吧D:\myapp\serverlib目錄下Dog.class檔案刪除,然後在D:\myapp\syslib目錄下存放一個Dog.class檔案,此時列印結果如下:

Sample:loader1
Dog:sun.misc.Launcher$AppClassLoader@1b84c92
Sample:loader3
Dog:loader3

由此可見,當由loader1載入的Sample類首次主動使用Dog類時,Dog類由系統類載入器載入,如果把D:\myapp\serverlib和D:\myapp\syslib目錄下的Dog.class檔案都刪除,然後在D:\myapp\client目錄下存放一個Dog.class檔案。此時檔案結構如下圖所示:
這裡寫圖片描述
當Loader1載入Sample類首次主動使用Dog類時,由於loader1及其父類載入器都無法載入Dog類,因此test(loader2)會丟擲ClassNotFoundExcption.
這又是因為什麼原因呢?
這又牽扯到名稱空間的問題。
同一個名稱空間內的類時相互可見的。
子載入器的名稱空間包含所有父類載入器的名稱空間,因此由子載入器載入的類能看見父類載入器載入的類。例如系統類載入器載入的類能看見根類載入器載入的類。由父載入器載入的類不能看見子載入器載入的類。
如果兩個載入器之間沒有直接或間接的父子關係,那麼它們各自載入的類相互不可見。
對於上述問題,loader1可以載入Sample類,而Dog類只能由loader2載入Dog類,loader1是Loader2的父類載入器,父載入器loader1載入的類Sample不能看見子載入器loader2載入的類Dog,所以會丟擲異常。


對於上述例項中的main方法,我們不呼叫test方法,換成如下程式碼
Class clazz = loader1.loadClass("Sample");
Object obj = clazz.newInstance();
Sample sample = (Sample)obj;
System.out.println(sample.v1);

MyClassLoader類由系統類載入器載入,而Sample類由loader1類載入器載入,所以MyClassLoader類看不見Sample類。在MyClassLoader類的main方法中使用Sample類,會導致NoClassFoundError錯誤。
當兩個不同名稱空間內的類相互不可見時,可採用Java反射機制來訪問物件例項的屬性和方法。
將上述程式碼修改:

Class clazz = loader1.loadClass("Sample");
Object obj = clazz.newInstance();
Field field = clazz.getField("v1");
int v1 = field.getInt(obj);
System.out.println(v1);

此時,可以獲取到物件中的v1屬性值。利用反射機制,我們可以跨越這種名稱空間的限制。
補充:
名稱空間:
這裡寫圖片描述
執行時包:
這裡寫圖片描述