圖文兼備看懂類加載機制的各個階段,就差你了!

寫在前面:類加載機制卻是聽得很多了,可是又知不知道它到底有什麼用呢?爲何要學它呢?由於面試(真實.jpg),其實也不只僅是面試,掌握它能夠掌握對類加載的時機,在真正須要使用到類時才加載到內存中,能夠減輕服務器的壓力,並且,許多框架底層源碼都用到了反射這個東西,反射的原理就是基於類加載機制的,因此掌握了這門絕活,學反射就不會一頭霧水了,看源碼也知其因此然了。java

開篇概述:Java文件在編譯時轉換爲字節碼文件,字節碼文件就是對一個類的描述,Java虛擬機把Class文件加載到內存,而且通過驗證、準備、解析和初始化,最終造成能夠被JVM直接使用的Java類型,這就是類加載機制。下面就是對類加載機制各個過程的詳細分析,每一個階段都會盡我所能把最詳細清晰的圖貼上去帶你理解這個階段究竟是怎樣的面試

類的生命週期

類的生命週期

類的生命週期分爲7個階段,在圖中我已經標註了每一個階段對類所作的主要事情,你請拿好,若是這張圖能幫助到你,你的點贊是對我最大的鼓勵和支持!(跑遠了哈哈哈)其中驗證、準備、解析三個部分統稱爲鏈接,下面我就會對每個部分作出通俗易懂的解釋,用最友好的圖示來告訴你,這一個階段JVM到底作了什麼~服務器

觸發類加載的時機

聲明:加載是類加載的其中一個階段,類加載包含了前五個階段(加載、驗證、準備、解析、初始化),要區分開加載和類加載的區別。數據結構

咱們來看看何時會類加載呢?多線程

第一個階段是加載,在Java虛擬機規範中沒有明確規定一個類在何時會被加載,可是它嚴格規定了只有如下6種狀況必須對類進行初始化操做,在初始化操做以前一定會觸發類的加載和鏈接框架

(1)遇到newgetstaticputstaticinvokestatic這四條字節碼指令時函數

  • 使用new關鍵字實例化對象時;對應new字節碼指令佈局

  • 讀取或設置一個類的靜態字段(被final修飾的、在編譯期把結果放入常量池的靜態變量除外)時;對應getstaticputstatic字節碼指令學習

  • 調用一個類的靜態方法時;對應invokestatic字節碼指令spa

(2)使用java.lang.reflect包的方法第一次對類進行反射調用時會觸發類的初始化

(3)初始化類時,若是發現父類尚未初始化,則須要先觸發父類的初始化

(4)虛擬機啓動時,用戶須要指定一個主函數類(main()方法所在的類),虛擬機會先啓動這個類

(5)使用JDK7新加入的動態語言支持時,若是一個java.lang.invoke.MethodHanlde實例最後的解析結果爲REF_getstaticREF_putstaticREF_invokeStaticREF_newInvokeSpecial四種類型的方法句柄時,都須要先初始化該句柄對應的類

(6)接口中定義了JDK 8新加入的默認方法(default修飾符),實現類在初始化以前須要先初始化其接口

上面幾種類型是否是看得懵逼,下面我就會對類的初始化進行舉例,讓大家經過更直觀的場景能夠理解上面幾種狀況。由於只要類被初始化,它就必定得先加載該類到內存中

實戰初始化類

定義一個StaticClass,若是類被初始化,那麼會自動執行靜態代碼塊,在控制檯能夠看到信息。

/** * @author Zeng * @date 2020/4/8 23:21 */
public class StaticClass {

    static {
        System.out.println("StaticClass initialized!");
    }

    public static int A = 0;

    public static void staticFunction(){
        System.out.println("staticFunction executed;");
    }

}
複製代碼

我以第一種狀況給你演示一下類是否真的被加載和初始化了

咱們使用一個Test類調用靜態變量A和靜態方法staticFunction(),以下圖所示

/** * @author Zeng * @date 2020/4/8 23:21 */
public class Test {
    public static void main(String[] args) {
        //new
        StaticClass obj = new StaticClass();
        //getstatic
        int a = StaticClass.A;
        //putstatic
        StaticClass.A = 1;
        //invokestatic
        StaticClass.staticFunction();
    }
}
複製代碼

控制檯的結果以下,很明顯StaticClass是被初始化了

類初始化

咱們可使用JVM啓動參數-XX:+TraceClassLoading進行查看StaticClass類有沒有被加載

