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

本章內容是對《深刻理解Java虛擬機:JVM高級特性和最佳實踐》的理解和歸納。java

前言

這是我CVTE面試時候的一個坎兒,由於面試官當時問個人時候,我堅決果斷的回答了沒有接觸過這一塊的知識。因此以後會從網上挑一些經典的面試題作總結。面試

類的加載機制

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

image

經過這張圖咱們能夠了解到,整個過程的流程了。下面主要介紹最主要的前5個部分:安全

加載

須要完成如下三項任務:數據結構

(1)經過一個類的全限定名來獲取定義此類的二進制字節流。ide

(2)將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構。函數

(3)在內存中生成一個表明這個類的java.lang.Class對象,做爲方法區這個類的各類數據的訪問入口。post

往簡單了說,就是讀取數據,並對數據的形式作一個轉化,變成一個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();
    }
}
複製代碼

另一個是我在牛客練習時知道的知識,叫作左編譯右運行,實際上是向上轉型的概念,可是這種記法更生動形象。 直接用代碼來驗證這句話,如今將子類和父類修改爲如下形式。

// 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去調用這個函數,會出現什麼狀況?

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

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

以上就是個人學習成果,若是有什麼我沒有思考到的地方或是文章內存在錯誤,歡迎與我分享。


相關文章推薦:

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

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

相關文章
相關標籤/搜索