(轉(zhuǎn)載+原創(chuàng))【深入Java虛擬機(jī)】之四:類加載機(jī)制
近期學(xué)習(xí)了類加載機(jī)制,管中窺豹,偶有心得。jvm學(xué)習(xí)路漫漫其修遠(yuǎn)兮,諸君共勉。文章最后附上個(gè)人學(xué)習(xí)后的思維導(dǎo)圖。
2014-01-08 00:09:27?蘭亭風(fēng)雨?
版權(quán)聲明:本文為博主原創(chuàng)文章,遵循?CC 4.0 BY-SA?版權(quán)協(xié)議,轉(zhuǎn)載請(qǐng)附上原文出處鏈接和本聲明。
本文鏈接:https://blog.csdn.net/mmc_maodun/article/details/17881581
轉(zhuǎn)載請(qǐng)注明出處:http://blog.csdn.net/ns_code/article/details/17881581
類加載過程
? ??類從被加載到虛擬機(jī)內(nèi)存中開始,到卸載出內(nèi)存為止,它的整個(gè)生命周期包括:加載、驗(yàn)證、準(zhǔn)備、解析、初始化、使用和卸載七個(gè)階段。它們開始的順序如下圖所示:

? ? 其中類加載的過程包括了加載、驗(yàn)證、準(zhǔn)備、解析、初始化五個(gè)階段。在這五個(gè)階段中,加載、驗(yàn)證、準(zhǔn)備和初始化這四個(gè)階段發(fā)生的順序是確定的,而解析階段則不一定,它在某些情況下可以在初始化階段之后開始,這是為了支持Java語言的運(yùn)行時(shí)綁定(也成為動(dòng)態(tài)綁定或晚期綁定)。另外注意這里的幾個(gè)階段是按順序開始,而不是按順序進(jìn)行或完成,因?yàn)檫@些階段通常都是互相交叉地混合進(jìn)行的,通常在一個(gè)階段執(zhí)行的過程中調(diào)用或激活另一個(gè)階段。
? ? 這里簡(jiǎn)要說明下Java中的綁定:綁定指的是把一個(gè)方法的調(diào)用與方法所在的類(方法主體)關(guān)聯(lián)起來,對(duì)java來說,綁定分為靜態(tài)綁定和動(dòng)態(tài)綁定:
靜態(tài)綁定:即前期綁定。在程序執(zhí)行前方法已經(jīng)被綁定,此時(shí)由編譯器或其它連接程序?qū)崿F(xiàn)。針對(duì)java,簡(jiǎn)單的可以理解為程序編譯期的綁定。java當(dāng)中的方法只有final,static,private和構(gòu)造方法是前期綁定的。
動(dòng)態(tài)綁定:即晚期綁定,也叫運(yùn)行時(shí)綁定。在運(yùn)行時(shí)根據(jù)具體對(duì)象的類型進(jìn)行綁定。在java中,幾乎所有的方法都是后期綁定的。
? ? 下面詳細(xì)講述類加載過程中每個(gè)階段所做的工作。
? ?加載
? ? 加載時(shí)類加載過程的第一個(gè)階段,在加載階段,虛擬機(jī)需要完成以下三件事情:
? ? 1、通過一個(gè)類的全限定名來獲取其定義的二進(jìn)制字節(jié)流。
? ? 2、將這個(gè)字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)。
? ? 3、在Java堆中生成一個(gè)代表這個(gè)類的java.lang.Class對(duì)象,作為對(duì)方法區(qū)中這些數(shù)據(jù)的訪問入口。
? ? 注意,這里第1條中的二進(jìn)制字節(jié)流并不只是單純地從Class文件中獲取,比如它還可以從Jar包中獲取、從網(wǎng)絡(luò)中獲?。ㄗ畹湫偷膽?yīng)用便是Applet)、由其他文件生成(JSP應(yīng)用)等。
? ? 相對(duì)于類加載的其他階段而言,加載階段(準(zhǔn)確地說,是加載階段獲取類的二進(jìn)制字節(jié)流的動(dòng)作)是可控性最強(qiáng)的階段,因?yàn)殚_發(fā)人員既可以使用系統(tǒng)提供的類加載器來完成加載,也可以自定義自己的類加載器來完成加載。
? ? 加載階段完成后,虛擬機(jī)外部的 二進(jìn)制字節(jié)流就按照虛擬機(jī)所需的格式存儲(chǔ)在方法區(qū)之中,而且在Java堆中也創(chuàng)建一個(gè)java.lang.Class類的對(duì)象,這樣便可以通過該對(duì)象訪問方法區(qū)中的這些數(shù)據(jù)。
? ??說到加載,不得不提到類加載器,下面就具體講述下類加載器。
? ? 類加載器雖然只用于實(shí)現(xiàn)類的加載動(dòng)作,但它在Java程序中起到的作用卻遠(yuǎn)遠(yuǎn)不限于類的加載階段。對(duì)于任意一個(gè)類,都需要由它的類加載器和這個(gè)類本身一同確定其在就Java虛擬機(jī)中的唯一性,也就是說,即使兩個(gè)類來源于同一個(gè)Class文件,只要加載它們的類加載器不同,那這兩個(gè)類就必定不相等。這里的“相等”包括了代表類的Class對(duì)象的equals()、isAssignableFrom()、isInstance()等方法的返回結(jié)果,也包括了使用instanceof關(guān)鍵字對(duì)對(duì)象所屬關(guān)系的判定結(jié)果。
? ? 站在Java虛擬機(jī)的角度來講,只存在兩種不同的類加載器:
啟動(dòng)類加載器:它使用C++實(shí)現(xiàn)(這里僅限于Hotspot,也就是JDK1.5之后默認(rèn)的虛擬機(jī),有很多其他的虛擬機(jī)是用Java語言實(shí)現(xiàn)的),是虛擬機(jī)自身的一部分。
所有其他的類加載器:這些類加載器都由Java語言實(shí)現(xiàn),獨(dú)立于虛擬機(jī)之外,并且全部繼承自抽象類java.lang.ClassLoader,這些類加載器需要由啟動(dòng)類加載器加載到內(nèi)存中之后才能去加載其他的類。
? ? 站在Java開發(fā)人員的角度來看,類加載器可以大致劃分為以下三類:
啟動(dòng)類加載器:Bootstrap ClassLoader,跟上面相同。它負(fù)責(zé)加載存放在JDK\jre\lib(JDK代表JDK的安裝目錄,下同)下,或被-Xbootclasspath參數(shù)指定的路徑中的,并且能被虛擬機(jī)識(shí)別的類庫(如rt.jar,所有的java.*開頭的類均被Bootstrap ClassLoader加載)。啟動(dòng)類加載器是無法被Java程序直接引用的。
擴(kuò)展類加載器:Extension ClassLoader,該加載器由sun.misc.Launcher$ExtClassLoader實(shí)現(xiàn),它負(fù)責(zé)加載JDK\jre\lib\ext目錄中,或者由java.ext.dirs系統(tǒng)變量指定的路徑中的所有類庫(如javax.*開頭的類),開發(fā)者可以直接使用擴(kuò)展類加載器。
應(yīng)用程序類加載器:Application ClassLoader,該類加載器由sun.misc.Launcher$AppClassLoader來實(shí)現(xiàn),它負(fù)責(zé)加載用戶類路徑(ClassPath)所指定的類,開發(fā)者可以直接使用該類加載器,如果應(yīng)用程序中沒有自定義過自己的類加載器,一般情況下這個(gè)就是程序中默認(rèn)的類加載器。
? ? ?應(yīng)用程序都是由這三種類加載器互相配合進(jìn)行加載的,如果有必要,我們還可以加入自定義的類加載器。因?yàn)镴VM自帶的ClassLoader只是懂得從本地文件系統(tǒng)加載標(biāo)準(zhǔn)的java class文件,因此如果編寫了自己的ClassLoader,便可以做到如下幾點(diǎn):
?1)在執(zhí)行非置信代碼之前,自動(dòng)驗(yàn)證數(shù)字簽名。
?2)動(dòng)態(tài)地創(chuàng)建符合用戶特定需要的定制化構(gòu)建類。
?3)從特定的場(chǎng)所取得java class,例如數(shù)據(jù)庫中和網(wǎng)絡(luò)中。
事實(shí)上當(dāng)使用Applet的時(shí)候,就用到了特定的ClassLoader,因?yàn)檫@時(shí)需要從網(wǎng)絡(luò)上加載java class,并且要檢查相關(guān)的安全信息,應(yīng)用服務(wù)器也大都使用了自定義的ClassLoader技術(shù)。
? ? 這幾種類加載器的層次關(guān)系如下圖所示:

