深入理解Java虛擬機——JVM的類加載機制

虛擬機把描述類的數據從Class文件加載到內存,並對數據進行校驗、轉換解析和初始化,最終形成可以被虛擬機直接使用的Java類型,這就是虛擬機的類加載機制。 

類加載的規則:

全盤負責,當一個類加載器負責加載某個Class時,該Class所依賴的和引用的其他Class也將由該類加載器負責載入,除非顯示使用另外一個類加載器來載入

父類委託,先讓父類加載器試圖加載該類,只有在父類加載器無法加載該類時才嘗試從自己的類路徑中加載該類

緩存機制,緩存機制將會保證所有加載過的Class都會被緩存,當程序中需要使用某個Class時,類加載器先從緩存區尋找該Class,只有緩存區不存在,系統纔會讀取該類對應的二進制數據,並將其轉換成Class對象,存入緩存區。這就是爲什麼修改了Class後,必須重啓JVM,程序的修改纔會生效。

類加載的過程:

    包括加載、鏈接(含驗證、準備、解析)、初始化

如下圖所示:


1、加載:

  類加載指的是將類的class文件讀入內存,併爲之創建一個java.lang.Class對象,作爲方法區這個類的數據訪問的入口

也就是說,當程序中使用任何類時,系統都會爲之建立一個java.lang.Class對象。具體包括以下三個部分:

(1)通過類的全名產生對應類的二進制數據流。(根據early load原理,如果沒找到對應的類文件,只有在類實際使用時纔會拋出錯誤)

(2)分析並將這些二進制數據流轉換爲方法區方法區特定的數據結構

(3)創建對應類的java.lang.Class對象,作爲方法區的入口(有了對應的Class對象,並不意味着這個類已經完成了加載鏈接)

通過使用不同的類加載器,可以從不同來源加載類的二進制數據,通常有如下幾種來源:

(1)從本地文件系統加載class文件,這是絕大部分程序的加載方式

(2)從jar包中加載class文件,這種方式也很常見,例如jdbc編程時用到的數據庫驅動類就是放在jar包中,jvm可以從jar文件中直接加載該class文件

(3)通過網絡加載class文件

(4)把一個Java源文件動態編譯、並執行加載 

2、鏈接:

    鏈接指的是將Java類的二進制文件合併到jvm的運行狀態之中的過程。在鏈接之前,這個類必須被成功加載。

類的鏈接包括驗證、準備、解析這三步。具體描述如下:

2.1  驗證:

    驗證是用來確保Java類的二進制表示在結構上是否完全正確(如文件格式、語法語義等)。如果驗證過程出錯的話,會拋出java.lang.VertifyError錯誤。

主要驗證以下內容:

  • 文件格式驗證
    • 驗證字節流是否符合class文件格式的規範,並且能被當前虛擬機處理,如是否以魔數0xCAFEBABE開頭、主次版本號是否在當前虛擬機處理範圍內、常量池是否有不支持的常量類型等。只有經過格式驗證的字節流,纔會存儲到方法區的數據結構,剩餘3個驗證都基於方法區的數據進行。
  • 元數據驗證
    • 對字節碼描述的數據進行語義分析,以保證符合Java語言規範,如是否繼承了final修飾的類、是否實現了父類的抽象方法、是否覆蓋了父類的final方法或final字段等。
  • 字節碼驗證
    • 對類的方法體進行分析,確保在方法運行時不會有危害虛擬機的事件發生,如保證操作數棧的數據類型和指令代碼序列的匹配、保證跳轉指令的正確性、保證類型轉換的有效性等。
  • 符號引用驗證
    • 爲了確保後續的解析動作能夠正常執行,對符號引用進行驗證,如通過字符串描述的全限定名是都能找到對應的類、在指定類中是否存在符合方法的字段描述符等。

2.2  準備:

  準備過程則是創建Java類中的靜態域(static修飾的內容),並將這些域的值設置爲默認值,同時在方法區中分配內存空間。準備過程並不會執行代碼。

注意這裏是做默認初始化,不是做顯式初始化。例如:

public static int value = 12;

上面的代碼中,在準備階段,會給value的值設置爲0(默認初始化)。在後面的初始化階段纔會給value的值設置爲12(顯式初始化)。

但是有個特殊情況:

public static final int value = 12;

在編譯階段會爲value生成ConstantValue屬性,在準備階段虛擬機會根據ConstantValue屬性將value賦值爲100。 

2.3  解析:

  解析的過程就是確保這些被引用的類能被正確的找到(將符號引用替換爲直接引用)。解析的過程可能會導致其它的Java類被加載。

  符號引用和直接引用有什麼不同?

  1、符號引用使用一組符號來描述所引用的目標,可以是任何形式的字面常量,定義在Class文件格式中。

  2、直接引用可以是直接指向目標的指針、相對偏移量或則能間接定位到目標的句柄。

3、初始化:

  初始化階段是類加載過程的最後一步。到了初始化階段,才真正執行類中定義的Java程序代碼(或者說是字節碼)。

  初始化階段是執行類構造器<clinit>方法的過程,<clinit>方法由類變量的賦值動作和靜態語句塊按照在源文件出現的順序合併而成,該合併操作由編譯器完成。

    1、<clinit>方法對於類或接口不是必須的,如果一個類中沒有靜態代碼塊,也沒有靜態變量的賦值操作,那麼編譯器不會生成<clinit>;

    2、<clinit>方法與實例構造器不同,不需要顯式的調用父類的<clinit>方法,虛擬機會保證父類的<clinit>優先執行;

    3、爲了防止多次執行<clinit>,虛擬機會確保<clinit>方法在多線程環境下被正確的加鎖同步執行,如果有多個線程同時初始化一個類,

      那麼只有一個線程能夠執行<clinit>方法,其它線程進行阻塞等待,直到<clinit>執行完成。

    4、注意:執行接口的<clinit>方法不需要先執行父接口的<clinit>,只有使用父接口中定義的變量時,纔會執行。

  在以下幾種情況中,會執行初始化過程:

(1)創建類的實例

(2)訪問類或接口的靜態變量(特例:如果是用static final修飾的常量,那就不會對類進行顯式初始化。static final 修改的變量則會做顯式初始化

(3)調用類的靜態方法

(4)反射(Class.forName(packagename.className))

(5)初始化類的子類。注:子類初始化問題:滿足主動調用,即父類訪問子類中的靜態變量、方法,子類纔會初始化;否則僅父類初始化。

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

  

類的初始化過程(重要)

Student s = new Student();在內存中做了哪些事情?

  • 加載Student.class文件進內存
  • 棧內存爲s開闢空間
  • 堆內存爲學生對象開闢空間
  • 對學生對象的成員變量進行默認初始化
  • 對學生對象的成員變量進行顯示初始化
  • 通過構造方法對學生對象的成員變量賦值
  • 學生對象初始化完畢,把對象地址賦值給s變量

public class TestInstance {

    public static TestInstance instance = new TestInstance();  //初始化 a=1,b=1
    public static int a; 
    public static int b = 0;  //a=1,b=0

    public TestInstance() {
        a++;
        b++;
    }

    public static void main(String[] args) {
        System.out.println(TestInstance.a);
        System.out.println(TestInstance.b);
    }
}

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