JVM必備基礎知識(一) -- 類的載入機制

ClericYi發表於2020-02-21

JVM必備基礎知識(一) -- 類的載入機制

本章內容是對《深入理解Java虛擬機器:JVM高階特性和最佳實踐》的理解和概括。

前言

這是我CVTE面試時候的一個坎兒,因為面試官當時問我的時候,我毫不猶豫的回答了沒有接觸過這一塊的知識。所以之後會從網上挑一些經典的面試題做總結。

類的載入機制

先使用一張圖整個載入機制所包含的過程。

image

通過這張圖我們可以瞭解到,整個過程的流程了。下面主要介紹最主要的前5個部分:

載入

需要完成以下三項任務:

(1)通過一個類的全限定名來獲取定義此類的二進位制位元組流。

(2)將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構。

(3)在記憶體中生成一個代表這個類的java.lang.Class物件,作為方法區這個類的各種資料的訪問入口。

往簡單了說,就是讀取資料,並對資料的形式做一個轉化,變成一個JVM能夠認識的模樣,然後對應的JVM的記憶體空間。(注意這個時候,還沒有真正的把類塞進去!!)

驗證

確保Class檔案的位元組流中包含的資訊符合當前虛擬機器的要求,並且不會危害虛擬機器自身的安全。

為什麼需要這麼一個環節呢? Class檔案的產生,並不是一定來自Java原始碼。他甚至可以由我們直接編寫而成,驗證能幫我過濾掉錯誤的Class檔案,保障虛擬機器的正確執行。

需要完成以下四項任務:

(1)檔案格式驗證:驗證位元組流是否符合Class檔案格式的規範,並且能被當前版本的虛擬機器處理。

(2)後設資料驗證:對類的後設資料資訊中的資料型別等進行校驗。

(3)位元組碼驗證:對類的方法體進行校驗。

(4)符號引用驗證:動作的正確執行。

準備

正式為類變數分配記憶體並設定類變數初始值的階段,這些變數所使用的記憶體都將在方法區中進行分配。

這個階段存在一個思考。

public class Bean {
    public static int i_static = 123;
    public int i = 123;
}
複製程式碼

為什麼我們可以直接從main()函式中呼叫到的i_static,而呼叫不到i呢? 讀者肯定會說,這不是廢話嗎,i_static是用static修飾的,當然可以呼叫。但是這是從使用的角度來思考了。 其實這就是準備階段要乾的事情了,在這個階段,虛擬機器已經為這些資料做好了存放的工作,所以我們能夠呼叫。但是i這個變數,在你沒有例項化之前,他是沒有被存放在記憶體空間的,自然也就不能夠呼叫了。更直白的說,就是你找不到唄,找不到我怎麼用。

解析

虛擬機器將常量池內的符號引用替換為直接引用的過程。

  • 符號引用:符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可。
  • 直接引用:直接引用可以是直接指向目標的指標、相對偏移量或是一個能間接定位到目標的控制程式碼。

解析是一個不定時的工作內容,因為像new,陣列引用這些都是一個視情況而定的事件。

初始化

在書中很明確的提及到以下五種情況,是需要立即對類進行初始化的,或者說只有這五種情況下是需要對一個類進行主動引用:

(1)使用new關鍵字例項化物件的時候、讀取或設定一個類的靜態欄位(被final修飾、已在編譯期把結果放入常量池的靜態欄位除外)的時候,以及呼叫一個類的靜態方法的時候。

(2)對類進行反射呼叫的時候。

(3)當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。

(4)當虛擬機器啟動時,虛擬機器會先初始化帶main()的主類。

(5)當使用JDK 1.7的動態語言支援時,如果一個java.lang.invoke.MethodHandle例項最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法控制程式碼”“並且這個方法控制程式碼所對應的類沒有進行過初始化,則需要先觸發其初始化。”

對於靜態變數和靜態塊的載入,是按照在程式碼中的順序來進行初始化的。

public class Bean {
    // public static int i_static = 123;
    static {
        i_static = 0;
    }
    public static int i_static = 123;
}
複製程式碼

使用這個Bean類進行列印i_static的時候,出現的先後順序,通過列印就能知道區別了。

優先順序如下:

  • 靜態 → 例項;
  • 父類 → 子類

實踐測試程式碼

// Bean類
public class Bean {
    static {
        System.out.println("Bean static load");
    }
    Bean(){
        System.out.println("Bean load");
    }
}

// 繼承自Bean的子類
public class BeanSon extends Bean {
    static {
        System.out.println("BeanSon static load");
    }

    BeanSon(){
        System.out.println("BeanSon load");
    }
}

// 具體呼叫
public class Main {
    public static void main(String[] args) {
        Bean bean = new BeanSon();
    }
}
複製程式碼

JVM必備基礎知識(一) -- 類的載入機制

另外一個是我在牛客練習時知道的知識,叫做左編譯右執行,其實是向上轉型的概念,但是這種記法更生動形象。 直接用程式碼來驗證這句話,現在將子類和父類修改成以下形式。

// BeanSon
public class BeanSon extends Bean {
    static {
        System.out.println("BeanSon static load");
    }

    BeanSon() {
        System.out.println("BeanSon load");
    }

    @Override
    void commonHas() {
        System.out.println("commonHas BeanSon");
    }

    void doSomething() {
        System.out.println("doSomething");
    }
}

// Bean
public class Bean {
    static {
        System.out.println("Bean static load");
    }
    Bean(){
        System.out.println("Bean load");
    }

    void commonHas(){
        System.out.println("commonHas Bean");
    }
}
複製程式碼

然後使用上面的Main類中的物件bean去呼叫這個函式,會出現什麼情況?

JVM必備基礎知識(一) -- 類的載入機制

找不到doSomething()這個函式?這就是左編譯的意思了,雖然是按照右邊的子類執行,但是是不會將子類多出來的方法加入到方法區。 再呼叫上圖中的commonHas()方法後,你又會發現列印的結果是這樣的。

JVM必備基礎知識(一) -- 類的載入機制

它執行出了子類的結果,這也就是右執行的意思了。 以上就是面試一個知識點了,明天繼續,fire!!

以上就是我的學習成果,如果有什麼我沒有思考到的地方或是文章記憶體在錯誤,歡迎與我分享。


相關文章推薦:

JVM必備基礎知識(二)-- 類載入器和雙親委派模型

JVM必備基礎知識(三)-- GC垃圾回收機制

相關文章