? ? 這種層次關(guān)系稱為類加載器的雙親委派模型。我們把每一層上面的類加載器叫做當(dāng)前層類加載器的父加載器,當(dāng)然,它們之間的父子關(guān)系并不是通過繼承關(guān)系來實(shí)現(xiàn)的,而是使用組合關(guān)系來復(fù)用父加載器中的代碼。該模型在JDK1.2期間被引入并廣泛應(yīng)用于之后幾乎所有的Java程序中,但它并不是一個(gè)強(qiáng)制性的約束模型,而是Java設(shè)計(jì)者們推薦給開發(fā)者的一種類的加載器實(shí)現(xiàn)方式。
? ? 雙親委派模型的工作流程是:如果一個(gè)類加載器收到了類加載的請(qǐng)求,它首先不會(huì)自己去嘗試加載這個(gè)類,而是把請(qǐng)求委托給父加載器去完成,依次向上,因此,所有的類加載請(qǐng)求最終都應(yīng)該被傳遞到頂層的啟動(dòng)類加載器中,只有當(dāng)父加載器在它的搜索范圍中沒有找到所需的類時(shí),即無法完成該加載,子加載器才會(huì)嘗試自己去加載該類。
? ? 使用雙親委派模型來組織類加載器之間的關(guān)系,有一個(gè)很明顯的好處,就是Java類隨著它的類加載器(說白了,就是它所在的目錄)一起具備了一種帶有優(yōu)先級(jí)的層次關(guān)系,這對(duì)于保證Java程序的穩(wěn)定運(yùn)作很重要。例如,類java.lang.Object類存放在JDK\jre\lib下的rt.jar之中,因此無論是哪個(gè)類加載器要加載此類,最終都會(huì)委派給啟動(dòng)類加載器進(jìn)行加載,這邊保證了Object類在程序中的各種類加載器中都是同一個(gè)類。
? ?驗(yàn)證
? ? 驗(yàn)證的目的是為了確保Class文件中的字節(jié)流包含的信息符合當(dāng)前虛擬機(jī)的要求,而且不會(huì)危害虛擬機(jī)自身的安全。不同的虛擬機(jī)對(duì)類驗(yàn)證的實(shí)現(xiàn)可能會(huì)有所不同,但大致都會(huì)完成以下四個(gè)階段的驗(yàn)證:文件格式的驗(yàn)證、元數(shù)據(jù)的驗(yàn)證、字節(jié)碼驗(yàn)證和符號(hào)引用驗(yàn)證。
文件格式的驗(yàn)證:驗(yàn)證字節(jié)流是否符合Class文件格式的規(guī)范,并且能被當(dāng)前版本的虛擬機(jī)處理,該驗(yàn)證的主要目的是保證輸入的字節(jié)流能正確地解析并存儲(chǔ)于方法區(qū)之內(nèi)。經(jīng)過該階段的驗(yàn)證后,字節(jié)流才會(huì)進(jìn)入內(nèi)存的方法區(qū)中進(jìn)行存儲(chǔ),后面的三個(gè)驗(yàn)證都是基于方法區(qū)的存儲(chǔ)結(jié)構(gòu)進(jìn)行的。
元數(shù)據(jù)驗(yàn)證:對(duì)類的元數(shù)據(jù)信息進(jìn)行語義校驗(yàn)(其實(shí)就是對(duì)類中的各數(shù)據(jù)類型進(jìn)行語法校驗(yàn)),保證不存在不符合Java語法規(guī)范的元數(shù)據(jù)信息。
字節(jié)碼驗(yàn)證:該階段驗(yàn)證的主要工作是進(jìn)行數(shù)據(jù)流和控制流分析,對(duì)類的方法體進(jìn)行校驗(yàn)分析,以保證被校驗(yàn)的類的方法在運(yùn)行時(shí)不會(huì)做出危害虛擬機(jī)安全的行為。
符號(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)。
? ?準(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)備階段過后的初始值為0,而不是3,因?yàn)檫@時(shí)候尚未開始執(zhí)行任何Java方法,而把value賦值為3的putstatic指令是在程序編譯后,存放于類構(gòu)造器<clinit>()方法之中的,所以把value賦值為3的動(dòng)作將在初始化階段才會(huì)執(zhí)行。
? ? 下表列出了Java中所有基本數(shù)據(jù)類型以及reference類型的默認(rèn)零值:

