Java基礎——深入理解類的載入

it_was發表於2020-09-18

Java虛擬機器將描述類的資料從.Class檔案裝入虛擬機器記憶體,並對資料進行驗證,準備,解析和初始化,最終形成Java虛擬機器可以直接使用的Java型別的過程為類的載入

執行期間。
不是編譯期間哦,類的所有工作都是在程式執行期間完成的,雖然這給Java虛擬機器編譯帶來額外的困難,但是這種動態載入和動態連結卻實現了Java的動態擴充套件這一語言特性

3.1 :boom:最重要的一點 —— 類的生命週期:boom:

:star:載入(loading)
:star:連結(Linking)

  • :small_orange_diamond:驗證(Verification)
  • :small_orange_diamond:準備(Preparation)
  • :small_orange_diamond:解析(Resolution)

:star:使用(Using)
:star:解除安裝(Unloading)

載入,驗證,準備,初始化和解除安裝這五個階段的開始順序是確定的,而解析則不一定,在某些情況下會在類的初始化階段之後開始!!

3.2 類初始化的六大時機:clock2:

載入,驗證和準備已經開始
:one:遇到new,getstatic,putstatic或invokestatic這四條位元組碼指令

  • 對於第一個new指令毋庸置疑,新建立例項必定會涉及類初始化
  • 對於二三指令就是獲取或者設定一個靜態欄位
  • 對於第四條指令就是呼叫靜態方法

:two:當使用java.lang.reflect包的方法進行反射呼叫時會進行相應的類初始化
:three:當初始化類的時候,如果發現父類沒有被初始化則先初始化父類!
:four:當Java虛擬機器啟動時,會首先進行main主類進行初始化
剩下兩種可以去了解
:boom: 注意,Java虛擬機器規範規定,有且只有以上六種情況才會進行類的初始化,這六種情況被稱為對一個型別的主動引用

:raised_hand:既然有主動引用,就有對應的被動引用,舉例說明

/**
情況一:透過子類引用父類的靜態欄位不會觸發子類的初始化!!!
**/
public class Main {
    public static void main(String[] args){
        System.out.println(subclass.age);
    }
}
class parent{
    public static int age  = 0; //靜態欄位
    static {
        System.out.println("parent is loading!");
    }
}
class subclass extends  parent{
    static {
        System.out.println("subclass is loading!");
    }
}
/**
情況二:透過陣列定義來引用類,不會觸發類的初始化!!!
**/
public class Main {
    public static void main(String[] args){
        parent[] parents = new parent[10];
        System.out.println(subclass.age);
    }
}
class parent{
    public static int age  = 0; //靜態欄位
    static {
        System.out.println("parent is loading!");
    }
}
/**
情況三:常量!!!
**/
public class Main {
    public static void main(String[] args){
        parent[] parents = new parent[10];
        System.out.println(subclass.age);
    }
}
public class parent{
    public final static int age  = 0; //常量欄位
    static {
        System.out.println("parent is loading!");
    }
}
解釋一下:上述程式碼並沒有列印 parent is loading! 原因在於,常量在編譯階段透過傳播最佳化,已經將此常量的值存入在Main類的常量池種,實際上對parent類中常量的引用都轉為對Main中常量的引用!!!

:facepunch:以上就是類的初始化時機和一些儘管看起來覺得需要進行初始化實際上並沒有進行初始化的情況!!!

:one:載入

  • 透過一個類的全限定名來獲取定義此類的二進位制位元組流
  • 將這個位元組流中蘇代表的靜態儲存結構轉換為方法去的執行時資料集
  • 在記憶體中生成一個代表這個類的java.lang.Class物件(僅此一份)!作為方法去這個類的各種資料的訪問入口
    注意:陣列的載入與以上步驟不同,但最終降維還是要進行類的載入過程的!

:two:驗證

  • 檔案格式驗證
  • 後設資料驗證
  • 位元組碼驗證
  • 符號引用驗證
    詳細過程自己看書!

:three:準備

準備階段主要是正式為類中定義的變數(即靜態變數,static修飾的變數)分配記憶體並設定類變數初始值的階段!注意此時並不分配例項變數!例項變數會隨著物件例項化一起分配在Java堆中!

