Java虛擬機-類加載機制

總覽

類從被加載到虛擬機,到被卸載。其整個生命週期包括以上幾個階段:加載 (Loading)、驗證 (Verification)、準備 (Preparation)、解析 (Resolution)、初始化 (Initialization)、使用 (Using)、卸載 (Unloading)java

其中類加載過程包括加載 (Loading)、驗證 (Verification)、準備 (Preparation)、解析 (Resolution)、初始化 (Initialization)五個階段。除了解析 (Resolution)階段順序不肯定之外,其餘四個階段是按順序開始的。數據庫

解析階段可能在初始化階段以後開始,其目的是爲了支持Java的動態綁定,好比多態(調用父類方法實際執行的是子類覆蓋的方法)。安全

加載階段

在加載階段,虛擬機完成如下三件事情:網絡

  1. 經過一個類的全限定名來獲取其定義的二進制字節流 (獲取的途徑能夠從Class文件中、Jar包中、網絡中(好比Applet)、或由其餘文件生成(JSP應用))。
  2. 將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構
  3. 在Java堆中生成一個表明這個類的java.lang.Class對象,做爲對方法區中這些數據的訪問入口。

類加載器 ClassLoader

在加載階段咱們能夠實現本身的ClassLoader,從而能夠動態的建立符合特定化需求的類,或者是能夠從特定的數據源 (網絡、文件系統、數據庫等等) 獲取class文件。數據結構

大體的類加載器層級結構以下,多線程

  • 啓動類加載器 (Bootstrap ClassLoader),負責加載 JDK\jre\lib 下,或被-Xbootclasspath參數指定的路徑中的,而且能被虛擬機識別的類庫(如rt.jar,全部java.*開頭的類均被Bootstrap ClassLoader加載)。啓動類加載器沒法被Java程序直接引用佈局

  • 擴展類加載器 (Extension ClassLoader),負責加載JDK\jre\lib\ext目錄中,或者由java.ext.dirs系統變量指定的路徑中的全部類庫(如javax.*開頭的類),開發者能夠直接使用擴展類加載器spa

  • 應用程序類加載器 (Application ClassLoader), 負責加載用戶類路徑(ClassPath)所指定的類,開發者能夠直接使用該類加載器。若是沒有自定義的類加載器,通常這就是程序中默認的類加載器。線程

如上圖所示的這種層級結構成爲Java類加載器的雙親委派模型。當前加載器上層的叫作父加載器,但他們的關係不是靠繼承來實現,而是使用組合的方式。設計

雙親委派

若是一個類加載器收到加載類的請求,首先將請求委託給父加載器,依次向上層請求,直到頂端的啓動加載器,此時只有當該加載器找不到對應的類時,纔會讓子類去加載,直到找到該類。

Java設計者提出的這種約束模型,

  1. 首先具有了一種優先級的層次關係
  2. 其次保證Java程序運行的穩定和安全

例如要請求加載java.lang.Object類,最終全部的加載器都會委託啓動加載器去加載,從而保證了不管是哪個加載器想要加載Object類,最終都指向同一個類。

驗證

驗證是爲了確保Class文件中的字節流符合虛擬機的要求,而且不會損害虛擬機安全。

驗證大體分爲四個階段,

  1. 文件格式驗證,保證文件的字節流能被正確的解析,並被存儲到方法區中
  2. 元數據驗證,確保元數據信息符合Java語法規範
  3. 字節碼驗證,對類的方法體校驗,確保運行時不會危害到虛擬機
  4. 符號引用驗證,對類自身外的匹配校驗 (好比常量池中的符號引用)

準備

主要爲類變量分配內存並設置變量初始值,都在方法區中進行分配。

注意要點:

  1. 此時的內存分配只包括類變量 (static)。實例變量在對象初始化時分配在Java堆中。
  2. 此時變量的初始值是對應數據類型的默認零值(如0、null、false等)。

舉個例子,以下

public static int number = 6;
複製代碼

變量number在準備階段後的值爲0,而不是6。由於此時尚未開始執行Java方法,而將變量number賦值爲6是在程序編譯後,執行putstatic指令,存放於類的構造器<clinit>()方法中,因此number賦值爲6的操做將在初始化階段進行。

但若是上述變量前加上final關鍵字,則會在編譯期就將其結果放入常量池中,即準備階段後此時number值爲6.

解析

該階段虛擬機完成將常量池中符號引用轉化爲直接引用的過程。其中,

  • 符號引用,是以一組符號來描述所引用的目標,與虛擬機實現的內存佈局無關,即所引用的目標不必定加載到了內存中。
  • 直接引用,是一個指向目標的指針。與虛擬機實現的內存佈局相關,若是目標有了直接引用,說明已經被加載到了內存中。

解析主要針對類或接口、字段、類方法、接口方法四類符號引用,他們和常量池中的類型對應關係以下表,

符號引用 常量類型(常量池)
類或接口 CONSTANT_Class_info
字段 CONSTANT_Fieldref_info
類方法 CONSTANT_Methodref_info
接口方法 CONSTANT_InterfaceMethodref_info

初始化

此階段開始執行類中的Java代碼,主要執行的是類構造器<clinit>()方法。

<clinit>()是由編譯器自動收集類中全部類變量的賦值動做和靜態語句塊中的語句合併產生的,編譯器收集的順序由 語句在源文件中出現的順序決定。特別注意的是,靜態語句塊只能訪問到定義在它以前的類變量,定義在它以後的類 變量只能賦值,不能訪問。例如如下代碼:

public class Test {
    static { 
        i = 0; // 給變量賦值能夠正常編譯經過
        System.out.print(i); // 這句編譯器會提示「非法向前引用」
    } 
    static int i = 1;
}
複製代碼

因爲父類的 <clinit>()方法先執行,也就意味着父類中定義的靜態語句塊的執行要優先於子類。例如如下代碼:

static class Parent { 
    public static int A = 1; static { A = 2; } 
}

static class Sub extends Parent { 
    public static int B = A; 
} 

public static void main(String[] args) { 
    System.out.println(Sub.B); // 2 
}
複製代碼

接口中不可使用靜態語句塊,但仍然有類變量初始化的賦值操做,所以接口與類同樣都會生成 <clinit>()方法。但 接口與類不一樣的是,執行接口的 <clinit>()方法不須要先執行父接口的 <clinit>() 方法。只有當父接口中定義的變量使 用時,父接口才會初始化。另外,接口的實現類在初始化時也同樣不會執行接口的<clinit>() 方法。

虛擬機會保證一個類的 <clinit>() 方法在多線程環境下被正確的加鎖和同步,若是多個線程同時初始化一個類,只會 有一個線程執行這個類的 <clinit>() 方法,其它線程都會阻塞等待,直到活動線程執行 <clinit>() 方法完畢。若是在一 個類的 <clinit>() 方法中有耗時的操做,就可能形成多個線程阻塞,在實際過程當中此種阻塞很隱蔽。

相關文章
相關標籤/搜索