類加載機(jī)制

類加載機(jī)制

虛擬機(jī)把描述類的數(shù)據(jù)從 Class 文件加載到內(nèi)存,并對(duì)數(shù)據(jù)進(jìn)行校驗(yàn),轉(zhuǎn)換解析,初始化,最終形成能被虛擬機(jī)直接使用的 Java 類型,這就是虛擬機(jī)的類加載機(jī)制。

類加載的時(shí)機(jī)

類從被加載到虛擬機(jī)開(kāi)始,到卸載出內(nèi)存為止,整個(gè)生命周期包括:

加載,驗(yàn)證,準(zhǔn)備,解析,初始化,使用,卸載

類加載.png

其中加載,驗(yàn)證,準(zhǔn)備,初始化,卸載這 5 個(gè)階段順序是確定的,類加載過(guò)程必須按照這種順序按部就班的開(kāi)始,而解析階段則不一定,解析階段在某些情況下可以在初始化階段之后再開(kāi)始,這是為了支持java語(yǔ)言的運(yùn)行時(shí)綁定(動(dòng)態(tài)綁定/晚期綁定)。

1 加載

加載階段主要完成 3 件事情:

  1. 通過(guò)類的全限定名來(lái)獲取此類的二進(jìn)制字節(jié)流
  2. 將這個(gè)字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)
  3. 在內(nèi)存中生成一個(gè)代表這個(gè)類的 java.lang.Class 對(duì)象,作為這個(gè)類的各種數(shù)據(jù)在方法區(qū)訪問(wèn)入口

2 驗(yàn)證

目的:確保 Class 文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求,并且不會(huì)損害虛擬機(jī)自身的安全。

  1. 文件格式校驗(yàn):字節(jié)流是否符合 Class 文件格式的規(guī)范,并能被當(dāng)前版本的虛擬機(jī)處理。
  2. 元數(shù)據(jù)校驗(yàn):字節(jié)碼描述的信息進(jìn)行語(yǔ)義分析,保證其描述的信息符合 Java 語(yǔ)言規(guī)范的要求。
  3. 字節(jié)碼校驗(yàn):通過(guò)數(shù)據(jù)流和控制流分析,確定程序語(yǔ)義是合法的,符合邏輯的。在第二階段對(duì)元數(shù)據(jù)信息中的數(shù)據(jù)類型做完校驗(yàn)后,這個(gè)階段將對(duì)類的方法體進(jìn)行校驗(yàn)分析,保證被校驗(yàn)的類的方法在運(yùn)行時(shí)不會(huì)做出危害虛擬機(jī)的安全事件。
  4. 符號(hào)引用校驗(yàn):這是最后一個(gè)階段的驗(yàn)證,它發(fā)生在虛擬機(jī)將符號(hào)引用轉(zhuǎn)化為直接引用的時(shí)候(解析階段中發(fā)生該轉(zhuǎn)化,后面會(huì)有講解),主要是對(duì)類自身以外的信息(常量池中的各種符號(hào)引用)進(jìn)行匹配性的校驗(yàn)。

3 準(zhǔn)備

準(zhǔn)備階段是正式為類變量分配內(nèi)存并設(shè)置類變量初始值的階段,這些內(nèi)存都將在方法區(qū)中分配。

