類加載器的主要任務(wù)就是根據(jù)一個(gè)類的全限定名來讀取此類的二進(jìn)制字節(jié)流到JVM內(nèi)部,然后轉(zhuǎn)化為一個(gè)與目標(biāo)類對(duì)應(yīng)的java.lang.Class對(duì)象實(shí)例。當(dāng)然類加載器所執(zhí)行的加載操作僅僅屬于JVM中加載過程的一個(gè)階段而已,一個(gè)完整的類加載過程必須經(jīng)歷加載、連接、初始化這3個(gè)步驟。如圖1所示:

圖 1 完整的類加載過程

類加載過程各階段的任務(wù),見圖2所示:

圖 2 各階段的任務(wù)

類的生命周期:

加載 loading

驗(yàn)證 verification

準(zhǔn)備 preparation

解析 resolution

初始化 initialization

使用 using

卸載 unloading

Java虛擬機(jī)規(guī)范在類的加載和連接的時(shí)機(jī)上提供了較大的靈活性,但Java虛擬機(jī)規(guī)范卻明確規(guī)定了類的初始化時(shí)機(jī),且分為主動(dòng)和被動(dòng)引用兩種;也就是說,一個(gè)類或者接口應(yīng)該在首次主動(dòng)使用時(shí)進(jìn)行初始化操作:

有且只有以下幾種情況必須立即對(duì)類進(jìn)行”初始化”(稱為對(duì)一個(gè)類進(jìn)行主動(dòng)引用):

(1)遇到new、getstatic、putstatic、invokestatic這四條字節(jié)碼指令時(shí)(使用new實(shí)例化對(duì)象的時(shí)候、讀取或設(shè)置一個(gè)類的靜態(tài)字段、調(diào)用一個(gè)類的靜態(tài)方法)。

(2)使用java.lang.reflet包的方法對(duì)類進(jìn)行反射調(diào)用的時(shí)候。

(3)當(dāng)初始化一個(gè)類的時(shí)候,如果發(fā)現(xiàn)其父類沒有進(jìn)行過初始化,則需要先觸發(fā)其父類的初始化。

(4)當(dāng)虛擬機(jī)啟動(dòng)時(shí),虛擬機(jī)會(huì)初始化主類(包含main方法的那個(gè)類)。

(5)調(diào)用一個(gè)類或接口的靜態(tài)字段,或者對(duì)這些靜態(tài)字段進(jìn)行賦值操作的時(shí)候(即節(jié)碼中,調(diào)用getstatic()或putstatic()方法指令),不過用final關(guān)鍵字修飾的靜態(tài)字段除外,它被初始化為一個(gè)編譯時(shí)的常量表達(dá)式。

被動(dòng)引用:

(1)通過子類引用父類的靜態(tài)字段,不會(huì)導(dǎo)致子類初始化(對(duì)于靜態(tài)字段,只有直接定義這個(gè)字段的類才會(huì)被初始化)。

(2)通過數(shù)組定義類應(yīng)用類:ClassA [] array=new ClassA[10]。觸發(fā)了一個(gè)名為[LClassA的類的初始化,它是一個(gè)由虛擬機(jī)自動(dòng)生成的、直接繼承于Object的類,創(chuàng)建動(dòng)作由字節(jié)碼指令newarray觸發(fā)。

(3)常量會(huì)在編譯階段存入調(diào)用類的常量池。

在此大家需要注意,盡管一個(gè)類在初始化之前必須要求它的父類提前完成初始化操作,但對(duì)于接口而言,這條規(guī)則卻顯得并不適用。編譯器會(huì)為接口生成()構(gòu)造器,用于初始化接口中定義的成員變量。一個(gè)接口在初始化時(shí),并不要求其父類接口全部完成了初始化,只有在真正使用到父接口的時(shí)候才會(huì)初始化。

一、加載Loading

加載任務(wù)是由類加載器所負(fù)責(zé)的,當(dāng)然類加載器所執(zhí)行的加載操作僅僅屬于JVM中加載過程的一個(gè)階段,同樣也是類加載過程的第一個(gè)階段,而后續(xù)還需要連接和初始化階段的配合才能構(gòu)成一個(gè)完整的類加載過程。加載:就是根據(jù)一個(gè)類的全限定名來讀取此類的二進(jìn)制字節(jié)流到JVM內(nèi)部,然后轉(zhuǎn)化為一個(gè)與目標(biāo)類對(duì)應(yīng)的java.lang.Class對(duì)象實(shí)例。又可細(xì)分以下3步:

(1)通過一個(gè)類的全限定名來獲取此類的二進(jìn)制字節(jié)流??梢允莄lass文件,可以是jar。

(2)將這個(gè)字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)。

