總體概括:將.class文件加載到內(nèi)存烹玉,并對數(shù)據(jù)進行校驗,轉(zhuǎn)換解析和初始化,最后形成一個能被虛擬機使用的java類型预厌,這個過程就是類的加載機制
虛擬機的加載機制包括以下幾個重要步驟:
1.加載 ? ? 獲取類.class文件的二進制流娩鹉,在方法區(qū) 生成一個代表這個類的java.lang.Class對象攻谁,作為這個類的各種數(shù)據(jù)的訪問入口。
2.驗證 ? 確保.class文件的字節(jié)流中包含的信息符合虛擬機的要求弯予,不會危害虛擬機的自身安全
3.準備? 為類變量分配內(nèi)存并設(shè)置類變量初始值(零值)
4.解析 ? ? 是虛擬機將常量池內(nèi)的符號引用轉(zhuǎn)換為直接引用的過程
5.初始化?? 為類變量賦值 執(zhí)行Clinit方法
先來一張圖整體了解一下:
下面開始具體分析
1.加載過程
類的加載主要是由類加載器(ClassLoader)完成戚宦,類加載器會在后面的文章中介紹 類加載器工作流程
類的加載主要完成三件事
1)通過類的全限定名來獲取定義此類的二進制字節(jié)流,并加載到內(nèi)存(也就是.class文件)
2)將二進制流中的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)換為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu)
3)在方法區(qū)中生成一個代表該類的java.lang.Class對象實例熙涤,用來作為訪問該類各種數(shù)據(jù)的外部接口
值得注意的有一下兩點:
1.二進制流的獲取有多種途徑最典型的有從zip中獲取阁苞,如.jar包,從網(wǎng)絡(luò)中獲取祠挫,運算時生成那槽,由其他文件生成如.jsp文件
2.如果加載的是一個非數(shù)組類,加載階段可以直接用系統(tǒng)提供的引導(dǎo)類加載器加載等舔,也可以由一個繼承自ClassLoader的自定義加載器加載骚灸。
1)如果是數(shù)組類,數(shù)組本身不通過類加載器構(gòu)建慌植,而是由java虛擬機直接創(chuàng)建甚牲。
2)但是數(shù)組元素類型如果是引用類型如String[],依舊會采用類加載器去加載蝶柿,并且該數(shù)組將在加載該元素類型的類加載器的類名稱空間上被標識丈钙。(一個類必須類加載器一起決定了唯一性)
3)如果數(shù)組元素類型不是引用類型如int[],虛擬機會把數(shù)組標記為與引導(dǎo)類加載器相關(guān)聯(lián)交汤。
4)如果數(shù)組的元素類型不是引用類型雏赦,那么數(shù)組的可見性將默認為public
2.驗證過程
驗證的主要目的,是要確保class文件的字節(jié)流包含的信息要符合當前虛擬機的要求,并且不會有惡意的代碼會危害到虛擬機的安全星岗。
驗證過程分為四大塊:
1.文件格式的驗證 --保證了字節(jié)流能真確的解析并存儲到方法區(qū)填大,這階段是基于二進制,自由通過了這一階段的驗證俏橘,字節(jié)流才會進入內(nèi)存的方法區(qū)中存儲允华。其后面的三個驗證階段都是在方法區(qū)的存儲結(jié)構(gòu)中進行
2.元數(shù)據(jù)的驗證 (語法檢測)
3.字節(jié)碼的驗證? -- 確定程序的語義是否合法,符合邏輯
4.符號引用的驗證 -- 可以看做是對類自身以外的信息進行匹配性校驗寥掐,發(fā)生在虛擬機將符號引用轉(zhuǎn)換為直接引用的時候靴寂,也就是類加載的 解析階段。
具體的驗證方式與方法相當復(fù)雜曹仗,此處不一一分析榨汤,有興趣的可以去查閱《java虛擬機規(guī)范》一書的class文件結(jié)構(gòu)一章
驗證階段完成之后,二進制流已經(jīng)在方法區(qū)中正確存儲怎茫,語法語義校驗也已經(jīng)通過收壕,符號引用驗證也已經(jīng)完成。
3.準備過程
準備階段可以看做是類的系統(tǒng)初始化階段轨蛤,也就是為類的 類變量 分配內(nèi)存空間并設(shè)置初始值的過程
注意:此處與后文提到的類變量均是指類的 static變量
通常情況下 初始化類變量也就是為static變量賦 零值 的過程?? 如:
public? static int? value =123;在經(jīng)過準備階段之后value的值為 0蜜宪,要到初始化階段結(jié)束之后value才會為123
但是也有特殊情況,那就是如果類字段的字段屬性表中存在該屬性祥山,那么在準備階段變量value就會被初始化為該屬性指定的值圃验。如:
public? static final? int? value =123; 那么準備階段之后value的值為123.
也就是說,常量對象會在準備階段就會賦值
4.解析過程
解析過程是將常量池中的 符號引用 轉(zhuǎn)換為 直接引用 的過程
解析階段分為四種情況
1)類和接口的解析
????? 假設(shè)當前代碼所處的類為 D缝呕,如果要把一個從未解析過的符號引用 N 解析為一個類或者接口? C? 的直接引用澳窑,那個它會經(jīng)歷如下幾個階段:
???? (1)如果 C 不是一個數(shù)組類型,那么虛擬機會把代表 N 的全限定名傳給 D 的類加載器去加載這個類C供常,在加載過程中又可能觸發(fā)其他相關(guān)類的加載動作摊聋,一旦加載過程出現(xiàn)異常,則解析失敗栈暇。
????? (2)如果 C 是一個數(shù)組類型麻裁,并且數(shù)組的元素類型為對象,也就是N的描述符會為[Ljava/lang/Integer的形式源祈,那將會按照上面第一點的加載規(guī)則去加載對應(yīng)的類型煎源,接著由虛擬機生成一個代表這個數(shù)組維度和元素的數(shù)組對象。
?????? (3)如果上面的步驟沒有異常香缺,那么 C 在虛擬機中已經(jīng)是一個有效的類或者接口了手销,但解析完成之前還要進行符號引用驗證,確定 D 是否具備對 C 的訪問權(quán)限
2)字段的解析
??????? 假設(shè)當前字段所在的類或接口已經(jīng)解析完成图张,用 C 表示:
???????? (1)如果 C 中本身就包含了簡單名稱與字段描述都與目標相匹配的字段原献,則返回這個字段的直接引用馏慨,查找結(jié)束。
???????? (2)否則姑隅,如果該類實現(xiàn)了接口,將會按照繼承關(guān)系從下往上遞歸搜索各個接口跟父接口倔撞,如果接口中包含了簡單名稱與字段描述都與目標相匹配的字段讲仰,則返回這個字段的引用,查找結(jié)束痪蝇。
???????? (3)否則鄙陡,如果該類不是java.lang.Object的話,將會按照繼承關(guān)系從下往上遞歸搜索其父類躏啰,如果父類中包含了簡單名稱與字段描述都與目標相匹配的字段趁矾,則返回這個字段的引用,查找結(jié)束给僵。
????????? (4) 否則毫捣,查找失敗,拋出異常
????????? (5) 如果查找成功帝际,還要對字段進訪問行權(quán)限驗證蔓同,如果不具備訪問權(quán)限也將拋出異常
3)類方法的解析
???????假設(shè)當前的類或接口已經(jīng)解析完成,用 C表示:
????????? (1) 類方法 與 接口方法符號引用的常量類型定義是分開的蹲诀,如果類方法表中發(fā)現(xiàn)class_index中索引的 C 是個接口斑粱,直接拋出異常
????????? (2) 如果通過了第一步,在類C中查找是否有簡單名稱與描述符都與目標相匹配的方法脯爪,如果有返回這個方法的直接引用则北,查找結(jié)束。
?????????? (3)否則痕慢,在類 C 的父類中遞歸查找是否有簡單名稱與描述符都與目標相匹配的方法尚揣,如果有返回這個方法的直接引用,查找結(jié)束守屉。
??????????? (4)否則惑艇,在類 C 實現(xiàn)的接口以及接口的父類中遞歸查找否有簡單名稱與描述符都與目標相匹配的方法,如果有返回這個方法的直接引用拇泛,查找結(jié)束滨巴。
??????????? (5) 否則,查找失敗俺叭,拋出異常
??????????? (6) 如果查找成功恭取,還要對方法進行訪問權(quán)限驗證,如果不具備訪問權(quán)限也將拋出異常
4)接口方法的解析
?假設(shè)當前的類或接口已經(jīng)解析完成熄守,用C表示:
????????? (1) 類方法與接口方法符號引用的常量類型定義是分開的蜈垮,如果接口方法表中發(fā)現(xiàn)class_index中索引的C是個類耗跛,直接拋出異常
????????? (2) 如果通過了第一步,在接口C中查找是否有簡單名稱與描述符都與目標相匹配的方法攒发,如果有返回這個方法的直接引用调塌,查找結(jié)束。
?????????? (3)否則惠猿,在接口 C 的父類中遞歸查找(直到j(luò)ava.lang.Object)是否有簡單名稱與描述符都與目標相匹配的方法羔砾,如果有返回這個方法的直接引用,查找結(jié)束偶妖。
??????????? (4) 否則姜凄,查找失敗,拋出異常
??????????? (5) 由于接口中的方法默認都是public的趾访,所以不存在訪問權(quán)限校驗的問題
解析過程相對復(fù)雜态秧,但原理相通,比較好理解
5.初始化過程
初始化過程是類加載階段的最后一步扼鞋,在編譯器生成.class文件時會在class文件中自動生成一個類初始化方法與對象初始化方法申鱼。即Clinit ()【類初始化方法】 與 init()【對象初始化方法】。
類初始化的過程實質(zhì)上是為 類變量(static) 根據(jù)程序員的意愿賦值的過程藏鹊。(執(zhí)行Clinit方法的過程)
Clinit方法的定義是:編譯器自動收集類中的所有 類變量(static) 的賦值動作和 靜態(tài)語句塊? 中的語句合并產(chǎn)生的润讥,編譯器收集的順序是由語句在源文件中出現(xiàn)的順序決定的,靜態(tài)語句塊中只能訪問到定義在靜態(tài)語句塊之前的變量盘寡,定義在它后面的變量可以賦值但不能訪問楚殿。
Clinit方法的執(zhí)行要注意以下幾點:
1.在執(zhí)行子類的 clinit 方法時會確保父類的 clinit 方法先執(zhí)行完畢
2.由于父類的clinit方法先執(zhí)行,所以父類中的靜態(tài)塊與靜態(tài)變量要先于子類的賦值操作
3.如果子類與父類中都沒有靜態(tài)變量與靜態(tài)塊竿痰,則編譯器不會生成clinit方法
4.接口中的clinit方法
??? (1)接口中不能使用靜態(tài)塊脆粥,但是依舊可以有賦值操作,所以接口也有clinit方法
??? (2)但是接口與類不同的是影涉,接口執(zhí)行clinit方法時不需要先執(zhí)行父類的clint方法
??? (3)只有當接口的 類變量 使用時变隔,父接口才會初始化
??? (4)接口的實現(xiàn)類 在初始化時一樣不會執(zhí)行接口的clinit方法
5.clinit方法線程安全,在同一個類加載器下蟹倾,一個類型只會初始化一次匣缘!
到此處類的加載過程已經(jīng)分析完畢!