對(duì)于該階段有以下幾點(diǎn)需要注意:

  1. 這時(shí)候進(jìn)行內(nèi)存分配的僅包括類變量(static),而不包括實(shí)例變量,實(shí)例變量會(huì)在對(duì)象實(shí)例化時(shí)隨著對(duì)象一塊分配在Java堆中。

  2. 這里所設(shè)置的初始值通常情況下是數(shù)據(jù)類型默認(rèn)的零值(如0、0L、null、false等),而不是被在Java代碼中被顯式地賦予的值。

    假設(shè)一個(gè)類變量的定義為:public static int value = 3;

    那么變量 value 在準(zhǔn)備階段過(guò)后的初始值為 0,而不是 3,因?yàn)檫@時(shí)候尚未開(kāi)始執(zhí)行任何 Java 方法,而把 value 賦值為 3 的 putstatic 指令是在程序編譯后,存放于類構(gòu)造器 <clinit>() 方法之中的,所以把value 賦值為 3 的動(dòng)作將在初始化階段才會(huì)執(zhí)行。

    這里還需要注意如下幾點(diǎn):

    • 對(duì)基本數(shù)據(jù)類型來(lái)說(shuō),對(duì)于類變量(static)和全局變量,如果不顯式地對(duì)其賦值而直接使用,則系統(tǒng)會(huì)為其賦予默認(rèn)的零值,而對(duì)于局部變量來(lái)說(shuō),在使用前必須顯式地為其賦值,否則編譯時(shí)不通過(guò)。
    • 對(duì)于同時(shí)被 static 和 final 修飾的常量,必須在聲明的時(shí)候就為其顯式地賦值,否則編譯時(shí)不通過(guò);而只被 final 修飾的常量則既可以在聲明時(shí)顯式地為其賦值,也可以在類初始化時(shí)顯式地為其賦值,總之,在使用前必須為其顯式地賦值,系統(tǒng)不會(huì)為其賦予默認(rèn)零值。
    • 對(duì)于引用數(shù)據(jù)類型 reference 來(lái)說(shuō),如數(shù)組引用、對(duì)象引用等,如果沒(méi)有對(duì)其進(jìn)行顯式地賦值而直接使用,系統(tǒng)都會(huì)為其賦予默認(rèn)的零值,即 null。
    • 如果在數(shù)組初始化時(shí)沒(méi)有對(duì)數(shù)組中的各元素賦值,那么其中的元素將根據(jù)對(duì)應(yīng)的數(shù)據(jù)類型而被賦予默認(rèn)的零值。
    數(shù)據(jù)類型 默認(rèn)零值 數(shù)據(jù)類型 默認(rèn)零值
    int 0 boolean false
    long 0L float 0.0f
    short (short) 0 double 0.0d
    char '\u000' reference null
    byte (byte) 0
  3. 如果類字段的字段屬性表中存在 ConstantValue 屬性,即同時(shí)被 fina l和 static 修飾,那么在準(zhǔn)備階段變量 value 就會(huì)被初始化為 ConstValue 屬性所指定的值。

假設(shè)上面的類變量value被定義為: public static final int value = 3;

編譯時(shí) Javac 將會(huì)為 valu e生成 ConstantValue 屬性,在準(zhǔn)備階段虛擬機(jī)就會(huì)根據(jù) ConstantValue 的設(shè)置將 value 賦值為3。我們可以理解為 static final 常量在編譯期就將其結(jié)果放入了調(diào)用它的類的常量池中。

4 解析

解析階段是虛擬機(jī)將常量池中的符號(hào)引用轉(zhuǎn)化為直接引用的過(guò)程。

  • 符號(hào)引用:符號(hào)引用以一組符號(hào)來(lái)描述所引用的目標(biāo),符號(hào)可以是任何形式的字面量,只要使用時(shí)能無(wú)歧義地定位到目標(biāo)即可。符號(hào)引用與虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局無(wú)關(guān),引用的目標(biāo)并不一定已經(jīng)加載到了內(nèi)存中。
  • 直接引用:直接引用可以是直接指向目標(biāo)的指針、相對(duì)偏移量或是一個(gè)能間接定位到目標(biāo)的句柄。直接引用是與虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局相關(guān)的,同一個(gè)符號(hào)引用在不同虛擬機(jī)實(shí)例上翻譯出來(lái)的直接引用一般不會(huì)相同。如果有了直接引用,那說(shuō)明引用的目標(biāo)必定已經(jīng)存在于內(nèi)存之中了。