? ?這里還需要注意如下幾點(diǎn):
對(duì)基本數(shù)據(jù)類型來說,對(duì)于類變量(static)和全局變量,如果不顯式地對(duì)其賦值而直接使用,則系統(tǒng)會(huì)為其賦予默認(rèn)的零值,而對(duì)于局部變量來說,在使用前必須顯式地為其賦值,否則編譯時(shí)不通過。
對(duì)于同時(shí)被static和final修飾的常量,必須在聲明的時(shí)候就為其顯式地賦值,否則編譯時(shí)不通過;而只被final修飾的常量則既可以在聲明時(shí)顯式地為其賦值,也可以在類初始化時(shí)顯式地為其賦值,總之,在使用前必須為其顯式地賦值,系統(tǒng)不會(huì)為其賦予默認(rèn)零值。
對(duì)于引用數(shù)據(jù)類型reference來說,如數(shù)組引用、對(duì)象引用等,如果沒有對(duì)其進(jìn)行顯式地賦值而直接使用,系統(tǒng)都會(huì)為其賦予默認(rèn)的零值,即null。
如果在數(shù)組初始化時(shí)沒有對(duì)數(shù)組中的各元素賦值,那么其中的元素將根據(jù)對(duì)應(yīng)的數(shù)據(jù)類型而被賦予默認(rèn)的零值。
? ??3、如果類字段的字段屬性表中存在ConstantValue屬性,即同時(shí)被final和static修飾,那么在準(zhǔn)備階段變量value就會(huì)被初始化為ConstValue屬性所指定的值。
? ?假設(shè)上面的類變量value被定義為:?
public static final int value = 3;
? ? 編譯時(shí)Javac將會(huì)為value生成ConstantValue屬性,在準(zhǔn)備階段虛擬機(jī)就會(huì)根據(jù)ConstantValue的設(shè)置將value賦值為3?;貞?strong>上一篇博文中對(duì)象被動(dòng)引用的第2個(gè)例子,便是這種情況。我們可以理解為static final常量在編譯期就將其結(jié)果放入了調(diào)用它的類的常量池中。
? ?解析
? ?解析階段是虛擬機(jī)將常量池中的符號(hào)引用轉(zhuǎn)化為直接引用的過程。在Class類文件結(jié)構(gòu)一文中已經(jīng)比較過了符號(hào)引用和直接引用的區(qū)別和關(guān)聯(lián),這里不再贅述。前面說解析階段可能開始于初始化之前,也可能在初始化之后開始,虛擬機(jī)會(huì)根據(jù)需要來判斷,到底是在類被加載器加載時(shí)就對(duì)常量池中的符號(hào)引用進(jìn)行解析(初始化之前),還是等到一個(gè)符號(hào)引用將要被使用前才去解析它(初始化之后)。
? ? 對(duì)同一個(gè)符號(hào)引用進(jìn)行多次解析請(qǐng)求時(shí)很常見的事情,虛擬機(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é)束;如果沒有,則會(huì)按照繼承關(guān)系從上往下遞歸搜索該類所實(shí)現(xiàn)的各個(gè)接口和它們的父接口,還沒有,則按照繼承關(guān)系從上往下遞歸搜索其父類,直至查找結(jié)束,查找流程如下圖所示:

???從下面一段代碼的執(zhí)行結(jié)果中很容易看出來字段解析的搜索順序:

? ? 執(zhí)行結(jié)果如下:
? ? 執(zhí)行了super類靜態(tài)語句塊
? ? 執(zhí)行了父類靜態(tài)語句塊
? ? 33
? ? 如果注釋掉Father類中對(duì)m定義的那一行,則輸出結(jié)果如下:
? ? 執(zhí)行了super類靜態(tài)語句塊
? ? 11
? ??這里我們便可以分析如下:static變量發(fā)生在靜態(tài)解析階段,也即是初始化之前,此時(shí)已經(jīng)將字段的符號(hào)引用轉(zhuǎn)化為了內(nèi)存引用,也便將它與對(duì)應(yīng)的類關(guān)聯(lián)在了一起,由于在子類中沒有查找到與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);
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ^
1 錯(cuò)誤
? ??3、類方法解析:對(duì)類方法的解析與對(duì)字段解析的搜索步驟差不多,只是多了判斷該方法所處的是類還是接口的步驟,而且對(duì)類方法的匹配搜索,是先搜索父類,再搜索接口。
? ??4、接口方法解析:與類方法解析步驟類似,知識(shí)接口不會(huì)有父類,因此,只遞歸向上搜索父接口就行了。
? ??初始化
? ??初始化是類加載過程的最后一步,到了此階段,才真正開始執(zhí)行類中定義的Java程序代碼。在準(zhǔn)備階段,類變量已經(jīng)被賦過一次系統(tǒng)要求的初始值,而在初始化階段,則是根據(jù)程序員通過程序指定的主觀計(jì)劃去初始化類變量和其他資源,或者可以從另一個(gè)角度來表達(dá):初始化階段是執(zhí)行類構(gòu)造器<clinit>()方法的過程。
? ?這里簡(jiǎn)單說明下<clinit>()方法的執(zhí)行規(guī)則:
? ? 1、<clinit>()方法是由編譯器自動(dòng)收集類中的所有類變量的賦值動(dòng)作和靜態(tài)語句塊中的語句合并產(chǎn)生的,編譯器收集的順序是由語句在源文件中出現(xiàn)的順序所決定的,靜態(tài)語句塊中只能訪問到定義在靜態(tài)語句塊之前的變量,定義在它之后的變量,在前面的靜態(tài)語句中可以賦值,但是不能訪問。
? ? 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ì)于類或接口來說并不是必須的,如果一個(gè)類中沒有靜態(tài)語句塊,也沒有對(duì)類變量的賦值操作,那么編譯器可以不為這個(gè)類生成<clinit>()方法。
? ? 4、接口中不能使用靜態(tài)語句塊,但仍然有類變量(final static)初始化的賦值操作,因此接口與類一樣會(huì)生成<clinit>()方法。但是接口魚類不同的是:執(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)單的例子,以便更清晰地說明如上規(guī)則:

? ? ?執(zhí)行上面的代碼,會(huì)打印出2,也就是說b的值被賦為了2。
? ? 我們來看得到該結(jié)果的步驟。首先在準(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語句或static變量賦值操作等在代碼中出現(xiàn)的順序來執(zhí)行相關(guān)的static語句,因此當(dāng)觸發(fā)執(zhí)行Father的<clinit>()方法時(shí),會(huì)先將a賦值為1,再執(zhí)行static語句塊中語句,將a賦值為2,而后再執(zhí)行Child類的<clinit>()方法,這樣便會(huì)將b的賦值為2.
? ??如果我們顛倒一下Father類中“public static int a = 1;”語句和“static語句塊”的順序,程序執(zhí)行后,則會(huì)打印出1。很明顯是根據(jù)規(guī)則1,執(zhí)行Father的<clinit>()方法時(shí),根據(jù)順序先執(zhí)行了static語句塊中的內(nèi)容,后執(zhí)行了“public static int a = 1;”語句。
? ? 另外,在顛倒二者的順序之后,如果在static語句塊中對(duì)a進(jìn)行訪問(比如將a賦給某個(gè)變量),在編譯時(shí)將會(huì)報(bào)錯(cuò),因?yàn)楦鶕?jù)規(guī)則1,它只能對(duì)a進(jìn)行賦值,而不能訪問。
總結(jié)
?? ? 整個(gè)類加載過程中,除了在加載階段用戶應(yīng)用程序可以自定義類加載器參與之外,其余所有的動(dòng)作完全由虛擬機(jī)主導(dǎo)和控制。到了初始化才開始執(zhí)行類中定義的Java程序代碼(亦及字節(jié)碼),但這里的執(zhí)行代碼只是個(gè)開端,它僅限于<clinit>()方法。類加載過程中主要是將Class文件(準(zhǔn)確地講,應(yīng)該是類的二進(jìn)制字節(jié)流)加載到虛擬機(jī)內(nèi)存中,真正執(zhí)行字節(jié)碼的操作,在加載完成后才真正開始。
