類加載器的主要任務就是根據一個類的全限定名來讀取此類的二進制字節流到JVM內部,然后轉化為一個與目標類對應的java.lang.Class對象實例。當然類加載器所執行的加載操作僅僅屬于JVM中加載過程的一個階段而已,一個完整的類加載過程必須經歷加載、連接、初始化這3個步驟。如圖1所示:

圖 1 完整的類加載過程

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

圖 2 各階段的任務

類的生命周期:

加載 loading

驗證 verification

準備 preparation

解析 resolution

初始化 initialization

使用 using

卸載 unloading

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

有且只有以下幾種情況必須立即對類進行”初始化”(稱為對一個類進行主動引用):

(1)遇到new、getstatic、putstatic、invokestatic這四條字節碼指令時(使用new實例化對象的時候、讀取或設置一個類的靜態字段、調用一個類的靜態方法)。

(2)使用java.lang.reflet包的方法對類進行反射調用的時候。

(3)當初始化一個類的時候,如果發現其父類沒有進行過初始化,則需要先觸發其父類的初始化。

(4)當虛擬機啟動時,虛擬機會初始化主類(包含main方法的那個類)。

(5)調用一個類或接口的靜態字段,或者對這些靜態字段進行賦值操作的時候(即節碼中,調用getstatic()或putstatic()方法指令),不過用final關鍵字修飾的靜態字段除外,它被初始化為一個編譯時的常量表達式。

被動引用:

(1)通過子類引用父類的靜態字段,不會導致子類初始化(對于靜態字段,只有直接定義這個字段的類才會被初始化)。