前面說(shuō)解析階段可能開(kāi)始于初始化之前,也可能在初始化之后開(kāi)始,虛擬機(jī)會(huì)根據(jù)需要來(lái)判斷,到底是在類被加載器加載時(shí)就對(duì)常量池中的符號(hào)引用進(jìn)行解析(初始化之前),還是等到一個(gè)符號(hào)引用將要被使用前才去解析它(初始化之后)。

對(duì)同一個(gè)符號(hào)引用進(jìn)行多次解析請(qǐng)求時(shí)很常見(jiàn)的事情,虛擬機(jī)實(shí)現(xiàn)可能會(huì)對(duì)第一次解析的結(jié)果進(jìn)行緩存(在運(yùn)行時(shí)常量池中記錄直接引用,并把常量標(biāo)示為已解析狀態(tài)),從而避免解析動(dòng)作重復(fù)進(jìn)行。

解析動(dòng)作主要針對(duì)類或接口、字段、類方法、接口方法四類符號(hào)引用進(jìn)行,分別對(duì)應(yīng)于常量池中的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info四種常量類型。

  1. 類或接口的解析:判斷所要轉(zhuǎn)化成的直接引用是對(duì)數(shù)組類型,還是普通的對(duì)象類型的引用,從而進(jìn)行不同的解析。

  2. 字段解析:對(duì)字段進(jìn)行解析時(shí),會(huì)先在本類中查找是否包含有簡(jiǎn)單名稱和字段描述符都與目標(biāo)相匹配的字段,如果有,則查找結(jié)束;如果沒(méi)有,則會(huì)按照繼承關(guān)系從上往下遞歸搜索該類所實(shí)現(xiàn)的各個(gè)接口和它們的父接口,還沒(méi)有,則按照繼承關(guān)系從上往下遞歸搜索其父類,直至查找結(jié)束,查找流程如下圖所示:

    img
    class Super{  
        public static int m = 11;  
        static{  
            System.out.println("執(zhí)行了super類靜態(tài)語(yǔ)句塊");  
        }  
    }  
    class Father extends Super{  
        public static int m = 33;  
        static{  
            System.out.println("執(zhí)行了父類靜態(tài)語(yǔ)句塊");  
        }  
    }  
    class Child extends Father{  
        static{  
            System.out.println("執(zhí)行了子類靜態(tài)語(yǔ)句塊");  
        }  
    }  
    public class StaticTest{  
        public static void main(String[] args){  
            System.out.println(Child.m);  
        }  
    } 
    執(zhí)行了super類靜態(tài)語(yǔ)句塊
    執(zhí)行了父類靜態(tài)語(yǔ)句塊
    33
    //如果注釋掉Father類中對(duì)m定義的那一行,則輸出結(jié)果如下:
    執(zhí)行了super類靜態(tài)語(yǔ)句塊
    11
    
    static變量發(fā)生在靜態(tài)解析階段,也即是初始化之前,此時(shí)已經(jīng)將字段的符號(hào)引用轉(zhuǎn)化為了內(nèi)存引用,也便將它與對(duì)應(yīng)的類關(guān)聯(lián)在了一起,由于在子類中沒(méi)有查找到與m相匹配的字段,那么m便不會(huì)與子類關(guān)聯(lián)在一起,因此并不會(huì)觸發(fā)子類的初始化。
    最后需要注意:理論上是按照上述順序進(jìn)行搜索解析,但在實(shí)際應(yīng)用中,虛擬機(jī)的編譯器實(shí)現(xiàn)可能要比上述規(guī)范要求的更嚴(yán)格一些。如果有一個(gè)同名字段同時(shí)出現(xiàn)在該類的接口和父類中,或同時(shí)在自己或父類的接口中出現(xiàn),編譯器可能會(huì)拒絕編譯。如果對(duì)上面的代碼做些修改,將Super改為接口,并將Child類繼承Father類且實(shí)現(xiàn)Super接口,那么在編譯時(shí)會(huì)報(bào)出如下錯(cuò)誤:
    StaticTest.java:24: 對(duì) m 的引用不明確,F(xiàn)ather 中的 變量 m 和 Super 中的 變量 m 都匹配
    System.out.println(Child.m);
    
  3. 類方法解析:對(duì)類方法的解析與對(duì)字段解析的搜索步驟差不多,只是多了判斷該方法所處的是類還是接口的步驟,而且對(duì)類方法的匹配搜索,是先搜索父類,再搜索接口。

  4. 接口方法解析:與類方法解析步驟類似,知識(shí)接口不會(huì)有父類,因此,只遞歸向上搜索父接口就行了。

