前面一篇講解了類加載的時機,現(xiàn)在來看看 類加載的過程 是怎樣的。
目錄
一、加載
二、驗證
三、準(zhǔn)備
四、解析
五、初始化
Java 虛擬機中類加載的全過程,也就是加載、驗證、準(zhǔn)備、解析和初始化這 5 個階段所執(zhí)行的具體動作。
一、加載
“加載” 是 “類加載” 過程的一個階段,在加載階段,虛擬機需要完成以下 3 件事情:
- 通過一個類的全限定名來獲取定義此類的二進制字節(jié)流。
- 將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu)。
- 在內(nèi)存中生成一個代表這個類的 java.lang.Class 對象,作為方法區(qū)這個類的各種數(shù)據(jù)的訪問入口。
相對于類加載過程的其他階段,一個非數(shù)組的加載階段(準(zhǔn)確的說,是加載階段中獲取類的二進制流的動作)是開發(fā)人員可控性最強的,因為加載階段既可以使用系統(tǒng)提供的引導(dǎo)類加載器來完成,也可以由用戶自定義的類加載器去完成,開發(fā)人員可以通過定義自己的類加載器去控制字節(jié)流的獲取方式(即重寫一個類加載器的 loadClass() 方法)。
對于數(shù)組而言,情況就有所不同,數(shù)組類本身不通過類加載器創(chuàng)建,它是由 Java 虛擬機直接創(chuàng)建的。但數(shù)組類與類加載器仍然有很密切的關(guān)系,因為數(shù)組類的元素類型最終是要靠類加載器去創(chuàng)建,一個數(shù)組創(chuàng)建過程就遵循以下規(guī)則:
- 如果數(shù)組的組件類型是引用類型,那就遞歸采用上面所說的加載過程去加載這個組件類型,數(shù)組類將在加載該組件類型的類加載器的類名稱空間上被標(biāo)識(這點很重要,一個類必須與類加載器一起確定唯一性)。
- 如果數(shù)組的組件類型不是引用類型(如 in[] 數(shù)組),Java 虛擬機將會把數(shù)組類標(biāo)記為與引導(dǎo)類加載器關(guān)聯(lián)。
- 數(shù)組類的可見性與它的組件類型的可見性一致,如果組件類型不是引用類型,那數(shù)組類的可見性將默認(rèn)為 public。
加載階段完成后看,虛擬機外部的二進制字節(jié)流就按照虛擬機所需的格式存儲在方法區(qū)之中,方法區(qū)中的數(shù)據(jù)存儲格式由虛擬機實現(xiàn)自行定義,虛擬機規(guī)范未規(guī)定此區(qū)域的具體數(shù)據(jù)結(jié)構(gòu)。然后在內(nèi)存中實例化一個 java.lang.Class 類的對象,這個對象將作為程序訪問方法區(qū)中的這些類型數(shù)據(jù)的外部接口。
加載階段與連接階段(即驗證、準(zhǔn)備和解析 3 個階段)的部分內(nèi)容(如一部分字節(jié)碼文件格式驗證動作)是交叉進行的,加載階段尚未完成,連接階段可能已經(jīng)開始,但這些夾在加載階段之中進行的動作,仍然屬于連接階段的內(nèi)容,這兩個階段的開始時間仍然保持固定的先后順序。
二、驗證
驗證是連接階段的第一步,這一階段的目的是為了確保 Class 文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機的要求,并且不會危害虛擬機自身的安全。
從整體上看,驗證階段大致上會完成下面 4 個階段的檢驗動作:文件格式驗證、元數(shù)據(jù)驗證、字節(jié)碼驗證、符號應(yīng)用驗證。
-
文件格式驗證
第一階段要驗證字節(jié)流是否符合 Class 文件格式的規(guī)范,并且能被當(dāng)前版本的虛擬機處理。這一階段可能包括下面這些驗證點:
- 是否以魔數(shù) 0xCAFEBABE 開頭。
- 主、次版本號是否在當(dāng)前虛擬機處理范圍之內(nèi)。
- 常量池中的常量是否有不被支持的常量類型。
- 指向常量的各種索引值中是否有指向不存在的常量或不符合類型的常量。
- CONSTNAT_Utf8_info 型的常量中是否有不符合 UTF8 編碼的數(shù)據(jù)。
- .........
-
元數(shù)據(jù)的驗證
第二階段是對字節(jié)碼描述的信息進行語義分析,以保證其描述的信息符合 Java 語言規(guī)范的要求,這個階段可能包括的驗證點如下:
- 這個類是否有父類(除了 java.lang.Object 之外,所有的類都應(yīng)當(dāng)也有父類)。
- 這個類的父類是否繼承了不允許被繼承的類(被 final 修飾的類)
- 如果這個類不是抽象類,是否顯示了其父類或接口之中要求實現(xiàn)的所有方法。
- 類中的字段、方法是否與父類產(chǎn)生矛盾。
- ........
-
字節(jié)碼驗證
字節(jié)碼驗證是整個驗證過程中最復(fù)雜的一個階段,主要母的是通過數(shù)據(jù)流和控制流分析,確定程序語言是合法的、符合邏輯的。在第三階段對元數(shù)據(jù)信息中的數(shù)據(jù)類型做完校驗后,這個階段將對類的方法體進行校驗分析,保證被校驗類的方法在運行時不會做出危害虛擬機安全的時間,例如:
- 保證任意時刻操作數(shù)棧的數(shù)據(jù)類型與指令代碼序列都能配合工作,例如不會出現(xiàn)類似這樣的情況:在操作棧放置了一個 int 類型的數(shù)據(jù),使用時卻按 long 類型來加載人本地變量表中。
- 保證跳轉(zhuǎn)指令不會跳轉(zhuǎn)到方法體以外的字節(jié)碼指令撒還給你。
- .......
-
符號引用驗證
最后一個階段的校驗發(fā)生在虛擬機將符號引用轉(zhuǎn)化為直接引用的時候,這個轉(zhuǎn)化動作將在連接的第三階段——解析階段中發(fā)生。符號引用驗證可以看作是對類自身以外的信息進行匹配性校驗,通常需要校驗下列內(nèi)容:
- 符號引用中通過字符串描述的全限定名是否能找到對應(yīng)的類。
- 在指定類中是否存在符合方法的字段描述符以及簡單名稱所描述的方法和字段。
- 符號引用中的類、字段、方法的訪問行(private、protected、public、default)是否可被當(dāng)前類訪問。
- ........
三、準(zhǔn)備
準(zhǔn)備階段是正式為 類變量 分配內(nèi)存并設(shè)置類變量初始化的階段,這些變量所使用的內(nèi)存都將在方法區(qū)中進行分配。這個階段中有兩個容易產(chǎn)生混淆的概念需要強調(diào)一下,首先,這時候進行內(nèi)存分配的僅包括類變量(被 static 修飾的變量),而不包括實例變量,實例變量將會在對象實例化時隨著對象一起分配在 Java 堆中。其次,這里所說的初始化值“通常情況”下是數(shù)據(jù)類型的零值,假設(shè)一個類變量的定義為:
public static int value = 123;
那變量 value 在準(zhǔn)備階段過后的初始值為 0 而不是 123,因為這個時候尚未開始執(zhí)行任何 java 方法,而把 value 賦值為 123 的 putstatic 指令是程序被編譯后,存放于類構(gòu)造器 <clinit>() 方法之中,所以把 value 賦值為 123 的動作將在初始化階段才會執(zhí)行。下表列出了所有基本數(shù)據(jù)類型的零值。