類加載

能夠看到JVM確確實實是加載了StaticClass

類加載的過程

知道了觸發類加載的6種作法之後,咱們就深刻類加載的過程,探祕每個過程發生的事情

加載階段

加載階段,Java虛擬機須要完成三件事情

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

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

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

怎麼理解上面的第二、3點呢?

靜態存儲結構指的是Class文件結構,它是一組以8位字節爲單位的二進制流,此時是靜態的,虛擬機會把這個文件的相關類信息加載到方法區當中,並在Java堆上建立java.lang.Class的對象,該對象就是圖中的SubClass類,注意不是SubClass的對象實例,而是java.lang.Class的對象實例

到這裏咱們會產生一個疑問,加載階段不是應該在鏈接階段以前執行嗎?爲何還沒進行驗證、準備和解析就能夠把類信息放入方法區?

注意:加載階段和鏈接的部分動做(如一部分字節碼的文件格式驗證動做)是交叉進行的,也就是說加載階段還沒完成,鏈接階段可能已經開始,但這些夾在加載階段之中進行的動做(驗證文件格式、字節碼驗證······)都屬於鏈接操做。

鏈接階段

鏈接階段包括驗證、準備和解析,下面咱們每個階段來細看,咱們不用記住每個階段內部具體校驗的東西,從總體上歸納該階段幹了一些什麼。

驗證階段

驗證階段主要包括四個檢驗動做:

文件格式檢驗:驗證上面的Class文件字節流是否符合Class文件格式的規範,而且能被當前版本的虛擬機處理。例如文件前四個字節是否爲CA FE BA BE表明這是一個Class文件。

元數據驗證:對字節碼描述的信息進行語義分析,保證其描述的信息符合要求,例如數據類型是否正確,是否正確繼承類·····

字節碼驗證最複雜的一個階段,經過數據流分析和控制流分析,肯定程序語義是合法的、符合邏輯的,例如是否在return後面還有語句,這些語句是不可達的······

符號引用驗證:對類自身之外的各種信息進行匹配性校驗,通俗地說,該類是否缺乏或禁止訪問它依賴的某些外部類、方法、字段等資源

下面用上面的字節碼文件做例子來給你說明這四個檢驗動做:

文件格式檢驗:假如在獲取到Class文件以後,我偷偷地將開頭修改成CA FE DE AD,虛擬機還能正常加載該文件嗎?

咱們此時使用java SubClass命令嘗試加載該文件,看看發生什麼結果!

上圖中的錯誤信息已經很是明顯了,告訴咱們魔數3405700782是一個非法值,咱們再來看看這個值是什麼~

這不就是咱們剛剛修改的地方嘛,所以,文件格式驗證會對文件的相關格式作檢驗,固然個人例子只是冰山一角,實際上JVM作的校驗多了去了。我只是將最容易理解的方式寫給大家,讓大家對整個過程有個最直觀的理解,儘可能不要死記硬背

元數據驗證、字節碼驗證和符號引用驗證因爲篇幅問題就再也不作例子了,咱們只要知道驗證階段是校驗字節碼文件裏的一切東西都要符合《Java虛擬機規範》

準備階段

準備階段是正式爲類中定義的變量(即靜態變量、被static修飾的變量)分配內存並設置類變量初始值的階段,咱們須要知道兩個要點

  1. 類變量被分配在哪一個存儲區域
  2. 類變量的初始值是什麼

首先咱們先來講第1點,咱們都知道方法區是存儲類相關信息的區域,在JDK7及之前,類變量是存儲在方法區當中的,而在JDK8及以後,類變量已經隨着Class對象一塊兒存放在Java堆當中了,這時候類變量存放在方法區這句話已經只是停留在邏輯上的概念表述層面了。

第2點是類變量的初始值,假設有一個類變量public static int value = 666,在準備階段事後的初始值是0而不是666,在初始化階段時纔會被賦值爲123

注意若是有一個靜態類變量爲public static final int value = 666,那麼它在準備階段JVM就已經會給它賦值爲666,不會賦零值。

解析階段(重要!)

將常量池內的符號引用替換爲直接引用的過程。

符號引用:符號引用是以一組符號來描述所引用的目標,符號能夠是任何的字面形式的字面量,只要不會出現衝突可以定位到就行。符號引用於JVM內存佈局無關。

符號引用的做用是在編譯的過程當中,JVM並不知道引用的具體地址,因此用符號引用進行代替,而在解析階段將會將這個符號引用轉換爲真正的內存地址。

