理解JVM(四):JVM類加載機制

Class文件

咱們寫的Java代碼,通過編譯器編譯以後,就成爲了.class文件,從本地機器碼變成了字節碼。Class文件是一組以8位字節爲基礎單位的二進制流,各個數據項目嚴格按照順序緊湊地排列在Class文件之中,中間沒有添加任何分隔符,這使得整個Class文件中存儲的內容幾乎所有是程序運行的必要數據,沒有空隙存在。Class文件中只有2種數據結構:無符號數和表。html

每一個Class文件的頭4個字節稱爲魔數(Magic Number),值爲0xCAFEBABE。緊接着4個字節是Class文件的版本號。再日後,就是類的具體信息了,好比常量池、類索引、父類索引、接口索引、字段、方法等信息了。java

所謂類的加載,就是把Class文件讀到內存中。mysql

類的生命週期

類從被加載到虛擬機內存中開始,到卸載出內存爲止,它的整個生命週期包括:加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸載(Unloading)7個階段。其中驗證、準備、解析3個部分統稱爲鏈接(Linking)。

加載、驗證、準備、初始化和卸載這5個階段的順序是肯定的,類的加載過程必須按照這種順序循序漸進地開始,而解析階段則不必定:它在某些狀況下能夠在初始化階段以後再開始,這是爲了支持Java語言的運行時綁定(也稱爲動態綁定或晚期綁定)。注意,是循序漸進地「開始」,而不是循序漸進地「進行」或「完成」,強調這點是由於這些階段一般都是互相交叉地混合式進行的,一般會在一個階段執行的過程當中調用、激活另一個階段。sql

加載

在加載階段,虛擬機作3件事:緩存

  1. 經過一個類的全限定名來獲取定義此類的二進制字節流。
  2. 將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構。
  3. 在內存中生成一個表明這個類的java.lang.Class對象,做爲方法區這個類的各類數據的訪問入口。

驗證

驗證是鏈接階段的第一步,這一階段的目的是爲了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,而且不會危害虛擬機自身的安全。安全

驗證階段大體上會完成4個階段的檢驗動做數據結構

  1. 文件格式驗證:驗證字節流是否符合Class文件格式的規範,而且能被當前版本的虛擬機處理。好比是否以魔數0xCAFEBABE開頭,主、次版本號是否能被當前虛擬機處理,常量類型,指向常量的索引是否符合要求等。這階段的驗證是基於二進制字節流進行的,只有經過了這個階段的驗證後,字節流纔會進入內存的方法區中進行存儲,因此後面的3個驗證階段所有是基於方法區的存儲結構進行的,不會再直接操做字節流。
  2. 元數據驗證:對字節碼描述的信息進行語義分析,以保證其描述的信息符合Java語言規範的要求。好比繼承關係。
  3. 字節碼驗證:對類的方法體進行校驗分析,保證被校驗類的方法在運行時不會作出危害虛擬機安全的事件。經過數據流和控制流分析,肯定程序語義是合法的、符合邏輯的。
  4. 符號引用驗證:對類自身之外(常量池中的各類符號引用)的信息進行匹配性校驗,確保解析動做能正常執行。它發生在虛擬機將符號引用轉化爲直接引用的時候,這個轉化動做將在鏈接的第三階段——解析階段中發生。

驗證階段是很是重要的,但不是必須的。它對程序運行期沒有影響,若是所引用的類通過反覆驗證,那麼能夠考慮採用-Xverify:none參數來關閉大部分的類驗證措施,以縮短虛擬機類加載的時間。多線程

準備

準備階段是正式爲類變量分配內存並設置類變量初始值的階段,這些變量所使用的內存都將在方法區中進行分配。這個階段中有兩個容易產生混淆的概念須要強調一下,首先,這時候進行內存分配的僅包括類變量(被static修飾的變量),而不包括實例變量,實例變量將會在對象實例化時隨着對象一塊兒分配在Java堆中。其次,這裏所說的初始值「一般狀況」下是數據類型的零值。jvm

假設一個類變量的定義爲:public static int value = 123;源碼分析

那變量value在準備階段事後的初始值爲0而不是123,由於這時候還沒有開始執行任何Java 方法,而把value賦值爲123的putstatic指令是程序被編譯後,存放於類構造器<clinit>()方 法之中,因此把value賦值爲123的動做將在初始化階段纔會執行。

固然也有特殊狀況:若是類字段的字段屬性表中存在ConstantValue屬性,那在準備階段變量value就會被初始化爲ConstantValue屬性所指定的值。

假設上面類變量value的定義變爲:public static final int value = 123;

編譯時Javac將會爲value生成ConstantValue屬性,在準備階段虛擬機就會根據ConstantValue的設置將value賦值爲123。

解析