5 初始化

初始化是類加載過(guò)程的最后一步,到了此階段,才真正開(kāi)始執(zhí)行類中定義的Java程序代碼。在準(zhǔn)備階段,類變量已經(jīng)被賦過(guò)一次系統(tǒng)要求的初始值,而在初始化階段,則是根據(jù)程序員通過(guò)程序指定的主觀計(jì)劃去初始化類變量和其他資源,或者可以從另一個(gè)角度來(lái)表達(dá):初始化階段是執(zhí)行類構(gòu)造器<clinit>()方法的過(guò)程。

這里簡(jiǎn)單說(shuō)明下 <clinit>() 方法的執(zhí)行規(guī)則:

  1. <clinit>() 方法是由編譯器自動(dòng)收集類中的所有類變量的賦值動(dòng)作和靜態(tài)語(yǔ)句塊中的語(yǔ)句合并產(chǎn)生的,編譯器收集的順序是由語(yǔ)句在源文件中出現(xiàn)的順序所決定的,靜態(tài)語(yǔ)句塊中只能訪問(wèn)到定義在靜態(tài)語(yǔ)句塊之前的變量,定義在它之后的變量,在前面的靜態(tài)語(yǔ)句中可以賦值,但是不能訪問(wèn)。

  2. <clinit>() 方法與實(shí)例構(gòu)造器<init>() 方法(類的構(gòu)造函數(shù))不同,它不需要顯式地調(diào)用父類構(gòu)造器,虛擬機(jī)會(huì)保證在子類的 <clinit>() 方法執(zhí)行之前,父類的 <clinit>() 方法已經(jīng)執(zhí)行完畢。因此,在虛擬機(jī)中第一個(gè)被執(zhí)行的<clinit>() 方法的類肯定是 java.lang.Object。

  3. <clinit>() 方法對(duì)于類或接口來(lái)說(shuō)并不是必須的,如果一個(gè)類中沒(méi)有靜態(tài)語(yǔ)句塊,也沒(méi)有對(duì)類變量的賦值操作,那么編譯器可以不為這個(gè)類生成 <clinit>() 方法。

  4. 接口中不能使用靜態(tài)語(yǔ)句塊,但仍然有類變量(final static)初始化的賦值操作,因此接口與類一樣會(huì)生成 <clinit>() 方法。但是接口魚(yú)類不同的是:執(zhí)行接口的 <clinit>() 方法不需要先執(zhí)行父接口的 <clinit>() 方法,只有當(dāng)父接口中定義的變量被使用時(shí),父接口才會(huì)被初始化。另外,接口的實(shí)現(xiàn)類在初始化時(shí)也一樣不會(huì)執(zhí)行接口的 <clinit>() 方法。

  5. 虛擬機(jī)會(huì)保證一個(gè)類的 <clinit>() 方法在多線程環(huán)境中被正確地加鎖和同步,如果多個(gè)線程同時(shí)去初始化一個(gè)類,那么只會(huì)有一個(gè)線程去執(zhí)行這個(gè)類的 <clinit>() 方法,其他線程都需要阻塞等待,直到活動(dòng)線程執(zhí)行 <clinit>() 方法完畢。如果在一個(gè)類的 <clinit>() 方法中有耗時(shí)很長(zhǎng)的操作,那就可能造成多個(gè)線程阻塞,在實(shí)際應(yīng)用中這種阻塞往往是很隱蔽的。

