二、類加載的過程

1.加載

1.1.在加載階段,Java虛擬機(jī)需要完成以下三件事情:

? 1.通過一個類的全限定名獲取定義此類的二進(jìn)制字節(jié)流。
? 2.將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu)。
? 3.在內(nèi)存中生成一個代表這個類的java.lang.Class對象,作為方法區(qū)這個類的各種數(shù)據(jù)的訪問入口

1.2.獲取二進(jìn)制字節(jié)流的方法(Java虛擬機(jī)規(guī)范并沒有定義)

? 1.從ZIP壓縮包中讀取,這很常見,最終成為日后JAR、EAR、WAR格式的基礎(chǔ)。

? 2.從網(wǎng)絡(luò)中獲取,這種場景最典型的應(yīng)用就是Web Applet。

? 3.運行時計算生成,這種場景使用得最多的就是動態(tài)代理技術(shù)。

? 4.由其他文件生成,典型場景是JSP應(yīng)用,由JSP文件生成對應(yīng)的Class文件。

? 5.從數(shù)據(jù)庫中讀取,這種場景相對少見些,例如有些中間件服務(wù)器(如SAP Netweaver)可以選擇把程序安裝到數(shù)據(jù)庫中來完成程序代碼在集群間的分發(fā)。

? 6.可以從加密文件中獲取,這是典型的防Class文件被反編譯的保護(hù)措施,通過加載時解密Class文 件來保障程序運行邏輯不被窺探。

? 對于非數(shù)組類型:加載階段是開發(fā)人員可控性最強(qiáng)的階段,加載階段可以由用戶自定義的類加載器去完成,開發(fā)人員通過定義自己的類加載器去控制字節(jié)流的獲取方式(重寫一個類加載器的findClass()或loadClass()方法)。

? 對于數(shù)組類:數(shù)組類不通過類加載器加載,由Java虛擬機(jī)直接在內(nèi)存中動態(tài)構(gòu)造出來,但是數(shù)組類的元素類型是通過類加載器加載。如果數(shù)組的組件類型是引用類型,就遞歸采用本節(jié)定義的加載過程去加載,如果數(shù)組組件類型不是引用類型(如:int[]數(shù)組的組件類型為int),Java虛擬機(jī)將會把數(shù)組C標(biāo)記為與引導(dǎo)類加載器關(guān)聯(lián)。

? 數(shù)組類的可訪問性與它的組件類型的可訪問性一致,如果組件類型不是引用類型,它的數(shù)組類的 可訪問性將默認(rèn)為public,可被所有的類和接口訪問到。

? 注意:類的加載過程與連接階段的部分動作是交叉進(jìn)行的。

2.驗證

? 驗證是連接階段的第一步,這一步的目的是確保Class文件的字節(jié)流中包含的信息符合Java虛擬機(jī)的全部約束要求,保證這些信息被當(dāng)作代碼運行后不會危害虛擬機(jī)的自身安全。

? 注意:Class文件并不一定只能由Java源碼編譯而來,它可以使用包括靠鍵盤0和1直接在二進(jìn)制編輯器中敲出 Class文件在內(nèi)的任何途徑產(chǎn)生。如果Java虛擬機(jī)如果不檢查輸入的字節(jié)流,對其完全信任的話,很可能會因為載入了有錯誤或有惡意企圖的字節(jié)碼流而導(dǎo)致整個系統(tǒng)受攻擊甚至崩潰,所以驗證字節(jié)碼是Java虛擬機(jī)保護(hù)自身的一項必要措施。

2.1.文件格式的驗證

? 1.是否以魔數(shù):0XCAFEBABE開頭

? 2.主、次版本號是否在當(dāng)前Java虛擬機(jī)接受范圍之內(nèi)。

? 3.常量池的常量中是否有不被支持的常量類型(檢查常量tag標(biāo)志)。

? 4.指向常量的各種索引值中是否有指向不存在的常量或不符合類型的常量。

? 5.CONSTANT_Utf8_info型的常量中是否有不符合UTF-8編碼的數(shù)據(jù)。

? 6.Class文件中各個部分及文件本身是否有被刪除的或附加的其他信息。

2.2.元數(shù)據(jù)的驗證

? 1.是否有父類(除了java.lang.Object之外,所有的類都應(yīng)當(dāng)有父類)。

? 2.類的父類是否繼承了不允許被繼承的類(被final修飾的類)。

? 3.如果這個類不是抽象類,是否實現(xiàn)了其父類或接口之中要求實現(xiàn)的所有方法。

? 4.類中的字段、方法是否與父類產(chǎn)生矛盾(例如覆蓋了父類的final字段,或者出現(xiàn)不符合規(guī)則的方 法重載,例如方法參數(shù)都一致,但返回值類型卻不同等)。
? 第二階段的主要目的是對類的元數(shù)據(jù)信息進(jìn)行語義校驗,保證不存在與《Java語言規(guī)范》定義相悖的元數(shù)據(jù)信息。

