(四)Java 虛擬機是如何加載Java類的?

類的加載過程

類的整個加載過程從類字節流通過虛擬機的類加載器加載到內存供虛擬機使用,到垃圾收集器回收,其生命週期可分爲加載、鏈接、初始化、使用、卸載。其中鏈接可分爲驗證、解析、準備,如下:

  • 加載

加載是虛擬機藉助類加載器查找字節流並且創建類的過程,從設計圖(Class 結構文件)到產品實物(類)的過程。對於數組類來說,它並沒有對應的字節流,而是由 Java 虛擬機直接生成的。

  類加載器

類加載器好比工程師,負責產品從設計圖到加工生產實物產品的過程。一個工廠(虛擬機)中,工程師(類加載器)有內部既有(內置)的,也可以繼續外聘(自定義)。衆多工程師中,對於不同級別,負責不同的工作任務。虛擬機中內置的類加載器也同樣如此:負責加載 JAVA_HOME/jre/lib/ 中的類的加載器叫 Bootstrap ClassLoader ; 負責加載 JAVA_HOME/jre/lib/ext/ 中的類的加載器叫 Extension ClassLoader ; 負載加載項目 class path 中的類的加載器叫 Application ClassLoader ; 開發人員可以自定義類加載器,此處暫且叫它們 User ClassLoader  :

public static void main(String[] args) {

    System.out.println(System.getProperty("sun.boot.class.path"));
    System.out.println("\n");
    System.out.println(System.getProperty("java.ext.dirs"));
}

// Bootstrap ClassLoader 加載目錄:

/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/resources.jar

/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/rt.jar

/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/sunrsasign.jar

/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/jsse.jar

/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/jce.jar

/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/charsets.jar

/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/jfr.jar

/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/classes

 

// Extension ClassLoader 加載目錄:

/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/ext

 

類加載器搜索加載類時候,它在嘗試親自搜索某個類之前,先把這個任務委派給它的父類加載器,這個過程是由上至下依次檢查的,首先由最頂層的類加載器 Bootstrap ClassLoader 嘗試加載,如果沒加載到,則把任務委派給 Extension ClassLoader 嘗試加載,如果也沒加載到,則委派給 Application ClassLoader 嘗試進行加載,如果它也沒有加載得到的話,則返回給委託的發起者,由它到指定的文件系統或網絡等URL中加載該類。如果它們都沒有加載到這個類時,則拋出ClassNotFoundException異常。否則將這個找到的類生成一個類的定義,並將它加載到內存當中,最後返回這個類在內存中的Class實例對象。

雙親委派機制

類加載器使用的是雙親委託機制來搜索加載類的,每個類加載器實例都有一個父類加載器的引用(組合),虛擬機內置的類加載器(Bootstrap ClassLoader)本身沒有父類加載器,但可以用作其它類加載器實例的的父類加載器。

當一個類加載器實例需要加載某個類時:

  1. 判斷該類是否已經加載,如果已經加載,直接返回實例,否則繼續執行
  2. 判斷父類加載是否存在,如果存在,加載任務委派給父類加載器,否則繼續執行
  3. Boostrap ClassLoader 加載該類,如果加載成功,直接返回實例,否則繼續執行
  4. 執行 findClass 方法加載該類,如果都沒加載到該類,着拋出 ClassNotFoundException 
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;
        }
    }

在 Java 虛擬機中,類的唯一性是由類加載器實例以及類的全名一同確定的。即便是同一串字節流,經由不同的類加載器加載,也會得到兩個不同的類。在大型應用中,我們往往藉助這一特性,來運行同一個類的不同版本。

 

  • 鏈接

鏈接,是指將創建成的類合併至 Java 虛擬機中,使之能夠執行的過程。它可分爲驗證、準備以及解析三個階段。

 

驗證階段的目的,在於確保被加載類能夠滿足 Java 虛擬機的約束條件。

準備階段的目的,則是爲被加載類的靜態字段分配內存。除了分配內存外,部分 Java 虛擬機還會在此階段構造其他跟類層次相關的數據結構,比如說用來實現虛方法的動態綁定的方法表。

在 class 文件被加載至 Java 虛擬機之前,這個類無法知道其他類及其方法、字段所對應的具體地址,甚至不知道自己方法、字段的地址。因此,每當需要引用這些成員時,Java 編譯器會生成一個符號引用。在運行階段,這個符號引用一般都能夠無歧義地定位到具體目標上。

舉例來說,對於一個方法調用,編譯器會生成一個包含目標方法所在類的名字、目標方法的名字、接收參數類型以及返回值類型的符號引用,來指代所要調用的方法。

解析階段的目的,正是將這些符號引用解析成爲實際引用。如果符號引用指向一個未被加載的類,或者未被加載類的字段或方法,那麼解析將觸發這個類的加載(但未必觸發這個類的鏈接以及初始化。)

Java 虛擬機規範並沒有要求在鏈接過程中完成解析。它僅規定了:如果某些字節碼使用了符號引用,那麼在執行這些字節碼之前,需要完成對這些符號引用的解析。

 

  • 初始化

在 Java 代碼中,如果要初始化一個靜態字段,我們可以在聲明時直接賦值,也可以在靜態代碼塊中對其賦值。

如果直接賦值的靜態字段被 final 所修飾,並且它的類型是基本類型或字符串時,那麼該字段便會被 Java 編譯器標記成常量值(ConstantValue),其初始化直接由 Java 虛擬機完成。除此之外的直接賦值操作,以及所有靜態代碼塊中的代碼,則會被 Java 編譯器置於同一方法中,並把它命名爲 < clinit >。

類加載的最後一步是初始化,便是爲標記爲常量值的字段賦值,以及執行 < clinit > 方法的過程。Java 虛擬機會通過加鎖來確保類的 < clinit > 方法僅被執行一次。

 

P.S. 本系列文章爲學習出自鄭雨迪的《深入拆解 Java 虛擬機》課程整理筆記。購買請掃描下方二維碼: 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章