解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程。解析動做主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號引用進行。

  • 符號引用(Symbolic References):符號引用以一組符號來描述所引用的目標,符號可 以是任何形式的字面量,只要使用時能無歧義地定位到目標便可。符號引用與虛擬機實現的 內存佈局無關,引用的目標並不必定已經加載到內存中。各類虛擬機實現的內存佈局能夠各 不相同,可是它們能接受的符號引用必須都是一致的,由於符號引用的字面量形式明肯定義 在Java虛擬機規範的Class文件格式中。
  • 直接引用(Direct References):直接引用能夠是直接指向目標的指針、相對偏移量或是 一個能間接定位到目標的句柄。直接引用是和虛擬機實現的內存佈局相關的,同一個符號引 用在不一樣虛擬機實例上翻譯出來的直接引用通常不會相同。若是有了直接引用,那引用的目 標一定已經在內存中存在。

初始化

這一步開始執行類中定義的Java程序代碼(或者說是字節碼)。虛擬機會保證一個類的初始化方法在多線程環境中被正確地加鎖、同步,若是多個線程同時去初始化一個類,那麼只會有一個線程去執行這個類的初始化方法,其餘線程都須要阻塞等待,直到活動線程執行完畢。

JVM初始化步驟

  1. 假如這個類尚未被加載和鏈接,則程序先加載並鏈接該類
  2. 假如該類的直接父類尚未被初始化,則先初始化其直接父類
  3. 假如類中有初始化語句,則系統依次執行這些初始化語句

類初始化時機

只有當主動使用一個類的時候纔會觸發這個類的初始化,類的主動使用包括如下六種:

  • 建立類的實例,也就是new的方式
  • 訪問某個類或接口的靜態變量,或者對該靜態變量賦值
  • 調用類的靜態方法
  • 反射,好比Class.forName("com.mysql.jdbc.Driver")
  • 初始化某個類的子類,則其父類也會被初始化
  • Java虛擬機啓動時被標明爲啓動類的類(Java Test),直接使用java.exe命令來運行某個主類

類加載器

虛擬機設計團隊把類加載階段中的「經過一個類的全限定名來獲取描述此類的二進制字節流」這個動做放到Java虛擬機外部去實現,以便讓應用程序本身決定如何去獲取所須要的類。實現這個動做的代碼模塊稱爲「類加載器」。

雙親委派模型

從Java虛擬機的角度來說,只存在兩種不一樣的類加載器:一種是啓動類加載器(Bootstrap ClassLoader),這個類加載器使用C++語言實現,是虛擬機自身的一部分;另外一種就是全部其餘的類加載器,這些類加載器都由Java語言實現,獨立於虛擬機外部,而且全都繼承自抽象類java.lang.ClassLoader。

從Java開發人員的角度來看,類加載器能夠劃分爲如下3種:

  1. 啓動類加載器(Bootstrap ClassLoader):負責加載存放在JAVA_HOME\lib目錄中的,或被-Xbootclasspath參數指定的路徑中的,而且能被虛擬機識別的類庫(如rt.jar,全部的java.開頭的類均被Bootstrap ClassLoader加載)。啓動類加載器是沒法被Java程序直接引用的。
  2. 擴展類加載器(Extension ClassLoader):這個加載器由sun.misc.Launcher$ExtClassLoader實現,它負責加載JAVA_HOME\lib\ext目錄中的,或者被java.ext.dirs系統變量所指定的路徑中的全部類庫,開發者能夠直接使用擴展類加載器。
  3. 應用程序類加載器(Application ClassLoader):該類加載器由sun.misc.Launcher$AppClassLoader來實現,它負責加載用戶類路徑(ClassPath)所指定的類,開發者能夠直接使用該類加載器,若是應用程序中沒有自定義過本身的類加載器,通常狀況下這個就是程序中默認的類加載器。

咱們的應用程序都是由這3種類加載器互相配合進行加載的,若是有必要,還能夠加入 本身定義的類加載器。

雙親委派模型要求除了頂層的啓動類加載器外,其他的類加載器都應當有本身的父類加載器。這裏類加載器之間的父子關係通常不會以繼承的關係來實現,而是都使用組合關係來複用父加載器的代碼。它不是強制性的約束模型,而是Java設計者推薦的一種類加載器實現方式。

雙親委派模型的工做過程:若是一個類加載器收到了類加載的請求,它首先不會本身去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每個層次的類加載器都是如此,所以全部的加載請求最終都應該傳送到頂層的啓動類加載器中,只有當父加載器反饋本身沒法完成這個加載請求(它的搜索範圍中沒有找到所需的類)時,子加載器纔會嘗試本身去加載。

ClassLoader源碼分析:

public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
}

protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        // 首先判斷該類型是否已經被加載
        Class c = findLoadedClass(name);
        if (c == null) {
            //若是沒有被加載,就委託給父類加載或者委派給啓動類加載器加載
            try {
                if (parent != null) {
                     //若是存在父類加載器,就委派給父類加載器加載
                    c = parent.loadClass(name, false);
                } else {
                    //若是不存在父類加載器,就檢查是不是由啓動類加載器加載的類,經過調用本地方法native Class findBootstrapClass(String name)
                    c = findBootstrapClass0(name);
                }
            } catch (ClassNotFoundException e) {
                // 若是父類加載器和啓動類加載器都不能完成加載任務,才調用自身的加載功能
                c = findClass(name);
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
複製代碼

經過分析源碼,咱們知道,雙親委派模型能夠保證每一個類都只會被加載一次(相似緩存機制)。

參考

相關文章
相關標籤/搜索