public static int age = 19;
注意,類載入中的準備階段並不會立馬賦值為19,而是先賦初始值為0!!!!!直到進行類載入的初始化階段才會進行真正的賦值!!!

:four:解析

解析階段就是Java虛擬機器將常量池內的符號引用替換為直接引用的過程!!
詳細過程自己看書

:five: 初始化

進行準備階段變數已經賦值過一次的系統要求的初始零值(即靜態變數和常量),按照程式設計師主觀意願進行初始化類變數和其他資源!
換一種說法就是:執行類構造器 clinit()方法的過程:raising_hand:

  • clinit()方法是由編譯器自動收集類中的所有類變數的賦值動作和靜態語句塊(static{}塊)中的語句合併產生的,按出現順序收集!
  • clinit()方法與類的建構函式不同,它不需要顯式呼叫父類的構造器,Java虛擬機器會保證在子類 clinit()方法執行之前去執行父類的!因此在Java虛擬機器中,第一個被執行 clinit()方法的一定是java.lang.object
  • clinit()方法對於類和介面並不是必須的!如果一個類中沒有靜態語句塊,大可不必生成這個方法!
  • Java虛擬機器必須保證一個類的 clinit()方法在多執行緒環境中被正確的加鎖同步!如果多個執行緒進行類的初始化,那麼就只會有一個執行緒執行!所以如果這個方法耗時很長,可能會導致很隱蔽的阻塞!!!!:imp:

類載入器即是在類的載入過程種,透過獲取類的全限定名來獲取描述該類的二進位制位元組流,完成載入動作,但是其意義又遠不止於類的載入!

在Java虛擬機器中,對於任意一個類,都必須由類的載入器和這個類本身一同確立其在Java虛擬機器中的唯一性!言外之意,如果兩個類來源於同一個Class檔案,被同一個Java虛擬機器載入,如果載入他們的類載入器不同,那麼這兩個類一定是不同的!:boom:

注意:這裡的不同,包括代表類的Class物件的equals方法以及isInstance方法,同時也包括使用了 instanceof 關鍵字所做物件所屬關係判斷!!!

5.2 雙親委派模型

自JDK1.2 以來,Java一直保持這三層類載入、雙親委派模型的類載入架構:facepunch:

  • 啟動類載入器(BootStrap Class Loader):主要負責載入存放在 JAVA_HOME\lib 目錄下
  • 擴充套件類載入器(Extension Class Loader)
  • 應用程式類載入(Application Class Loader):負責載入使用者類路徑(class path)上所有的類庫,如果應用程式沒有自定義類載入器,一般這個類載入器就是預設的載入器

Java基礎——深入理解類的載入

雙親委派模型要求除了頂層的啟動類載入器外,其餘的類載入器都應該有自己的父類載入器,不過這裡的類載入器之間的父子關係不是以繼承來實現的,而是以組合關係來複用父載入器!!:raising_hand:

:arrow_right:結合上圖,我們再來看下原始碼!!:eyes:

private final ClassLoader parent; 
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 {//父載入器為空,使用啟動類載入器 BootstrapClassLoader 載入
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                   //丟擲異常說明父類載入器無法完成載入請求
                   //但並不做任何處理,只是繼續向下執行!
                }

                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; //返回自己的載入結果給子類!
        }
    }

從上面的過程我們可以看出,雙親委派模型的工作過程::boom:

  1. 當需要載入類的時候,首先判斷這個類是否載入過了!
  2. 沒有載入過 ,ok, 遞迴向上傳遞,檢查對應類載入器是否載入過這個類!
  3. 如果到了最頂層的啟動類載入都沒有載入,那就從啟動類載入器開始嘗試載入,即遞迴返回,每層載入器嘗試載入!

雙親委派模型的優點:
雙親委派模型保證了Java程式的穩定執行,可以避免類的重複載入(JVM 區分不同類的方式不僅僅根據類名,相同的類檔案被不同的類載入器載入產生的是兩個不同的類),也保證了 Java 的核心 API 不被篡改。因為越基礎的類由越頂層的類載入載入,

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章