前面一篇講解了類加載的時(shí)機(jī),現(xiàn)在來(lái)看看 類加載的過(guò)程 是怎樣的帅涂。
目錄
一议薪、加載
二、驗(yàn)證
三媳友、準(zhǔn)備
四斯议、解析
五、初始化
Java 虛擬機(jī)中類加載的全過(guò)程醇锚,也就是加載哼御、驗(yàn)證、準(zhǔn)備搂抒、解析和初始化這 5 個(gè)階段所執(zhí)行的具體動(dòng)作艇搀。
一、加載
“加載” 是 “類加載” 過(guò)程的一個(gè)階段求晶,在加載階段焰雕,虛擬機(jī)需要完成以下 3 件事情:
- 通過(guò)一個(gè)類的全限定名來(lái)獲取定義此類的二進(jìn)制字節(jié)流。
- 將這個(gè)字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)芳杏。
- 在內(nèi)存中生成一個(gè)代表這個(gè)類的 java.lang.Class 對(duì)象矩屁,作為方法區(qū)這個(gè)類的各種數(shù)據(jù)的訪問(wèn)入口辟宗。
相對(duì)于類加載過(guò)程的其他階段,一個(gè)非數(shù)組的加載階段(準(zhǔn)確的說(shuō)吝秕,是加載階段中獲取類的二進(jìn)制流的動(dòng)作)是開(kāi)發(fā)人員可控性最強(qiáng)的泊脐,因?yàn)榧虞d階段既可以使用系統(tǒng)提供的引導(dǎo)類加載器來(lái)完成,也可以由用戶自定義的類加載器去完成烁峭,開(kāi)發(fā)人員可以通過(guò)定義自己的類加載器去控制字節(jié)流的獲取方式(即重寫(xiě)一個(gè)類加載器的 loadClass() 方法)容客。
對(duì)于數(shù)組而言,情況就有所不同约郁,數(shù)組類本身不通過(guò)類加載器創(chuàng)建缩挑,它是由 Java 虛擬機(jī)直接創(chuàng)建的。但數(shù)組類與類加載器仍然有很密切的關(guān)系鬓梅,因?yàn)閿?shù)組類的元素類型最終是要靠類加載器去創(chuàng)建供置,一個(gè)數(shù)組創(chuàng)建過(guò)程就遵循以下規(guī)則:
- 如果數(shù)組的組件類型是引用類型,那就遞歸采用上面所說(shuō)的加載過(guò)程去加載這個(gè)組件類型绽快,數(shù)組類將在加載該組件類型的類加載器的類名稱空間上被標(biāo)識(shí)(這點(diǎn)很重要芥丧,一個(gè)類必須與類加載器一起確定唯一性)。
- 如果數(shù)組的組件類型不是引用類型(如 in[] 數(shù)組)坊罢,Java 虛擬機(jī)將會(huì)把數(shù)組類標(biāo)記為與引導(dǎo)類加載器關(guān)聯(lián)续担。
- 數(shù)組類的可見(jiàn)性與它的組件類型的可見(jiàn)性一致,如果組件類型不是引用類型活孩,那數(shù)組類的可見(jiàn)性將默認(rèn)為 public赤拒。
加載階段完成后看,虛擬機(jī)外部的二進(jìn)制字節(jié)流就按照虛擬機(jī)所需的格式存儲(chǔ)在方法區(qū)之中诱鞠,方法區(qū)中的數(shù)據(jù)存儲(chǔ)格式由虛擬機(jī)實(shí)現(xiàn)自行定義,虛擬機(jī)規(guī)范未規(guī)定此區(qū)域的具體數(shù)據(jù)結(jié)構(gòu)这敬。然后在內(nèi)存中實(shí)例化一個(gè) java.lang.Class 類的對(duì)象航夺,這個(gè)對(duì)象將作為程序訪問(wèn)方法區(qū)中的這些類型數(shù)據(jù)的外部接口。
加載階段與連接階段(即驗(yàn)證崔涂、準(zhǔn)備和解析 3 個(gè)階段)的部分內(nèi)容(如一部分字節(jié)碼文件格式驗(yàn)證動(dòng)作)是交叉進(jìn)行的阳掐,加載階段尚未完成津肛,連接階段可能已經(jīng)開(kāi)始药版,但這些夾在加載階段之中進(jìn)行的動(dòng)作歼跟,仍然屬于連接階段的內(nèi)容邓嘹,這兩個(gè)階段的開(kāi)始時(shí)間仍然保持固定的先后順序每窖。
二宵呛、驗(yàn)證
驗(yàn)證是連接階段的第一步藕坯,這一階段的目的是為了確保 Class 文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求伍纫,并且不會(huì)危害虛擬機(jī)自身的安全隆夯。
從整體上看钳恕,驗(yàn)證階段大致上會(huì)完成下面 4 個(gè)階段的檢驗(yàn)動(dòng)作:文件格式驗(yàn)證别伏、元數(shù)據(jù)驗(yàn)證、字節(jié)碼驗(yàn)證忧额、符號(hào)應(yīng)用驗(yàn)證厘肮。
-
文件格式驗(yàn)證
第一階段要驗(yàn)證字節(jié)流是否符合 Class 文件格式的規(guī)范,并且能被當(dāng)前版本的虛擬機(jī)處理睦番。這一階段可能包括下面這些驗(yàn)證點(diǎn):
- 是否以魔數(shù) 0xCAFEBABE 開(kāi)頭类茂。
- 主、次版本號(hào)是否在當(dāng)前虛擬機(jī)處理范圍之內(nèi)托嚣。
- 常量池中的常量是否有不被支持的常量類型巩检。
- 指向常量的各種索引值中是否有指向不存在的常量或不符合類型的常量。
- CONSTNAT_Utf8_info 型的常量中是否有不符合 UTF8 編碼的數(shù)據(jù)注益。
- .........
-
元數(shù)據(jù)的驗(yàn)證
第二階段是對(duì)字節(jié)碼描述的信息進(jìn)行語(yǔ)義分析碴巾,以保證其描述的信息符合 Java 語(yǔ)言規(guī)范的要求,這個(gè)階段可能包括的驗(yàn)證點(diǎn)如下:
- 這個(gè)類是否有父類(除了 java.lang.Object 之外丑搔,所有的類都應(yīng)當(dāng)也有父類)厦瓢。
- 這個(gè)類的父類是否繼承了不允許被繼承的類(被 final 修飾的類)
- 如果這個(gè)類不是抽象類,是否顯示了其父類或接口之中要求實(shí)現(xiàn)的所有方法啤月。
- 類中的字段煮仇、方法是否與父類產(chǎn)生矛盾。
- ........
-
字節(jié)碼驗(yàn)證
字節(jié)碼驗(yàn)證是整個(gè)驗(yàn)證過(guò)程中最復(fù)雜的一個(gè)階段谎仲,主要母的是通過(guò)數(shù)據(jù)流和控制流分析浙垫,確定程序語(yǔ)言是合法的、符合邏輯的郑诺。在第三階段對(duì)元數(shù)據(jù)信息中的數(shù)據(jù)類型做完校驗(yàn)后夹姥,這個(gè)階段將對(duì)類的方法體進(jìn)行校驗(yàn)分析,保證被校驗(yàn)類的方法在運(yùn)行時(shí)不會(huì)做出危害虛擬機(jī)安全的時(shí)間辙诞,例如:
- 保證任意時(shí)刻操作數(shù)棧的數(shù)據(jù)類型與指令代碼序列都能配合工作辙售,例如不會(huì)出現(xiàn)類似這樣的情況:在操作棧放置了一個(gè) int 類型的數(shù)據(jù),使用時(shí)卻按 long 類型來(lái)加載人本地變量表中飞涂。
- 保證跳轉(zhuǎn)指令不會(huì)跳轉(zhuǎn)到方法體以外的字節(jié)碼指令撒還給你旦部。
- .......
-
符號(hào)引用驗(yàn)證
最后一個(gè)階段的校驗(yàn)發(fā)生在虛擬機(jī)將符號(hào)引用轉(zhuǎn)化為直接引用的時(shí)候,這個(gè)轉(zhuǎn)化動(dòng)作將在連接的第三階段——解析階段中發(fā)生较店。符號(hào)引用驗(yàn)證可以看作是對(duì)類自身以外的信息進(jìn)行匹配性校驗(yàn)士八,通常需要校驗(yàn)下列內(nèi)容:
- 符號(hào)引用中通過(guò)字符串描述的全限定名是否能找到對(duì)應(yīng)的類。
- 在指定類中是否存在符合方法的字段描述符以及簡(jiǎn)單名稱所描述的方法和字段梁呈。
- 符號(hào)引用中的類婚度、字段、方法的訪問(wèn)行(private捧杉、protected陕见、public秘血、default)是否可被當(dāng)前類訪問(wèn)。
- ........
三评甜、準(zhǔn)備
準(zhǔn)備階段是正式為 類變量 分配內(nèi)存并設(shè)置類變量初始化的階段灰粮,這些變量所使用的內(nèi)存都將在方法區(qū)中進(jìn)行分配。這個(gè)階段中有兩個(gè)容易產(chǎn)生混淆的概念需要強(qiáng)調(diào)一下忍坷,首先粘舟,這時(shí)候進(jìn)行內(nèi)存分配的僅包括類變量(被 static 修飾的變量),而不包括實(shí)例變量佩研,實(shí)例變量將會(huì)在對(duì)象實(shí)例化時(shí)隨著對(duì)象一起分配在 Java 堆中柑肴。其次,這里所說(shuō)的初始化值“通常情況”下是數(shù)據(jù)類型的零值旬薯,假設(shè)一個(gè)類變量的定義為:
public static int value = 123;
那變量 value 在準(zhǔn)備階段過(guò)后的初始值為 0 而不是 123晰骑,因?yàn)檫@個(gè)時(shí)候尚未開(kāi)始執(zhí)行任何 java 方法,而把 value 賦值為 123 的 putstatic 指令是程序被編譯后绊序,存放于類構(gòu)造器 <clinit>() 方法之中硕舆,所以把 value 賦值為 123 的動(dòng)作將在初始化階段才會(huì)執(zhí)行。下表列出了所有基本數(shù)據(jù)類型的零值骤公。
上面提到抚官,在“通常情況”下初始值是零值,那相對(duì)的會(huì)有一些“特殊情況”:如果類字段的字段你屬性表中存在 ConstantValue 屬性阶捆,那在準(zhǔn)備階段變量 value 就會(huì)被初始化為 ConstantValue 屬性所指定的值凌节,假設(shè)上面的類變量 value 的定義變?yōu)椋?br>
public static final int value = 123;
編譯時(shí) javac 將會(huì)為 value 生成 ConstantValue 屬性,在準(zhǔn)備階段虛擬機(jī)就會(huì)根據(jù) ConstantValue 的設(shè)置將 value 賦值為 123.
四洒试、解析
解析階段是虛擬機(jī)將 常量池中的符號(hào)引用 轉(zhuǎn)化為 直接引用 的過(guò)程倍奢,在 Class 文件中它以 CONSTANT_Class_info、CONSTANT_FIeldref_info垒棋、CONSTANT_Methodref_info 等類型的常量出現(xiàn)娱挨。
那解析階段中所說(shuō)的符號(hào)引用和常量引用又有什么關(guān)系呢?
- 符號(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)存中。各種虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局可以各不相同淮韭,但是它們能接受的符號(hào)引用必須是一致的垢粮,因?yàn)榉?hào)引用的字面量形式明確定義在 Java 虛擬機(jī)規(guī)范的 Class 文件格式中。
- 直接引用:直接引用可以直接指向目標(biāo)的地址靠粪、相對(duì)偏移量或是一個(gè)能間接定位到目標(biāo)的句柄蜡吧。如果有了直接引用毫蚓,那引用的目標(biāo)必定已經(jīng)在內(nèi)存中存在。
虛擬機(jī)規(guī)范之中并未規(guī)定解析階段發(fā)生的具體時(shí)間昔善,只要求了 anewarray元潘、checkcast、getfield君仆、getstatic翩概、instanceof、invokedynamic返咱、invokeinterface钥庇、invokespecial、invokestatic咖摹、invokevitual评姨、ldc、ldc_w萤晴、multianewarray吐句、new、putfield 和 putstatic 這 16 個(gè)用于操作符號(hào)引用的字節(jié)碼指令之前硫眯。
解析動(dòng)作主要針對(duì)類或接口蕴侧、字段、類方法两入、接口方法净宵、方法類型、方法句柄和調(diào)用點(diǎn)限定符 7 類符號(hào)引用進(jìn)行裹纳,分別對(duì)應(yīng)于常量池的 CONSTANT_CLass_info择葡、CONSTANT_Fieldref_info、CONSTANT_Methodref_info剃氧、CONSTANT_InterfaceMethodref_info敏储、CONSTANT_MethodType_info、CONSTANT_MethodHandle_info 和 CONSTANT_InvokeDynamic_info 7 種常量類型朋鞍。
五已添、初始化
類初始化階段是類加載過(guò)程的最后一步,前面的類加載過(guò)程中滥酥,除了在加載階段用戶應(yīng)用程序可以通過(guò)自定義類加載器參與之外更舞,其余動(dòng)作完全由虛擬機(jī)主導(dǎo)和控制。到了初始化階段坎吻,才真正開(kāi)始執(zhí)行類中定義的 Java 程序代碼(或者說(shuō)是字節(jié)碼)缆蝉。
在準(zhǔn)備階段,變量已經(jīng)賦過(guò)一次系統(tǒng)要求的初始值(零值),而在初始化階段刊头,則根據(jù)程序猿通過(guò)程序制定的主觀計(jì)劃去初始化類變量和其他資源黍瞧,或者可以從另外一個(gè)角度來(lái)表達(dá):初始化階段是執(zhí)行類構(gòu)造器 <clinit>() 方法的過(guò)程。先看一下 <clinit>() 方法執(zhí)行過(guò)程中一些可能會(huì)影響程序運(yùn)行行為的特點(diǎn)和細(xì)節(jié)原杂。
- <clinit>() 方法是有編譯期自動(dòng)收集類中所有 類變量的賦值動(dòng)作 和 靜態(tài)語(yǔ)句塊(static {} 塊) 中語(yǔ)句合并產(chǎn)生的印颤,編譯期收集的順序是由語(yǔ)句在源文件中出現(xiàn)的順序所決定的,靜態(tài)語(yǔ)句塊中只能訪問(wèn)到定義在靜態(tài)語(yǔ)句塊之前的變量污尉,定義在它之后的變量膀哲,在前面的靜態(tài)語(yǔ)句塊可以復(fù)制,但是不能訪問(wèn)被碗,如下面代碼:
- <clinit>() 方法(類構(gòu)造 器)與 類的構(gòu)造 函數(shù)(或者說(shuō)實(shí)例構(gòu)造器 <init>() 方法)不同某宪,它不需要顯式的調(diào)用父類構(gòu)造器,虛擬機(jī)會(huì)保證在子類的 <clinit>() 方法執(zhí)行之前锐朴,父類的 <clinit>() 方法已經(jīng)執(zhí)行完畢兴喂,因此在虛擬機(jī)中第一個(gè)被執(zhí)行的 <clinit>() 方法的類肯定是 java.lang.Object。
-
由于父類的 <clinit>() 方法先執(zhí)行焚志,也就意味著父類中定義的靜態(tài)語(yǔ)句塊要優(yōu)先于子類的變量賦值操作衣迷,如下面的代碼,字段 B 的值會(huì)是 2 而不是 1酱酬。
- <clinit>() 方法對(duì)于類或接口來(lái)說(shuō)并不是必需的壶谒,如果一個(gè)類中沒(méi)有靜態(tài)語(yǔ)句塊,也沒(méi)有對(duì)類變量的賦值操作膳沽,那么編譯器可以不為這個(gè)類生成 <clinit>() 方法汗菜。
- 接口中不能使用靜態(tài)語(yǔ)句塊,但仍然有類變量的初始化的賦值操作挑社,因此接口與類一樣都會(huì)生成 <clinit>() 方法陨界。但接口與類不同的時(shí),執(zhí)行接口的 <clinit>() 方法不需要先執(zhí)行父接口的 <clinit>() 方法痛阻。只有當(dāng)父接口中定義的變量使用時(shí)菌瘪,父接口才會(huì)初始化。另外阱当,接口的實(shí)現(xiàn)類在初始化時(shí)也一樣不會(huì)執(zhí)行接口的 <clinit>() 方法俏扩。
- 虛擬機(jī)會(huì)保證一個(gè)類的 <clinit>() 方法在多線程環(huán)境中被正確的加鎖、同步弊添,如果多線程同時(shí)去初始化一個(gè)類动猬,那么只會(huì)有一個(gè)線程去執(zhí)行這個(gè)類的 <clinit>() 方法,其他線程都需要阻塞等待表箭,知道活動(dòng)線程執(zhí)行 <clinit>() 方法完畢。如果在一個(gè)類的 <clinit>() 方法中有耗時(shí)很長(zhǎng)的操作,就可能造成多個(gè)進(jìn)程阻塞免钻。
下一篇文章:關(guān)于類加載器的知識(shí)