(3)在java堆中生成一個(gè)代表這個(gè)類的Class對(duì)象,作為方法區(qū)這些數(shù)據(jù)的訪問入口。

參考《Java虛擬機(jī)規(guī)范(Java SE7版)》的描述,創(chuàng)建數(shù)組的類的情況稍微有些特殊,簡(jiǎn)單來說,數(shù)組類本身并不是類加載器負(fù)責(zé)創(chuàng)建,而是由JVM在運(yùn)行時(shí)根據(jù)需要而直接創(chuàng)建,但數(shù)組的元素類型仍然是依靠類加載器創(chuàng)建。

二、連接Linking

連接Linking階段要做的事情就是將已經(jīng)加載到JVM中的二進(jìn)制字節(jié)流的類數(shù)據(jù)信息合并到JVM的運(yùn)行時(shí)狀態(tài)中,然后該連接階段則由驗(yàn)證、準(zhǔn)備、解析3個(gè)階段構(gòu)成。

1.驗(yàn)證

這個(gè)階段驗(yàn)證讀取到的二進(jìn)制字節(jié)流是否符合虛擬機(jī)規(guī)范中Class文件的存儲(chǔ)格式,如果不符合,拋出java.lang.VerifyError異常或其子類的異常。

驗(yàn)證階段確保Class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求,并且不會(huì)危害虛擬機(jī)自身的安全。驗(yàn)證階段中的一些驗(yàn)證操作將會(huì)和加載階段一起執(zhí)行,而不會(huì)等待加載完成后才會(huì)執(zhí)行某些驗(yàn)證操作,比如當(dāng)一個(gè)類型的二進(jìn)制信息被加載到JVM內(nèi)部之前,就必須做文件格式驗(yàn)證,以免一些二進(jìn)制信息對(duì)JVM虛擬機(jī)產(chǎn)生不良影響,或者造成JVM進(jìn)程崩潰。驗(yàn)證階段大致可以劃分為:文件格式驗(yàn)證、語義分析、操作驗(yàn)證、符號(hào)引用驗(yàn)證。

驗(yàn)證階段分為四步:

(1)文件格式驗(yàn)證:驗(yàn)證字節(jié)流是否符合Class文件格式的規(guī)范,并且能被當(dāng)前版本的虛擬機(jī)處理。這個(gè)階段的驗(yàn)證時(shí)給予字節(jié)流進(jìn)行的,經(jīng)過了這個(gè)階段的驗(yàn)證之后,字節(jié)流才會(huì)進(jìn)入內(nèi)存的方法區(qū)中進(jìn)行存儲(chǔ)所以后面的驗(yàn)證階段都是給予方法區(qū)的存儲(chǔ)結(jié)構(gòu)進(jìn)行的。關(guān)于字節(jié)碼文件有效性驗(yàn)證

有以下這些驗(yàn)證點(diǎn):

*是否已魔數(shù)0xCAFEBABE開頭。

*主次版本號(hào)是否能被當(dāng)前虛擬機(jī)處理。

*常量池的常量中是否有不被支持的類型。

*常量池里的項(xiàng)是否執(zhí)行不存在的常量或不符合類型的常量。

(2)元數(shù)據(jù)驗(yàn)證:對(duì)類的元數(shù)據(jù)信息進(jìn)行語義校驗(yàn),保證不存在不符合java語言規(guī)范的元數(shù)據(jù)信息。

有以下這些驗(yàn)證點(diǎn):

*是否 有父類(除java.lang.Object外,所有類都應(yīng)有父類)。

*是否繼承了final類。

*如果不是抽象類,是否實(shí)現(xiàn)了所有需要實(shí)現(xiàn)的方法。

(3)字節(jié)碼驗(yàn)證:進(jìn)行數(shù)據(jù)流和控制流分析,對(duì)類的方法體進(jìn)行校驗(yàn)分析,保證被校驗(yàn)的類的方法在運(yùn)行時(shí)不會(huì)做出危害虛擬機(jī)安全的行為。

(4)符號(hào)引用驗(yàn)證:發(fā)生在虛擬機(jī)將符號(hào)引用轉(zhuǎn)化為直接引用的時(shí)候(解析階段),對(duì)常量池中的各種符號(hào)引用的信息進(jìn)行匹配性的校驗(yàn)。

2.準(zhǔn)備

連接階段的驗(yàn)證操作完成后,接下來JVM要做的事情就是對(duì)存放在方法區(qū)中類數(shù)據(jù)信息的類變量執(zhí)行初始化,這里所指的初始化僅僅是為類中的靜態(tài)變量分配內(nèi)存,并將其初始化為默認(rèn)值(由于沒有產(chǎn)生對(duì)象,因此實(shí)例變量將不在此操作范圍內(nèi)),而非用戶手動(dòng)賦值。如下表所示:

數(shù)據(jù)類型

默認(rèn)初始值

int

0

long

0L

short

(short)0

char

'u0000'

byte

(byte)0

boolean

false

float

0.0f

double

0.0d

reference

null

在此需要注意,JVM實(shí)現(xiàn)其實(shí)并不支持boolean類型,因此在JVM內(nèi)部,boolean類型往往被實(shí)現(xiàn)為一個(gè)int類型,初始值為0也就代表著false。當(dāng)然boolean類型的變量,盡管在JVM中當(dāng)作int來實(shí)現(xiàn),但在初始化時(shí)也總會(huì)被初始化為一個(gè)false值。

3.解析

解析階段是在虛擬機(jī)將常量池內(nèi)的符號(hào)引用替換為直接引用的過程。

什么是符號(hào)引用呢?符號(hào)引用以一組符號(hào)來描述所引用的目標(biāo),符號(hào)可以是任何形式的字面量,只要使用時(shí)能無歧義地定位到目標(biāo)即可。符號(hào)引用與虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局無關(guān),引用的目標(biāo)并不一定已經(jīng)加載到內(nèi)存中。

直接引用:直接引用可以是直接指向目標(biāo)的指針、相對(duì)偏移量或者一個(gè)能間接定位到目標(biāo)的句柄。如果有了直接引用,那引用的目標(biāo)必定已經(jīng)在內(nèi)存中存在。

要執(zhí)行解析操作的內(nèi)容包括:類或接口、類字段、類方法、接口中的方法。

(1)解析類或接口

類或接口(對(duì)應(yīng)于常量池的CONSTANT_Class_info類型)的解析:

假設(shè)當(dāng)前代碼所處的類為D,需要將一個(gè)從未解析過的符號(hào)引用N解析為一個(gè)類或接口C的直接引用:

i.如果C不是一個(gè)數(shù)組類型,虛擬機(jī)將會(huì)把代表C的全限定名傳遞給D的類加載器去加載這個(gè)類。