2.3.字節(jié)碼的驗證

? 注意:整個驗證過程中最復(fù)雜的一個階段,主要目的是通過數(shù)據(jù)流分析和控制流分析,確定程序語義是合法的、符合邏輯的。

? 字節(jié)碼驗證主要是對類的方法體進(jìn)行校驗分析

? 1.保證任意時刻操作數(shù)棧的數(shù)據(jù)類型與指令代碼序列都能配合工作。

? 2.保證任何調(diào)轉(zhuǎn)指令都不會跳轉(zhuǎn)到方法體外的字節(jié)碼指令上

? 3.保證方法體中的類型轉(zhuǎn)換總是有效的

? 注:如果一個類型有方法體的字節(jié)碼卻沒有通過字節(jié)碼驗證,那這個類型肯定是有問題的,但是如果一個方法體如果通過了字節(jié)碼驗證,也不一定能保證就是安全的,涉及了”停機(jī)問題“:即不能通過程序準(zhǔn)確地檢查出程序是否能在有限的時間之內(nèi)結(jié)束運

2.4.符號引用驗證(確保解析行為能正常執(zhí)行)

? 最后一個校驗行為

? 發(fā)生在虛擬機(jī)將符號引用轉(zhuǎn)換為直接引用的適合,這個轉(zhuǎn)化動作將在連接的第三階段——解析階段中發(fā)生。

? 符號引用驗證可以看作是對類自身以外(常量池中的各種符號引用)的各類信息進(jìn)行匹配性校驗,通俗來說就是,該類是否缺少或者被禁止訪問它依賴的某些外部類、方法、字段等資源。

本階段通常需要校驗下列內(nèi)容:

? 1.符號引用中通過字符串描述的全限定名是否能找到對應(yīng)的類。
? 2.在指定類中是否存在符合方法的字段描述符及簡單名稱所描述的方法和字段。
? 3.符號引用中的類、字段、方法的可訪問性(private、protected、public、<package>)是否可被當(dāng)前類訪問。

? 如果無法通過符號引用驗證,Java虛擬機(jī)將會拋出一個java.lang.IncompatibleClassChangeError的子類異常,典型的如: java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError等。

3.準(zhǔn)備階段

? 準(zhǔn)備階段是正式為類中定義的變量(即靜態(tài)變量,被static修飾的變量)分配內(nèi)存并設(shè)置類變量初始值的階段。

? 如果類字段 的字段屬性表中存在ConstantValue屬性,那在準(zhǔn)備階段變量值就會被初始化為ConstantV alue屬性所指定 的初始值,

4.解析

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

? 符號引用(Symbolic References):符號引用以一組符號來描述所引用的目標(biāo),符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標(biāo)即可。符號引用與虛擬機(jī)實現(xiàn)的內(nèi)存布局無關(guān),引用的目標(biāo)并不一定是已經(jīng)加載到虛擬機(jī)內(nèi)存當(dāng)中的內(nèi)容。各種虛擬機(jī)實現(xiàn)的內(nèi)存布局可以各不相同, 但是它們能接受的符號引用必須都是一致的,因為符號引用的字面量形式明確定義在《Java虛擬機(jī)規(guī) 范》的Class文件格式中。

? 直接引用(Direct References):直接引用是可以直接指向目標(biāo)的指針、相對偏移量或者是一個能間接定位到目標(biāo)的句柄。直接引用是和虛擬機(jī)實現(xiàn)的內(nèi)存布局直接相關(guān)的,同一個符號引用在不同虛 擬機(jī)實例上翻譯出來的直接引用一般不會相同。如果有了直接引用,那引用的目標(biāo)必定已經(jīng)在虛擬機(jī) 的內(nèi)存中存在。

? 解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調(diào)用點限定符這7類符號引用進(jìn)行。

4.1.類或接口的解析

? 假設(shè)當(dāng)前代碼所處的類為D,如果要把一個從未解析過的符號引用N解析為一個類或接口C的直接引用,那虛擬機(jī)完成整個解析的過程需要包括以下3個步驟:

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

? 2.如果C是一個數(shù)組類型,并且數(shù)組的元素類型為對象,也就是N的描述符會是類似“[Ljava/lang/Integer”的形式,那將會按照第一點的規(guī)則加載數(shù)組元素類型。

? 3.如果上面兩步?jīng)]有出現(xiàn)任何異常,那么C在虛擬機(jī)中實際上已經(jīng)成為一個有效的類或接口了, 但在解析完成前還要進(jìn)行符號引用驗證,確認(rèn)D是否具備對C的訪問權(quán)限。如果發(fā)現(xiàn)不具備訪問權(quán)限, 將拋出java.lang.IllegalAccessError異常。

