Java虛擬機器中的類載入有三大步驟:,連結,初始化.其中載入是指查詢位元組流(也就是由Java編譯器生成的class檔案)並據此建立類的過程,這中間我們需要藉助類載入器來查詢位元組流.
Java虛擬機器預設類載入器
Java虛擬機器提供了3種類載入器,啟動(Bootstrap)類載入器、擴充套件(Extension)類載入器、應用(Application)類載入器.除了啟動類載入器外,其他的類載入器都是java.lang.ClassLoader的子類.啟動類載入器由C++語言實現,沒有對應的Java物件,它負責將 <JAVA_HOME>/lib路徑下的核心類庫或-Xbootclasspath引數指定的路徑下的jar包載入到記憶體中.擴充套件類載入器是指sun.misc.Launcher$ExtClassLoader類,由Java語言實現,是Launcher的靜態內部類,它負責載入<JAVA_HOME>/lib/ext目錄下或者由系統變數-Djava.ext.dir指定位路徑中的類庫,他的父類載入器是null.應用類載入器是指sun.misc.Launcher$AppClassLoader類,他負責載入應用程式路徑下的類,這裡路徑指java -classpath或-D java.class.path 指定的路徑,他的父類載入器是擴充套件類載入器.
注意這裡面的父子類載入器並不是繼承的關係,只是ClassLoader類中的parent屬性.我們來看Launcher類中建立擴充套件類載入器的程式碼:
public ExtClassLoader(File[] var1) throws IOException {
super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
}
這裡設定了其父載入器為null.
雙親委派機制
Java虛擬機器在載入類時預設採用的是雙親委派機制,即當一個類載入器接收到載入請求時,會將請求轉發到父類載入器,如果父類載入器在路徑下沒有找到該類,才會交給子類載入器去載入.我們來看ClassLoader中laodClass方法:
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) {
//有父類載入器,呼叫父載入器的loadClass
c = parent.loadClass(name, false);
} else {
//呼叫Bootstrap Classloader
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
long t1 = System.nanoTime();
//到自己指定類載入路徑下查詢是否有class位元組碼
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;
}
}
通過這種層級我們可以避免類的重複載入,當父親已經載入了該類時,就沒有必要子類載入器再載入一次。其次也考慮到安全因素,比如我們自己寫一個java.lang.String的類,通過雙親委派機制傳遞到啟動類載入器,而啟動類載入器在核心Java API發現這個名字的類,發現該類已被載入,並不會重新載入我們新寫的java.lang.String,而直接返回已載入過的String.class,這樣保證生成的物件是同一種型別.
自定義類載入器
除了jvm自身提供的類載入器,我們還可以自定義類載入器,我們先寫一個Person類
public class Person {
private int age;
private String name;
//省略getter/setter方法
}
我們先看他是由哪個類載入器載入的.
public class TestJava {
public static void main(String[] args) throws Exception {
Person person = new Person();
System.out.println("person是由" + person.getClass().getClassLoader() + "載入的");
}
}
執行結果如下:
我們把Person.class放置在其他目錄下
再執行會發生什麼,在上面的loadClass方法中其實已經有了答案,會丟擲ClassNotFoundException,因為在指定路徑下查詢不到位元組碼.
我們現在寫一個自定義的類載入器,讓他能夠去載入person類,很簡單,我們只需要繼承ClassLoader並重寫findClass方法,這裡面寫查詢位元組碼的邏輯.
public class PersonCustomClassLoader extends ClassLoader {
private String classPath;
public PersonCustomClassLoader(String classPath) {
this.classPath = classPath;
}
private byte[] loadByte(String name) throws Exception {
name = name.replaceAll("\\.", "/");
FileInputStream fis = new FileInputStream(classPath + "/" + name
+ ".class");
int len = fis.available();
byte[] data = new byte[len];
fis.read(data);
fis.close();
return data;
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] data = loadByte(name);
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
}
我們來測試一下:
public class TestJava {
public static void main(String[] args) throws Exception {
PersonCustomClassLoader classLoader = new PersonCustomClassLoader("/home/shenxinjian");
Class<?> pClass = classLoader.loadClass("me.shenxinjian.algorithm.Person");
System.out.println("person是由" + pClass.getClassLoader() + "類載入器載入的");
}
}
測試結果如下:
編寫自定義類載入器的意義
- 當class檔案不在classPath路徑下,如上面那種情況,預設系統類載入器無法找到該class檔案,在這種情況下我們需要實現一個自定義的classLoader來載入特定路徑下的class檔案來生成class物件。
- 當一個class檔案是通過網路傳輸並且可能會進行相應的加密操作時,需要先對class檔案進行相應的解密後再載入到JVM記憶體中,這種情況下也需要編寫自定義的ClassLoader並實現相應的邏輯