深入JVM原理系列(一)

    上週分享了一篇關於RocketMQ的文章,有很多的不足,很多小夥伴說少了一些圖和代碼,然後還有小夥伴建議我貼一些應用層面的,跟框架整合的代碼。後來想了想,代碼的話GitHub上面太多Demo了,就不在此分享啦,加上最近一直在研究學習JVM這塊的內容,因此準備分享一個JVM的系列性的文章,讓之前沒怎麼研究JVM的或者沒有過深入瞭解JVM的同學,看完這系列文章能對JVM有一個全新的認識,結合上一篇的經驗,這次會加上很多代碼的實例並且配上相應的圖片。

    本篇文章主要講的是類的生命週期,然後將生命週期中的各個部分詳細剖析。類的生命週期主要分爲以下幾個階段,如下圖所示:

 

類加載

類的加載指的是將類的.class文件中的二進制數據讀入到內存中,將其放在運行時數據區的方法區內,然後在內存中創建一個java.lang.Class對象(JVM規範中並未說明class對象位於哪裏,HotSpot虛擬機將其放在了方法區中)用來封裝類在方法區內的數據結構。

 

類連接

    在類加載完之後就進入連接階段,這個階段分爲以下三個環節:

    1)驗證:確保被加載類的正確性,是否符合jvm規範

    2)準備:爲類的靜態變量分配內存,並將其初始化爲默認值

    在準備階段,Java虛擬機爲類的靜態變量分配內存,並設置默認的初始值。例如對於以下Sample類,在準備階段,將爲int類型的靜態變量a分配4個字節的內存空間,並且賦予默認值0,爲long類型的靜態變量b分配8個字節的內存空間,並且賦予默認值0。

public class Sample{  private static int a = 1;  private static long b;  static{      b = 2;  }}

    3)解析:把類中的符號引用轉換成直接引用

初始化

    連接完之後就進入初始化階段,爲類的靜態變量賦予正確的初始值。在程序中,靜態變量的初始化有兩種途徑:在靜態變量的聲明處進行初始化、在靜態代碼塊中進行初始化。

    關於類的初始化,那麼類是在哪些條件下會進行初始化呢?結論是每個類或接口首次被程序"主動使用"時,纔會初始化它們,什麼是主動使用?包括以下7點:

    1)創建類的實例

    2)訪問每個類或接口的靜態變量、或對該靜態變量賦值

    3)調用類的靜態方法

    4)反射(如:Class.forName("com.test.Test"))

    5)初始化一個類的子類

    6)Java虛擬機啓動時被標明爲啓動類的類

    7)JDK1.7開始提供的動態語言支持:java.lang.invoke.MethodHandle實例的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic句柄對應的類沒有初始化,則初始化

    除了上述的7種情況外,都認爲是非主動使用,不會觸發類的初始化操作。

類初始化過程中常量的詳解

    對於靜態變量字段來說,只有直接定義了該字段的類纔會被初始化;當一個類在初始化時,要求其父類全部都已經初始化完畢了。

    定義:final修飾的變量爲常量

    特點:常量在編譯階段會存入到調用這個常量的方法所在的類的常量池中。本質上,調用類並沒有直接引用到定義常量的類,因此並不會觸發定義常量的類的初始化。

public class MyTest {    public static void main(String[] args) {        System.out.println(MyParent.str);    }}class MyParent {    // 如果str爲常量 -> 則不會初始化該類    // 如果str爲變量(不用final修飾)-> 則會初始化該類,執行靜態代碼塊    public static final String str = "hello world";    static {        System.out.println("MyParent static block");    }}

    上面的代碼,在編譯完成後,刪除MyParent.class文件,輸出結果如下:

MyParent static blockhello word

    如果把修飾str的final去掉,編譯完成後,刪除MyParent.class文件,將拋出異常:

Caused by: java.lang.ClassNotFoundException: com.yto.cn.classload.MyParent3

    這是因爲常量在編譯完成後會被存放在常量池當中,並不會觸發類的初始化操作,因此刪除該常量所在的class文件對運行結果沒有影響。

 

編譯期常量與運行期常量區別

    當一個常量的值並非編譯期間可以確定的,那麼其值就不會被放到調用類的常量池中,那麼在程序運行時,會導致主動使用這個常量所在的類,顯然會導致這個類被初始化。如下面例子所示:

class MyParent3 {
    // 運行時才能確定的常量
    public static final String str = UUID.randomUUID().toString();
    static{
        System.out.println("MyParent3 static block");
    }
}

    如果在編譯完成後刪除MyParent3.class文件,則會拋出異常:

Caused by: java.lang.ClassNotFoundException: com.yto.cn.classload.MyParent3

類加載器深入解析

    類的加載的最終產品是位於內存中的Class對象。

    Class對象封裝了類在方法區內的數據結構,並且向Java程序員提供了訪問方法區內的數據結構的接口。

    類加載器用來把類加載到Java虛擬機中,從JDK1.2開始,類的加載過程採用雙親委託機制,在此委託機制中,除了Java虛擬機自帶的根類加載器以外,其餘的類加載器都有且只有一個父加載器。當Java程序要求請求加載器loader1加載Sample類時,loader1首先委託自己的父加載器去加載Sample類,若父加載器能加載,則由父加載器完成加載任務,否則才由加載器loader1本身加載Sample類。

    有兩種類型的類加載器:

    1)Java虛擬機自帶的類加載器

    根類加載器(Bootstrap):負責加載虛擬機的核心類庫,如java.lang.* 等。根類加載器從系統屬性sun.boot.class.path=$JAVA_HOME/jre/lib或 -Xbootclasspath所指定的目錄中加載類庫。根類加載器的實現依賴於底層操作系統,屬於虛擬機的實現的一部分,它並沒有繼承java.lang.ClassLoader類。

    擴展類加載器(Extension):它的父加載器爲根類加載器。從java.ext.dirs系統屬性所指定的目錄中加載類庫(-Djava.ext.dirs=sss/lib),如果沒有設置則從JDK的安裝目錄$JAVA_HOME/lib/ext子目錄下加載類庫,如果把用戶創建的JAR文件放在這個目錄下,也會自動由擴展類加載器加載,擴展類加載器是純Java類,是java.lang.ClassLoader類的子類。

    應用類加載器(Application):父類爲擴展類加載器,如果設置了系統屬性java.class.path(java -classpath/-Djava.class.path),則從指定的目錄中加載類;如果沒有設置,則從環境變量$classpath指定的目錄下加載類(這裏需要說明的是,如果$CLASSPATH爲空,jdk會默認將被運行的Java類的當前路徑作爲一個默認的$CLASSPATH,一但設置 了$CLASSPATH變量,則會到$CLASSPATH對應的路徑下去尋找相應的類,找不到就會報錯)。它是用戶自定義的類加載器的默認父加載器,是純Java類,是java.lang.ClassLoader類的子類。

    2)用戶自定義的類加載器

    是java.lang.ClassLoader的子類,用戶可以定製類的加載方式

    下面畫了關於各個類加載器的一個關係圖:

 

    由於每一章的篇幅不宜太多,本次主題最後剛剛提到各種類加載器,這塊內容比較多,最好集中到一起,所以放到下一篇,對類加載器的作用、源碼、和雙親委託機制做一個詳細的剖析,感興趣的小夥伴可以關注一下~

微信公衆號:

個人微信:T_Stone11

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