上面提到,在“通常情況”下初始值是零值,那相對的會有一些“特殊情況”:如果類字段的字段你屬性表中存在 ConstantValue 屬性,那在準(zhǔn)備階段變量 value 就會被初始化為 ConstantValue 屬性所指定的值,假設(shè)上面的類變量 value 的定義變?yōu)椋?br>
public static final int value = 123;
編譯時 javac 將會為 value 生成 ConstantValue 屬性,在準(zhǔn)備階段虛擬機就會根據(jù) ConstantValue 的設(shè)置將 value 賦值為 123.
四、解析
解析階段是虛擬機將 常量池中的符號引用 轉(zhuǎn)化為 直接引用 的過程,在 Class 文件中它以 CONSTANT_Class_info、CONSTANT_FIeldref_info、CONSTANT_Methodref_info 等類型的常量出現(xiàn)。
那解析階段中所說的符號引用和常量引用又有什么關(guān)系呢?
- 符號引用:符號引用以一組符號來描述所引用的目標(biāo),符號可以是任何形式的字面量,只要使用時無歧義的定位到目標(biāo)即可。符號引用于虛擬機實現(xiàn)的內(nèi)存布局無關(guān),引用的目標(biāo)并不一定已經(jīng)加載到內(nèi)存中。各種虛擬機實現(xiàn)的內(nèi)存布局可以各不相同,但是它們能接受的符號引用必須是一致的,因為符號引用的字面量形式明確定義在 Java 虛擬機規(guī)范的 Class 文件格式中。
- 直接引用:直接引用可以直接指向目標(biāo)的地址、相對偏移量或是一個能間接定位到目標(biāo)的句柄。如果有了直接引用,那引用的目標(biāo)必定已經(jīng)在內(nèi)存中存在。
虛擬機規(guī)范之中并未規(guī)定解析階段發(fā)生的具體時間,只要求了 anewarray、checkcast、getfield、getstatic、instanceof、invokedynamic、invokeinterface、invokespecial、invokestatic、invokevitual、ldc、ldc_w、multianewarray、new、putfield 和 putstatic 這 16 個用于操作符號引用的字節(jié)碼指令之前。
解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調(diào)用點限定符 7 類符號引用進行,分別對應(yīng)于常量池的 CONSTANT_CLass_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info、CONSTANT_MethodType_info、CONSTANT_MethodHandle_info 和 CONSTANT_InvokeDynamic_info 7 種常量類型。
五、初始化
類初始化階段是類加載過程的最后一步,前面的類加載過程中,除了在加載階段用戶應(yīng)用程序可以通過自定義類加載器參與之外,其余動作完全由虛擬機主導(dǎo)和控制。到了初始化階段,才真正開始執(zhí)行類中定義的 Java 程序代碼(或者說是字節(jié)碼)。
在準(zhǔn)備階段,變量已經(jīng)賦過一次系統(tǒng)要求的初始值(零值),而在初始化階段,則根據(jù)程序猿通過程序制定的主觀計劃去初始化類變量和其他資源,或者可以從另外一個角度來表達:初始化階段是執(zhí)行類構(gòu)造器 <clinit>() 方法的過程。先看一下 <clinit>() 方法執(zhí)行過程中一些可能會影響程序運行行為的特點和細(xì)節(jié)。
- <clinit>() 方法是有編譯期自動收集類中所有 類變量的賦值動作 和 靜態(tài)語句塊(static {} 塊) 中語句合并產(chǎn)生的,編譯期收集的順序是由語句在源文件中出現(xiàn)的順序所決定的,靜態(tài)語句塊中只能訪問到定義在靜態(tài)語句塊之前的變量,定義在它之后的變量,在前面的靜態(tài)語句塊可以復(fù)制,但是不能訪問,如下面代碼:
- <clinit>() 方法(類構(gòu)造 器)與 類的構(gòu)造 函數(shù)(或者說實例構(gòu)造器 <init>() 方法)不同,它不需要顯式的調(diào)用父類構(gòu)造器,虛擬機會保證在子類的 <clinit>() 方法執(zhí)行之前,父類的 <clinit>() 方法已經(jīng)執(zhí)行完畢,因此在虛擬機中第一個被執(zhí)行的 <clinit>() 方法的類肯定是 java.lang.Object。
-
由于父類的 <clinit>() 方法先執(zhí)行,也就意味著父類中定義的靜態(tài)語句塊要優(yōu)先于子類的變量賦值操作,如下面的代碼,字段 B 的值會是 2 而不是 1。
- <clinit>() 方法對于類或接口來說并不是必需的,如果一個類中沒有靜態(tài)語句塊,也沒有對類變量的賦值操作,那么編譯器可以不為這個類生成 <clinit>() 方法。
- 接口中不能使用靜態(tài)語句塊,但仍然有類變量的初始化的賦值操作,因此接口與類一樣都會生成 <clinit>() 方法。但接口與類不同的時,執(zhí)行接口的 <clinit>() 方法不需要先執(zhí)行父接口的 <clinit>() 方法。只有當(dāng)父接口中定義的變量使用時,父接口才會初始化。另外,接口的實現(xiàn)類在初始化時也一樣不會執(zhí)行接口的 <clinit>() 方法。
- 虛擬機會保證一個類的 <clinit>() 方法在多線程環(huán)境中被正確的加鎖、同步,如果多線程同時去初始化一個類,那么只會有一個線程去執(zhí)行這個類的 <clinit>() 方法,其他線程都需要阻塞等待,知道活動線程執(zhí)行 <clinit>() 方法完畢。如果在一個類的 <clinit>() 方法中有耗時很長的操作,就可能造成多個進程阻塞。
下一篇文章:關(guān)于類加載器的知識