ii.如果C是一個(gè)數(shù)組類型,并且數(shù)組的元素類型為對(duì)象(N的描述符類似[Ljava.lang.Integer),將會(huì)加載數(shù)組元素類型(java.lang.Integer),接著由虛擬機(jī)生成一個(gè)代表此數(shù)組維度和元素的數(shù)組對(duì)象。

iii.如果以上過程沒有發(fā)生異常,則C在虛擬機(jī)中已經(jīng)成為了一個(gè)有效的類和接口了,之后還要進(jìn)行的是符號(hào)引用驗(yàn)證,確認(rèn)D是否具有對(duì)C的訪問權(quán)限,如果沒有,將拋出java.lang.IllegalAccessError異常。

(2)解析類字段

字段(對(duì)應(yīng)于常量池的CONSTANT_Fieldref_info類型)解析:

i.對(duì)字段表中的class_index項(xiàng)中索引的CONSTANT_Class_info符號(hào)引用進(jìn)行解析。用C表示這個(gè)字段所屬的類或接口。

ii.如果C本身就包含了簡(jiǎn)單名稱和字段描述符都與目標(biāo)相匹配的字段,則返回這個(gè)字段的直接引用。

iii.否則,如果C實(shí)現(xiàn)了接口,則會(huì)按照繼承關(guān)系從下往上遞歸搜索各個(gè)接口和他的父接口,如果接口中包含了簡(jiǎn)單名稱和字段描述符都與目標(biāo)相匹配的字段,則返回這個(gè)字段的直接引用。

iv.否則,如果C不是java.lang.Object類型的話,將會(huì)按照繼承關(guān)系從下往上遞歸的搜索其父類,如果在父類中包含了簡(jiǎn)單名稱和字段描述符都與目標(biāo)相匹配的字段,則返回這個(gè)字段的直接引用。

v.否則,查找失敗,拋出java.lang.NoSuchFieldError異常。

虛擬機(jī)的編譯器實(shí)現(xiàn)可能會(huì)更嚴(yán)格:如果一個(gè)同名字段同時(shí)出現(xiàn)在C實(shí)現(xiàn)的接口和父類中,或者同時(shí)在自己或父類的多個(gè)接口中出現(xiàn),編譯器將可能拒絕編譯。

(3)解析類方法

類方法(對(duì)應(yīng)于常量池的CONSTANT_Methodref_info類型)解析:

i.對(duì)方法表中的class_index項(xiàng)中索引的CONSTANT_Class_info符號(hào)引用進(jìn)行解析。用C表示這個(gè)方法所屬的類或接口。

ii.類方法和接口方法符號(hào)引用的常量類型定義是分開的,如果在類方法表中發(fā)現(xiàn)class_index中索引的C是個(gè)接口,則拋出java.lang.IncompatibleClassChangeError。

iii.在類C中查找是否有簡(jiǎn)單名稱和描述符都與目標(biāo)相匹配的方法,如果有則返回這個(gè)方法的直接引用。

iv.否則,在C的父類中遞歸查找是否有簡(jiǎn)單名稱和描述符都與目標(biāo)相匹配的方法,如果有則返回這個(gè)方法的直接引用。

v.否則,在C實(shí)現(xiàn)的接口列表及它們的父接口中遞歸的查找是否有簡(jiǎn)單名稱和描述符都與目標(biāo)相匹配的方法,如果有說明C是個(gè)抽象類,查找結(jié)束,拋出java.lang.AbstractMethodError異常。

vi.否則,查找失敗,拋出java.lang.NoSuchMethodError異常。

vii.如果查找返回了直接引用,將會(huì)對(duì)這個(gè)方法進(jìn)行權(quán)限驗(yàn)證,如果發(fā)現(xiàn)不具備對(duì)這個(gè)方法的訪問權(quán)限,則拋出java.lang.IllegalAccessError異常。

(4)解析接口方法

接口方法(對(duì)應(yīng)于常量池的CONSTANT_InterfaceMethodref_info類型):

i.對(duì)方法表中的class_index項(xiàng)中索引的CONSTANT_Class_info符號(hào)引用進(jìn)行解析。用C表示這個(gè)方法所屬的類或接口。

ii.如果在接口方法表中發(fā)現(xiàn)class_index中索引的C是個(gè)類,則拋出java.lang.IncompatibleClassChangeError。

iii.否則,在接口C中查找是否有簡(jiǎn)單名稱和描述符都與目標(biāo)相匹配的方法,如果有則返回這個(gè)方法的直接引用。

iv.否則,在接口C的父接口中遞歸查找,知道java.lang.Object類(包括在內(nèi)),看是否有簡(jiǎn)單名稱和描述符都與目標(biāo)相匹配的方法,如果有則返回這個(gè)方法的直接引用。

v.否則,查找失敗,拋出java.lang.NoSuchMethodError。

三、初始化Initiazation

初始化是類加載過程的最后一個(gè)階段,前面的類加載動(dòng)作,除了在加載階段用戶應(yīng)用程序可以通過自定義類加載器參與之外,其余動(dòng)作完全由虛擬機(jī)主導(dǎo)和控制。到了初始化階段,才真正執(zhí)行類中定義的Java程序代碼(或者說是字節(jié)碼)。在初始化階段,JVM會(huì)將一個(gè)類的所有被static關(guān)鍵字修飾的代碼統(tǒng)統(tǒng)執(zhí)行一遍,如果執(zhí)行的是靜態(tài)變量,那么會(huì)使用程序員指定的值覆蓋掉之前在準(zhǔn)備階段中JVM為其設(shè)置的初始值,當(dāng)然如果程序中沒有為靜態(tài)變量顯示的指定賦值操作,那么所持有的值仍然是之前的初始值;反之如果執(zhí)行的是static代碼塊,那么在初始化階段中,JVM就會(huì)將執(zhí)行static代碼塊中的操作。

所有的類變量[靜態(tài)變量]初始化語句和靜態(tài)代碼塊都會(huì)在Java源碼執(zhí)行字節(jié)碼編譯時(shí),被前端編譯器放在收集器里,存放到一個(gè)特殊的方法中,這個(gè)方法就是()方法。對(duì)于類來說,這個(gè)方法可稱為類初始化方法,而對(duì)于接口來說,則可稱為接口初始化方法。簡(jiǎn)單來說,()方法的作用就是初始化一個(gè)類中的靜態(tài)變量,使用程序員指定的值覆蓋掉之前在準(zhǔn)備階段中JVM為其設(shè)置的初始值。

()方法執(zhí)行過程--特點(diǎn):

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

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

3.由于父類的()方法先執(zhí)行,所就意味著父類中定義的靜態(tài)語句塊要優(yōu)先于子類的類變量賦值操作。

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

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

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

7.()是線程安全的。如果多個(gè)線程同時(shí)去初始化一個(gè)類,只有一個(gè)線程會(huì)去執(zhí)行初始化,其他的線程都會(huì)阻塞,僅僅允許其中一個(gè)線程對(duì)其初始化操作,完成后才會(huì)通知正在等待的其他線程。

8.任何invoke之類的字節(jié)碼指令也無法調(diào)用()方法,因?yàn)樵摲椒ㄖ荒茉陬惣虞d的過程中被JVM所調(diào)用。