下面給出一個(gè)簡(jiǎn)單的例子,以便更清晰地說(shuō)明如上規(guī)則:

class Father{  
    public static int a = 1;  
    static{  
        a = 2;  
    }  
}  
class Child extends Father{  
    public static int b = a;  
}  
public class ClinitTest{  
    public static void main(String[] args){  
        System.out.println(Child.b);  
    }  
}  
//執(zhí)行上面的代碼,會(huì)打印出2,也就是說(shuō)b的值被賦為了2。
首先在準(zhǔn)備階段為類變量分配內(nèi)存并設(shè)置類變量初始值,這樣 A 和 B 均被賦值為默認(rèn)值 0,而后再在調(diào)用<clinit>() 方法時(shí)給他們賦予程序中指定的值。當(dāng)我們調(diào)用 Child.b 時(shí),觸發(fā) Child 的 <clinit>() 方法,根據(jù)規(guī)則2,在此之前,要先執(zhí)行完其父類Father的 <clinit>() 方法,又根據(jù)規(guī)則 1,在執(zhí)行 <clinit>() 方法時(shí),需要按static語(yǔ)句或 static 變量賦值操作等在代碼中出現(xiàn)的順序來(lái)執(zhí)行相關(guān)的 static 語(yǔ)句,因此當(dāng)觸發(fā)執(zhí)行Father的<clinit>() 方法時(shí),會(huì)先將 a 賦值為 1,再執(zhí)行 static 語(yǔ)句塊中語(yǔ)句,將a賦值為2,而后再執(zhí)行Child類的<clinit>() 方法,這樣便會(huì)將b的賦值為 2.
//如果我們顛倒一下Father類中“public static int a = 1;”語(yǔ)句和“static語(yǔ)句塊”的順序,程序執(zhí)行后,則會(huì)打印出1。
很明顯是根據(jù)規(guī)則1,執(zhí)行Father的<clinit>()方法時(shí),根據(jù)順序先執(zhí)行了static語(yǔ)句塊中的內(nèi)容,后執(zhí)行了“public static int a = 1;”語(yǔ)句。
另外,在顛倒二者的順序之后,如果在static語(yǔ)句塊中對(duì)a進(jìn)行訪問(wèn)(比如將a賦給某個(gè)變量),在編譯時(shí)將會(huì)報(bào)錯(cuò),因?yàn)楦鶕?jù)規(guī)則1,它只能對(duì)a進(jìn)行賦值,而不能訪問(wèn)。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 代碼編譯的結(jié)果從本地機(jī)器碼轉(zhuǎn)變?yōu)樽止?jié)碼,是存儲(chǔ)格式發(fā)展的一小步,確實(shí)編譯語(yǔ)言發(fā)展的一大步。 虛擬機(jī)把描述類的數(shù)據(jù)從...
    胡二囧閱讀 977評(píng)論 0 0
  • 一、類加載的時(shí)機(jī) 從類被加載到虛擬機(jī)內(nèi)存中開(kāi)始,到卸載出內(nèi)存為止,它的整個(gè)生命周期分為7個(gè)階段,加載(Loadin...
    Jivanmoon閱讀 577評(píng)論 0 0
  • 在Class文件描述的各種信息,最終都需要加載到虛擬機(jī)中才能運(yùn)行和使用。了解虛擬機(jī)類加載機(jī)制,就需要弄懂下面兩個(gè)問(wèn)...
    塞外的風(fēng)閱讀 373評(píng)論 0 0
  • 原文地址[http://blog.csdn.net/ns_code/article/details/1788158...
    期待現(xiàn)在閱讀 276評(píng)論 0 2
  • 虛擬機(jī)把描述類的數(shù)據(jù)從Class文件加載到內(nèi)存,并對(duì)數(shù)據(jù)進(jìn)行校驗(yàn)、轉(zhuǎn)換解析和初始化,最終形成可以被虛擬機(jī)直接使用的...
    小村醫(yī)閱讀 615評(píng)論 1 4