JVM初探(四):類載入器
一、概述
虛擬機器設計團隊把類載入階段中的“通過一個類的全限定名來獲取描述此類的二進位制位元組流”這個動作放到Java虛擬機器外部去實現,以便讓應用程式自己決定如何去獲取所需要的類。實現這個動作的程式碼模組稱為類載入器。
對於類載入,我們關注兩個方面的問題:
- JVM定義的三個類載入器(Bootstrap,Extension,System)
- 雙親委派模型(機制,JDBC違背案例)
二、類載入器
JVM使用以下三種類型的類載入器:
-
啟動類(Bootstrap)類載入器:
這個類存放在<JAVA_HOME>\lib目錄中,無法被Java程式直接引用;
-
擴充套件(Extension)類載入器
這個類載入器負責載入<JAVA_HOME>\lib\ext目錄中的所有類庫,開發者可以直接使用;
-
引用程式(Application)類載入器:
這個類載入器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也稱它為系統類載入器,負責載入使用者類路徑上所指定的類庫。開發者可以直接使用。
從java虛擬機器的角度來說,一種是啟動類載入器,是JVM的一部分;另一種是其他類載入器,獨立於JVM,全部繼承自抽象類java.lang.ClassLoader。只有其他類載入器程式設計師才能自己使用。
三、雙親委派模型
1.什麼是雙親委派模型
JVM在載入類時預設採用的是雙親委派機制。
通俗的講,就是某個特定的類載入器在接到載入類的請求時,首先將載入任務委託給父類載入器,然後父類載入器再委託給它的父類......以此類推,因此所有的載入請求最終都應該傳送到頂層的啟動類載入器中。
只有父類載入器無法完成載入請求時,才會使用子類載入器去載入。
雙親委派模型的一個重要作用是為了保證類載入過程的安全性:
- 假設有一個開發者自己編寫了一個名為
java.lang.Object
的類,想借此欺騙JVM。現在他要使用自定義ClassLoader
來載入自己編寫的java.lang.Object
類。- 但是無論哪一個類載入器要載入這個類,最終都會委派給啟動類載入器進行載入,啟動類載入器在其搜尋範圍內可以搜尋到的只有
rt.jar
中的java.lang.Object
類- 這樣就確保了Object類的唯一性,如果是String也是同理。
也因此,java判斷類是否是同一個類,就是通過判斷類的ClassLoader.class來判斷的,舉個簡單的例子:只要不是一個爹媽生出來的,哪怕長的一模一樣那也不是親兄弟。
2.類的載入順序
我們開啟ClassLoader
類,找到loadClass()
方法,可以在註釋上看到如何按順序搜尋類的:
* Invoke {@link #findLoadedClass(String)} to check if the class
* has already been loaded. </p></li>
*
* Invoke the {@link #loadClass(String) <tt>loadClass</tt>} method
* on the parent class loader. If the parent is <tt>null</tt> the class
* loader built-in to the virtual machine is used, instead. </p></li>
*
* Invoke the {@link #findClass(String)} method to find the
* class.
即:
- 使用
findLoadedClass()
方法檢查類是否已經載入; - 呼叫父類載入器的
loaderCalss()
方法。如果父類是null,就直接呼叫虛擬機器內建的Bootstrap載入; - 如果父類無法載入,使用自己
findClass()
載入。
我們再看一下loadClass()
方法的原始碼,更直觀的展示了雙親委派模型下是如何載入類的:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
//執行緒安全,保證只有一個執行緒能對類初始化
synchronized (getClassLoadingLock(name)) {
//1.檢查類是否已經被載入
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
//2.如果沒有被載入,判斷是否存在父類載入器
if (parent != null) {
//3.有就遞迴委派給父類載入器載入
c = parent.loadClass(name, false);
} else {
//4.否則就使用啟動類載入器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 如果到了啟動類載入器還找不到類,則丟擲ClassNotFoundException
}
//如果父類載入器無法載入,就使用自身的findClass載入
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;
}
}
這裡就是為什麼JDK在我們自定義類載入器的時候不推薦重寫loadClass()
只讓我們重寫findClass()
方法的原因了,因為如果重寫了方法,卻不遵守雙親委派模型,就有可能導致不可預料的後果。
當然,不推薦歸不推薦,如果有必要的話仍然可以重寫loadClass()
方法,比如JDBC、JNDI就打破了雙親委派模型。
3.三種類載入器的關係
我們執行一下以下程式碼:
public static void main( String[] args ) {
System.out.println("應用程式載入器:"+ClassLoader.getSystemClassLoader());
System.out.println("應用程式載入器的父類:"+ClassLoader.getSystemClassLoader().getParent());
System.out.println("擴充套件載入器的父類:"+ClassLoader.getSystemClassLoader().getParent().getParent());
}
//輸出結果
應用程式載入器:sun.misc.Launcher$AppClassLoader@18b4aac2
應用程式載入器的父類:sun.misc.Launcher$ExtClassLoader@29453f44
擴充套件載入器的父類:null
由於啟動類載入器是jvm內建的,我們無法直接呼叫,所以返回值是null。