JVM類載入器與雙親委派模型(一)
阿新 • • 發佈:2018-12-05
(1)動態載入
類載入器是 Java 語言的一個創新,也是 Java 語言流行的重要原因之一。它使得 Java 類可以被動態載入到 Java 虛擬機器中並執行。類載入器從 JDK 1.0 就出現了,最初是為了滿足 Java Applet 的需要而開發出來的。Java Applet 需要從遠端下載 Java 類檔案到瀏覽器中並執行。 這段話是從IBM網站上摘抄的一段,核心意思是說,Java語言可以動態的把需要的程式碼(類)從其他地方(硬碟或網路)載入到記憶體,然後使用。與之相對的,是傳統的C語言程式,在生成可執行檔案的時候,會把所有相關的庫檔案寫進目的碼程式碼中,這會導致可執行檔案異常的大,執行時佔用的記憶體很多。(2)類載入器體系 類載入器的主要任務就是從磁碟(其實也可以是其他地方,比如網路)中根據類的全限定名稱定位到class檔案,以二進位制流的方式將這個檔案讀入到記憶體,並將其轉化為JVM規定的資料結構儲存在方法區。但是方法區的類資料無法直接訪問,所以還需要將其包裝成一個Class物件,作為外界訪問的入口。在HotSpot的實現裡,Class物件由於其特殊性並沒有存放在堆中,而是放在方法區。
package java.lang;
public class Object {
static {
System.out.println("hello");
}
public static void main(String[] args) {
new Object();
}
}
然後執行一下:
java java java.lang.Object
錯誤: 在類 java.lang.String 中找不到 main 方法, 請將 main 方法定義為:
public static void main(String[] args)
否則 JavaFX 應用程式類必須擴充套件javafx.application.Application
出現這種情況,是因為jvm執行的Object類是rt.jar中的那個,而不是自己寫的。類載入器的工作原理就是,載入一個類時,先根據其全名去自己的快取中查詢,找到之後直接使用;沒有找到才去載入,然後快取。而Object類做為java的核心基礎類,在jvm啟動的時候就已經被載入到記憶體了,所以後來所有名為java.lang.Object的類都不會被載入到記憶體。可以使用-XX:+TraceClassLoading引數驗證一下:
java -XX:+TraceClassLoading java.lang.Object
[Opened /Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.lang.Object from /Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.io.Serializable from /Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/rt.jar]
…
錯誤: 在類 java.lang.Object 中找不到 main 方法, 請將 main 方法定義為:
public static void main(String[] args)
否則 JavaFX 應用程式類必須擴充套件javafx.application.Application
[Loaded java.lang.Shutdown from /Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.lang.Shutdown$Lock from /Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/rt.jar]
從日誌中可以看到,jvm首先打開了rt.jar,然後第一個載入的就是java.lang.Object。所以我們可以得出結論:對於同名的類,只有先載入的那個才會生效;後來的根本沒機會載入進記憶體。
JVM雖然是所謂“按需載入”,但是對於一些核心的類,在啟動之初就必須載入進記憶體。因為只有這樣才能保證記憶體中的java核心類是真的,而不是使用者自己編寫的。
然而,剛才的需求是讓記憶體中存在兩個同名的類,只不過二者的版本或者實現不同。當原始設計和後續需求出現矛盾的時候,只能引入多個類載入器了。也就是說,雖然類名一樣,但是如果是被不同的類載入器載入到記憶體的,那麼JVM將會視其二者為兩個不同的類。也就是說,JVM通過擴充套件類載入器體系的方式解決了這個矛盾,同時又沒有破壞之前的載入機制——同一個類載入器只會載入一個同名類。
(5)Java類的相等性 根據前面的論述,決定兩個類(class物件)是否相同的因素,包括該類的全限定名以及類載入器。同一個class檔案被不同的類載入器載入到記憶體中,那麼equals方法的返回值仍然是false。 所以,任意一個類,都是由class檔案本身和類載入器共同確定其在jvm中的唯一性的。每個類載入器都有自己獨立的類名稱空間。
(6)java.lang.ClassLoader Java通過擴充套件“類載入器”體系解決了記憶體中無法存在同名類的問題。在這個體系中,首先是C++實現的啟動類載入器,它負責將 <JAVA_HOME>/lib路徑下的核心類庫或-Xbootclasspath引數指定的路徑下的jar包載入到記憶體中。由於虛擬機器是按照檔名識別載入jar包的,如rt.jar,如果檔名不被虛擬機器識別,即使把jar包放在<JAVA_HOME>/lib目錄下也是沒有作用的。至於虛擬機器能識別的檔名有哪些,我也不知道。另外,即使是rt.jar,也不是裡面全部的類會被載入,從上面的類記載日誌中可以看到,比如包名開頭為“javax”的那些類就沒有載入。到底會載入哪些哪些類,只有去看官方文件了,甚至不同的jvm實現載入的類也不一樣。 其餘的類載入器都是使用java語言實現的,它們有一個共同的父類就是java.lang.ClassLoader。父類中實現了大多數核心方法:
public abstract class ClassLoader {...}
這是一個抽象類,無法直接建立物件,繼承該類的子類才可以建立物件。
首先是最核心的loadClass(String name)方法,從名稱上就可以看出來,這個方法就是用來載入類的,類載入動作也確實是從呼叫這個方法開始。程式碼如下:
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
它呼叫了過載的同名方法,並且第二個引數設為false,過載的方法程式碼如下:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
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;
}
}
真正幹活的是這個方法,但是它的訪問修飾符是protected,也就是隻有同包的類和子類才可以訪問。方法內部的邏輯和註釋已經寫的非常清楚了,流程就是先呼叫findLoadedClass()方法去自己的載入快取中查詢;若沒有找到則委託給父“類載入器”的
loadClass()方法去載入,父“類載入器”執行同樣的流程;若仍然沒有找到,則繼續呼叫爺爺“類載入器”的這個方法;直到parent的值為null,也就是沒有父“類載入器”了。
java實現的類載入器都是ClassLoader類的子類,從而都有一個parent欄位代表父“類載入器”;當這個欄位被設為null時,表明它的父“類載入器”是啟動類載入器,因為啟動類載入器是C++實現的,java程式碼無法直接引用,所以用null代替。於是loadClass方法的程式碼中有了下面的分支:
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
當parent == null時,不再遞迴呼叫parent.loadClass方法,而是直接呼叫findBootstrapClassOrNull,後者最終呼叫瞭如下方法:
private native Class findBootstrapClass(String name);
可以看出這是一個本地方法,也就是啟動類載入器中的C++程式碼。我們無法檢視這個方法的程式碼,但是可以猜測,應該也是先去快取中查詢;沒有的話再去自己的路徑中查詢並載入,若還是沒有,那就只好返回null了。注意,啟動類載入器查詢類的時候並不是滿世界亂找,而是限定了一個範圍或路徑,只要在這個範圍內沒有找到,就返回null;而這個範圍,前面已經說過了。
我突然明白,這個範圍限定機制也是類載入器體系的一部分。試想一下,假如沒有這個範圍限制,一是效率很定會下降;二是和子類劃分清楚勢力範圍,各司其職,不然的話父“類載入器”可以載入所有的類,還走這麼多層遞迴呼叫幹嘛,還要子“類載入器”幹嘛?
然後,如果快取中沒有找到,父“類載入器”也沒有找到,那就只好自己動手了,也就是呼叫findClass方法,其程式碼如下:
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
what?居然什麼都不幹直接拋異常?其實,這正是子類可以介入的地方,只需要覆蓋了這個方法,那麼子類的物件最後呼叫的將是自己的findClass方法,而不是這個。JDK1.2之前,常用的套路是子類直接覆蓋loadClass方法;之後,Java已經不建議直接覆蓋loadClass方法,而是覆蓋findClass方法,將自己的查詢邏輯封裝在這個方法中。
總結一下,所謂雙親委派模型就是,當一個類載入器載入某個類的時候,如果自己快取中沒有,那麼它不會立即從自己的載入路徑中載入這個類;相反,它會先委派給父“類載入器”去載入這個類,父“類載入器”遵循同樣的邏輯載入該類;當所有的父“類載入器”都沒有找到這個類時,最底層的類載入器才去嘗試自己載入。 保證這個機制正常執行的關鍵就是子“類載入器”繼承java.lang.ClassLoader類,並且沒有覆蓋其中作為類載入動作入口的loadClass方法。