(2)通過數組定義類應用類:ClassA [] array=new ClassA[10]。觸發了一個名為[LClassA的類的初始化,它是一個由虛擬機自動生成的、直接繼承于Object的類,創建動作由字節碼指令newarray觸發。

(3)常量會在編譯階段存入調用類的常量池。

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

一、加載Loading

加載任務是由類加載器所負責的,當然類加載器所執行的加載操作僅僅屬于JVM中加載過程的一個階段,同樣也是類加載過程的第一個階段,而后續還需要連接和初始化階段的配合才能構成一個完整的類加載過程。加載:就是根據一個類的全限定名來讀取此類的二進制字節流到JVM內部,然后轉化為一個與目標類對應的java.lang.Class對象實例。又可細分以下3步:

(1)通過一個類的全限定名來獲取此類的二進制字節流。可以是class文件,可以是jar。

(2)將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構。

(3)在java堆中生成一個代表這個類的Class對象,作為方法區這些數據的訪問入口。

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

二、連接Linking

連接Linking階段要做的事情就是將已經加載到JVM中的二進制字節流的類數據信息合并到JVM的運行時狀態中,然后該連接階段則由驗證、準備、解析3個階段構成。

1.驗證

這個階段驗證讀取到的二進制字節流是否符合虛擬機規范中Class文件的存儲格式,如果不符合,拋出java.lang.VerifyError異常或其子類的異常。

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

驗證階段分為四步:

(1)文件格式驗證:驗證字節流是否符合Class文件格式的規范,并且能被當前版本的虛擬機處理。這個階段的驗證時給予字節流進行的,經過了這個階段的驗證之后,字節流才會進入內存的方法區中進行存儲所以后面的驗證階段都是給予方法區的存儲結構進行的。關于字節碼文件有效性驗證

有以下這些驗證點:

*是否已魔數0xCAFEBABE開頭。

*主次版本號是否能被當前虛擬機處理。

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

*常量池里的項是否執行不存在的常量或不符合類型的常量。

(2)元數據驗證:對類的元數據信息進行語義校驗,保證不存在不符合java語言規范的元數據信息。

有以下這些驗證點:

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

*是否繼承了final類。

*如果不是抽象類,是否實現了所有需要實現的方法。

(3)字節碼驗證:進行數據流和控制流分析,對類的方法體進行校驗分析,保證被校驗的類的方法在運行時不會做出危害虛擬機安全的行為。

(4)符號引用驗證:發生在虛擬機將符號引用轉化為直接引用的時候(解析階段),對常量池中的各種符號引用的信息進行匹配性的校驗。

2.準備

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

數據類型

默認初始值

int

0

long

0L

short

(short)0

char

'u0000'

byte

(byte)0

boolean

false

float

0.0f

double

0.0d

reference

null

在此需要注意,JVM實現其實并不支持boolean類型,因此在JVM內部,boolean類型往往被實現為一個int類型,初始值為0也就代表著false。當然boolean類型的變量,盡管在JVM中當作int來實現,但在初始化時也總會被初始化為一個false值。

3.解析

解析階段是在虛擬機將常量池內的符號引用替換為直接引用的過程。

什么是符號引用呢?符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可。符號引用與虛擬機實現的內存布局無關,引用的目標并不一定已經加載到內存中。

直接引用:直接引用可以是直接指向目標的指針、相對偏移量或者一個能間接定位到目標的句柄。如果有了直接引用,那引用的目標必定已經在內存中存在。

要執行解析操作的內容包括:類或接口、類字段、類方法、接口中的方法。

(1)解析類或接口

類或接口(對應于常量池的CONSTANT_Class_info類型)的解析:

假設當前代碼所處的類為D,需要將一個從未解析過的符號引用N解析為一個類或接口C的直接引用:

i.如果C不是一個數組類型,虛擬機將會把代表C的全限定名傳遞給D的類加載器去加載這個類。

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

iii.如果以上過程沒有發生異常,則C在虛擬機中已經成為了一個有效的類和接口了,之后還要進行的是符號引用驗證,確認D是否具有對C的訪問權限,如果沒有,將拋出java.lang.IllegalAccessError異常。

(2)解析類字段

字段(對應于常量池的CONSTANT_Fieldref_info類型)解析:

i.對字段表中的class_index項中索引的CONSTANT_Class_info符號引用進行解析。用C表示這個字段所屬的類或接口。

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

iii.否則,如果C實現了接口,則會按照繼承關系從下往上遞歸搜索各個接口和他的父接口,如果接口中包含了簡單名稱和字段描述符都與目標相匹配的字段,則返回這個字段的直接引用。

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

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

虛擬機的編譯器實現可能會更嚴格:如果一個同名字段同時出現在C實現的接口和父類中,或者同時在自己或父類的多個接口中出現,編譯器將可能拒絕編譯。

(3)解析類方法

類方法(對應于常量池的CONSTANT_Methodref_info類型)解析:

i.對方法表中的class_index項中索引的CONSTANT_Class_info符號引用進行解析。用C表示這個方法所屬的類或接口。

ii.類方法和接口方法符號引用的常量類型定義是分開的,如果在類方法表中發現class_index中索引的C是個接口,則拋出java.lang.IncompatibleClassChangeError。

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

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

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

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

vii.如果查找返回了直接引用,將會對這個方法進行權限驗證,如果發現不具備對這個方法的訪問權限,則拋出java.lang.IllegalAccessError異常。

(4)解析接口方法

接口方法(對應于常量池的CONSTANT_InterfaceMethodref_info類型):

i.對方法表中的class_index項中索引的CONSTANT_Class_info符號引用進行解析。用C表示這個方法所屬的類或接口。

ii.如果在接口方法表中發現class_index中索引的C是個類,則拋出java.lang.IncompatibleClassChangeError。

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

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

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

三、初始化Initiazation

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

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

()方法執行過程--特點:

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

2.()方法與類的構造器()不同,它不需要顯示地調用父類類構造器,虛擬機會保證在子類的()方法執行之前,父類的()方法已經執行完畢。因此在虛擬機中第一個被執行()方法的類肯定是java.lang.Object。

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

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

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

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

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

8.任何invoke之類的字節碼指令也無法調用()方法,因為該方法只能在類加載的過程中被JVM所調用。