? 針對上面第3點訪問權(quán)限驗證,在JDK 9引入了模塊化以后,一個public類型也不再意味著程序任 何位置都有它的訪問權(quán)限,我們還必須檢查模塊間的訪問權(quán)限。我們說一個D擁有C的訪問權(quán)限,那就意味著以下3條規(guī)則中至少有其中一條成立:

? 1·被訪問類C是public的,并且與訪問類D處于同一個模塊。

? 2·被訪問類C是public的,不與訪問類D處于同一個模塊,但是被訪問類C的模塊允許被訪問類D的模塊進(jìn)行訪問。

? 3·被訪問類C不是public的,但是它與訪問類D處于同一個包中。

4.2.字段解析

? 如果C本身就包含了簡單名稱和字段描述符都與目標(biāo)相匹配的字段,則返回這個字段的直接引用,查找結(jié)束。

? 否則,如果在C中實現(xiàn)了接口,將會按照繼承關(guān)系從下往上遞歸搜索各個接口和它的父接口, 如果接口中包含了簡單名稱和字段描述符都與目標(biāo)相匹配的字段,則返回這個字段的直接引用,查找 結(jié)束。

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

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

4.3.方法解析

? 方法解析的第一個步驟與字段解析一樣,也是需要先解析出方法表的class_index項中索引的方 法所屬的類或接口的符號引用,如果解析成功,那么我們依然用C表示這個類,接下來虛擬機(jī)將會按 照如下步驟進(jìn)行后續(xù)的方法搜索:

? 1.由于Class文件格式中類的方法和接口的方法符號引用的常量類型定義是分開的,如果在類的 方法表中發(fā)現(xiàn)class_index中索引的C是個接口的話,那就直接拋出java.lang.IncompatibleClassChangeError 異常。

? 2.如果通過了第一步,在類C中查找是否有簡單名稱和描述符都與目標(biāo)相匹配的方法,如果有則 返回這個方法的直接引用,查找結(jié)束。

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

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

? 5.否則,宣告方法查找失敗,拋出java.lang.NoSuchMethodError。

最后,如果查找過程成功返回了直接引用,將會對這個方法進(jìn)行權(quán)限驗證,如果發(fā)現(xiàn)不具備對此 方法的訪問權(quán)限,將拋出java.lang.IllegalAccessError異常。

4.4.接口方法解析

? 接口方法也是需要先解析出接口方法表的class_index項中索引的方法所屬的類或接口的符號引 ,如果解析成功,依然用C表示這個接口,接下來虛擬機(jī)將會按照如下步驟進(jìn)行后續(xù)的接口方法搜 索:

? 1.與類的方法解析相反,如果在接口方法表中發(fā)現(xiàn)class_index中的索引C是個類而不是接口,那 么就直接拋出java.lang.IncompatibleClassChangeError異常。

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

? 3.否則,在接口C的父接口中遞歸查找,直到j(luò)ava.lang.Object類(接口方法的查找范圍也會包括 Object類中的方法)為止,看是否有簡單名稱和描述符都與目標(biāo)相匹配的方法,如果有則返回這個方 法的直接引用,查找結(jié)束。

? 4.對于規(guī)則3,由于Java的接口允許多重繼承,如果C的不同父接口中存有多個簡單名稱和描述符 都與目標(biāo)相匹配的方法,那將會從這多個方法中返回其中一個并結(jié)束查找,《Java虛擬機(jī)規(guī)范》中并 沒有進(jìn)一步規(guī)則約束應(yīng)該返回哪一個接口方法。但與之前字段查找類似地,不同發(fā)行商實現(xiàn)的Javac編譯器有可能會按照更嚴(yán)格的約束拒絕編譯這種代碼來避免不確定性。

? 5.否則,宣告方法查找失敗,拋出java.lang.NoSuchMethodError異常。

5.初始化

? 類的初始化階段是類加載過程的最后一個步驟,之前介紹的幾個類加載的動作里,除了在加載階段用戶應(yīng)用程序可以通過自定義類加載器的方式局部參與外,其余動作都完全由Java虛擬機(jī)來主導(dǎo)控 制。

? 直到初始化階段,Java虛擬機(jī)才真正開始執(zhí)行類中編寫的Java程序代碼,將主導(dǎo)權(quán)移交給應(yīng)用程序。

? 初始化階段就是執(zhí)行類構(gòu)造器<clinit>()方法的過程。<clinit>()并不是程序員在Java代碼中直接編寫 的方法,它是Javac編譯器的自動生成物,

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

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

? 接口中不能使用靜態(tài)語句塊,但仍然有變量初始化的賦值操作,因此接口與類一樣都會生成 <clinit>()方法。

? 如果多個線程同 時去初始化一個類,那么只會有其中一個線程去執(zhí)行這個類的<clinit>()方法

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容