1. 程式人生 > >JVM類載入器與雙親委派模型(一)

JVM類載入器與雙親委派模型(一)

(1)動態載入

      類載入器是 Java 語言的一個創新,也是 Java 語言流行的重要原因之一。它使得 Java 類可以被動態載入到 Java 虛擬機器中並執行。類載入器從 JDK 1.0 就出現了,最初是為了滿足 Java Applet 的需要而開發出來的。Java Applet 需要從遠端下載 Java 類檔案到瀏覽器中並執行。 這段話是從IBM網站上摘抄的一段,核心意思是說,Java語言可以動態的把需要的程式碼(類)從其他地方(硬碟或網路)載入到記憶體,然後使用。與之相對的,是傳統的C語言程式,在生成可執行檔案的時候,會把所有相關的庫檔案寫進目的碼程式碼中,這會導致可執行檔案異常的大,執行時佔用的記憶體很多。
那麼Java程式呢?也許靜態檔案(class檔案)很大,但是開始執行時只是只是其中一部分程式碼,其他程式碼沒有用到的時候不會載入進記憶體;而載入進記憶體的程式碼,不再使用的時候也可以被回收(垃圾回收機制)。總體下來,Java程式在執行時,似乎總是沒有使用全部的程式程式碼。這麼做的好處主要有兩個:節省記憶體;可以動態擴充套件。

(2)類載入器體系       類載入器的主要任務就是從磁碟(其實也可以是其他地方,比如網路)中根據類的全限定名稱定位到class檔案,以二進位制流的方式將這個檔案讀入到記憶體,並將其轉化為JVM規定的資料結構儲存在方法區。但是方法區的類資料無法直接訪問,所以還需要將其包裝成一個Class物件,作為外界訪問的入口。在HotSpot的實現裡,Class物件由於其特殊性並沒有存放在堆中,而是放在方法區。
           本質上,類載入器應該只有兩種,那就是JVM本身的由C++語言實現的類載入器,也稱為啟動類載入器;以及由Java語言本身實現的類載入器。關於後者,其實有個“自舉”的問題,也就是“先有雞還是先有蛋”:Java的類載入器也是一個類,它想要載入別的類,首先得把自己載入到記憶體中,問題是誰來載入呢?所以啟動類載入器是必須的,也就是藉助外力把自己載入到記憶體。從這裡可以看出來,java語言是無法自舉的。 但是從程式設計的角度將,可以細分為四類:首先是前面的啟動類載入器,由C++實現;然後是擴充套件類載入器,由Java實現,位於${JAVA_HOME}/rt.jar/sun/misc的Launcher類中,它是一個內部類,叫ExtClassLoader;接著是應用程式類載入器,也稱為系統類載入器,同ExtClassLoader,位於
Launcher類中,叫AppClassLoader;剩下的就是自定義的類載入器了。
    (4)問題       為什麼JVM要設計這麼多類載入器呢?所有的類都由啟動類載入器來載入有啥不好?       考慮這麼一個場景:要執行兩個java程式,它們都使用了某個類,然而是不同的版本。這時候,jvm能把依賴的這個類的兩個版本都載入到記憶體嗎? 做個實驗就好,現在我自己寫一個java.lang.Objcet類,如下:
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方法。