直接引用:能夠是指向目標的指針,偏移量或者可以直接定位的句柄。該引用是和內存中的佈局有關的,而且必定加載進來的。有了直接引用,那麼引用的目標一定已經在虛擬機內存中。

直接引用能夠理解爲:指向類對象、變量、方法的指針、指向實例的指針和一個間接定位到對象的對象句柄。

舉個例子讓你去理解它們兩個的區別

public class Test{
   public static void main(String[] args) {
     String s="adc";
     System.out.println("s="+s);
   }
}
複製代碼

上面這段代碼的變量s在編譯時會被解析成爲符號引用,符號引用的標誌是astore_<n>,對應下圖的astore_1

咱們在方法裏定義了一個局部變量s,把它指向adc存放的地址,可是在編譯時s並不知道adc的地址,JVM將變量sastore_1對應起來,astore_1的含義是將操做數棧頂的adc保存回索引爲1的局部變量表中,此時訪問變量s就會讀取局部變量表索引值爲1中的數據。因此局部變量s就是一個符號引用。

下面這段代碼的字符串被解析爲直接引用

public class Test{
    public static void main(String[] args) {
        System.out.println("s="+"adc");
    }
}
複製代碼

咱們能夠看到字節碼指令ldc直接將s=abc這一字符串從常量池中推送到棧,而後下一條字節碼指令invokevirtual表明調用實例方法,並無將字符串存入局部變量表中,因此這裏的s=abc就是一個直接引用。

總結一下:符號引用是指在編譯時沒法肯定對象的內存地址,因此必須使用一個符號引用去對應局部變量表中的一個特定位置,而後在解析階段將該變量的值或引用地址保存回局部變量表中,此後訪問該變量值都會從局部變量表對應的位置查找該值;而直接引用是在編譯時就能夠肯定。

初始化

類的初始化是類加載的最後一個階段了,在準備階段時,JVM已經爲類變量賦了零值,在初始化階段,會根據代碼去真正地初始化類變量值和其它資源

咱們先來看看StaticClass被初始化時是如何執行靜態代碼塊的?

咱們在IDEA中查看StaticClass的字節碼文件,看到熟悉的一個輸出語句,那麼咱們能夠推測靜態代碼塊被翻譯成下面這個<clinit>函數(先別走T.T,這個函數挺重要的,咱們要掌握的,堅持看下去,學會了很香的)

StaticClass字節碼的類構造器

靜態代碼塊其實就是一個類構造函數,當一個類被初始化時,就會被調用這個<clinit>方法對類進行初始化操做,注意這個方法只會執行一次,由於JVM加載某個類到內存中後,直到卸載以前,這個類一直都在內存當中,因此這也解釋了爲何靜態代碼塊只會執行一次

<clinit>方法

這個方法是由編譯器自動收集類中全部類變量的賦值操做和靜態代碼塊中的語句合併產生的,收集的順序是由語句在源文件中出現的順序決定的,靜態代碼塊只能訪問到定義在它以前的類變量,可是能夠爲定義在它以後的類變量賦值

public class Test{
    static {
        i = 0; //編譯經過
        System.out.print(i); //編譯失敗
    }
    static int i = 1;
}
複製代碼

在初始化一個類時,必須先初始化其父類,所以第一個執行<clinit>方法的必定是Object類。

<clinit>方法不是必須存在的,若是一個類中沒有類變量的賦值操做,也沒有靜態代碼塊,那麼這個類將沒有<clinit>方法。

若是多個線程同時但願初始化一個類,<clinit>方法會在多線程環境下保證正確地加鎖同步,只有其中一個線程去執行這個類的clinit<>()方法。

總結

完結撒花!!!看到這裏的你已經掌握了類加載機制的絕大多數內容了,主要須要掌握類加載機制的七個階段,類加載過程當中每一個階段所作的事情,什麼狀況下會觸發類的初始化解析階段的直接引用和符號引用在面試過程當中若是能解釋清楚是很是加分的,它表明你對虛擬機棧的結構很是清晰,也清楚類加載的每一階段主要作了什麼,首先很感謝你願意花時間來閱讀個人文章,若是這篇文章對你有一點點小的幫助,**你的點贊是對我最大的鼓勵和支持!**因爲做者能力有限,如文章有嚴重錯誤,請務必評論指出,樂意與你們交流和學習!

巨人的肩膀:

blog.csdn.net/qq_34402394…

相關文章
相關標籤/搜索