類加載機(jī)制
虛擬機(jī)把描述類等數(shù)據(jù)從Class文件加載到內(nèi)存,并對數(shù)據(jù)進(jìn)行校驗(yàn)、轉(zhuǎn)化解析和初始化,最終形成可以被虛擬機(jī)直接使用的Java類型。
- 類型的加載、鏈接、初始化過程都是在程序運(yùn)行期間完成的
- Java動(dòng)態(tài)拓展的語言特性就是依賴于運(yùn)行期動(dòng)態(tài)加載和動(dòng)態(tài)鏈接
1.類加載的時(shí)機(jī)
類從被加載到虛擬機(jī)內(nèi)存中開始到卸載出內(nèi)存為止,整個(gè)生命周期包括:加載、驗(yàn)證、準(zhǔn)備、解析、初始化、使用、卸載七個(gè)階段。其實(shí)。驗(yàn)證、準(zhǔn)備、解析3個(gè)部分統(tǒng)稱為鏈接。
- 七個(gè)階段是按順序“開始”,而不是按順序“進(jìn)行”或者“完成”,因?yàn)檫@些階段通常都是交互進(jìn)行的,通常會(huì)在一個(gè)階段執(zhí)行的過程中調(diào)用、激活下另外一個(gè)階段
- 什么時(shí)候需要開始類加載的第一個(gè)階段(加載)?虛擬機(jī)規(guī)范中被沒有強(qiáng)制約束,所以可以交給虛擬機(jī)的具體實(shí)現(xiàn)自由把握
- 初始化階段強(qiáng)制規(guī)定有切只有5中情況必須對類進(jìn)行“初始化”(而加載、校驗(yàn)、準(zhǔn)備、解析自然需要在此之前)
- 遇到new、getstatic、putstatic、或invokestatic這4條字節(jié)碼指令時(shí),如果沒有進(jìn)行過初始化,則需要先觸發(fā)其初始化。生成這4條指令最常見的Java代碼場景時(shí):使用new關(guān)鍵字實(shí)例化對象的時(shí)候、讀取或者設(shè)置一個(gè)類的靜態(tài)字段(被finale修飾、已在編譯期把結(jié)果放入常量池的靜態(tài)字段除外)的時(shí)候,以及調(diào)用一個(gè)類的靜態(tài)方法的時(shí)候。
- 使用java.lang.reflect包的方法對類進(jìn)行反射調(diào)用的時(shí)候,如果類沒有進(jìn)行過初始化,則需要觸發(fā)其初始化。
3.當(dāng)初始化一個(gè)類的時(shí)候,如果發(fā)現(xiàn)其父類還沒有進(jìn)行過初始化,則需要先觸發(fā)其父類的初始化。 - 當(dāng)虛擬機(jī)啟動(dòng)時(shí),用戶需要指定一個(gè)要執(zhí)行的主類(包含main()方法的那個(gè)類),虛擬機(jī)會(huì)先初始化這個(gè)主類。
- 當(dāng)使用JDK1.7動(dòng)態(tài)語言支持時(shí),如果java.lang.invoke.MethonHandle實(shí)例最后的解析結(jié)果REF_getStatic、REF_putStatic、REF_invokeStatia的方法句bing,并且這個(gè)方法句柄對應(yīng)的類沒有進(jìn)行過初始化,則需要觸發(fā)其初始化。
- 當(dāng)一個(gè)類在初始化,要求其父類全部都已經(jīng)初始化過了,但是一個(gè)接口初始化時(shí),并不要求其父接口全部都完成初始化,只有在真正使用到父接口的時(shí)候(如引用接口中定義的常量)才會(huì)初始化。
2.類加載的過程
2.1.加載
- 通過一個(gè)類的全限定名來獲取定義此類的二進(jìn)制字節(jié)流
- 將這個(gè)字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化成方法區(qū)的數(shù)據(jù)結(jié)構(gòu)
- 在內(nèi)存中生成一個(gè)代表這個(gè)類的java.lang.Class對象,作為方法區(qū)這個(gè)類的各種數(shù)據(jù)的訪問入口(并沒有明確規(guī)定所在Java堆中,對HotSpot虛擬機(jī)而言,Class對象比較特殊,它雖然所對象,但是存放在方法區(qū)里面)
- 加載階段完成后,虛擬機(jī)外部的二進(jìn)制字節(jié)流將按照虛擬機(jī)所需的格式存儲(chǔ)在方法區(qū)之中
- 加載階段與連接階段的部分內(nèi)容時(shí)交叉進(jìn)行的,加載階段未完成,連接階段可能已經(jīng)開始,但在這些夾在加載階段之中進(jìn)行的動(dòng)作,仍然屬于連接階段的內(nèi)容,這兩個(gè)階段的開始時(shí)間仍然保持著固定的順序
2.2驗(yàn)證
驗(yàn)證字節(jié)流是否符合Class文件格式的規(guī)范,并且能被當(dāng)前版本的虛擬機(jī)處理
- 1.文件格式驗(yàn)證階段:
- 前4個(gè)字節(jié)是否以魔數(shù) 0xCAFFBABF開頭
- 魔數(shù)后(2+2個(gè)字節(jié))的主次版本號(hào)是否在當(dāng)前虛擬機(jī)處理范圍內(nèi)
- 常量池中是否有不被支持的常量類型
- 指向常量的各種索引是否有指向不存在的常量或不符合類型的常量
......
主要驗(yàn)證是基于二進(jìn)制字節(jié)流進(jìn)行的,只有通過文件格式驗(yàn)證后,字節(jié)流才會(huì)進(jìn)入內(nèi)存的方法區(qū)進(jìn)行存儲(chǔ)
- 2.3元數(shù)據(jù)驗(yàn)證
對字節(jié)碼描述的信息進(jìn)行語義分析,以保證其描述的信息符合Java語言規(guī)范的要求
- 這個(gè)類是否有父類(出了頂級(jí)object類,所有的類都應(yīng)該有父類)
- 這個(gè)類的父類是否繼承了不允許被繼承的類(被final修飾的類)
- 如果這個(gè)類不是抽象類,是否實(shí)現(xiàn)了父類或接口之中要求實(shí)現(xiàn)的所有方法
......
- 2.4字節(jié)碼驗(yàn)證
通過數(shù)據(jù)流和控制流分析,確定程序語義是合法的、符合邏輯的。 - 4.符號(hào)引用驗(yàn)證
最好一個(gè)驗(yàn)證階段發(fā)生在虛擬機(jī)將符號(hào)引用轉(zhuǎn)化為直接引用的時(shí)候,這個(gè)轉(zhuǎn)化動(dòng)作將在連接的第三階段---解析階段中發(fā)生。
2.3準(zhǔn)備
準(zhǔn)備階段是正式為類變量分配內(nèi)存,并設(shè)置類變量初始值的階段,這些變量所使用的內(nèi)存都將在方法區(qū)中進(jìn)行分配
- 這時(shí)候進(jìn)行內(nèi)存分配的僅包括類變量(被static修飾的變量),而不包括實(shí)例變量,實(shí)例變量將在對象實(shí)例化時(shí)隨著對象一起分配在Java堆中
- 上面說的初始值“通常情況”下說數(shù)據(jù)類型的零值。賦值用戶定義的值需要在初始化階段。比如
public static int value =123
非“通常情況”為
public static final int value =123
2.4解析
解析階段是虛擬機(jī)將常量池內(nèi)的符號(hào)引用替換為直接引用的過程
- 符號(hào)引用:符號(hào)引用以一組符號(hào)來描述所引用的目標(biāo),符號(hào)可以是任何形式的字面量,只要使用時(shí)能無歧義地定位到目標(biāo)即可,與虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局無關(guān)
- 直接引用:可以直接指向目標(biāo)的指針、相對偏移量、或是一個(gè)能間接定位到目標(biāo)的句柄,與虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局相關(guān)
- 解析動(dòng)作主要針對一下七種:類或接口、字段、類方法、接口方法、方法類型、方法句柄、限定符
2.5初始化
真正開始執(zhí)行類中定義的Java程序代碼(或者說是字節(jié)碼),是執(zhí)行類構(gòu)造器<clinit>()方法的過程
造器<clinit>()方法的描述
- <clinit>()方法由編譯器自動(dòng)收集類中所有的類變量的賦值動(dòng)作和靜態(tài)語句看(static{}塊)中的語句合并產(chǎn)生,編譯器收集說順序是由語句在原文件中出現(xiàn)的順序所決定的,靜態(tài)語句快中只能訪問到定義在靜態(tài)語句快之前到變量,定義在它之后的變量可以賦值,但是不能訪問。如下代碼:非法向前引用
public class demo{
static {
i =0;
System.out.pring(i); //編譯器提示“非法向前引用"
}
static int i = 1;
}
- <clinit>()方法與類構(gòu)造函數(shù)(實(shí)例構(gòu)造器<init>()方法)不同,它不需要顯示的調(diào)用父類構(gòu)造器,虛擬機(jī)會(huì)保證子類的<init>()方法執(zhí)行前,父類的<init>()方法已經(jīng)執(zhí)行完畢。因此在虛擬機(jī)中第一個(gè)被執(zhí)行的<init>()方法的類肯定是java.lang.Object
- <clinit>()方法由于父類的<init>()方法先執(zhí)行,也就意味著父類中定義的靜態(tài)語句塊要優(yōu)先于子類的變量賦值操作
- <clinit>()方法對于類或接口來說不是必須的,因?yàn)橐粋€(gè)類中如果沒有靜態(tài)語句快,自然就沒有對類變量賦值的操作,那么編譯器可以不為這個(gè)類生成<clinit>()方法
- 接口中不能使用靜態(tài)語句塊,但仍然有變量初始化的賦值操作,因此接口與類一樣都生成<clinit>()方法。但接口與類不同的是,執(zhí)行接口的<clinit>()方法不需要先執(zhí)行父接口的<clinit>()方法。只要父接口的變量使用時(shí),父接口才會(huì)初始化。另外,接口的實(shí)現(xiàn)類在初始化時(shí)也一樣不會(huì)執(zhí)行接口的<clinit>()方法
- 虛擬機(jī)會(huì)保證<clinit>()方法的在多線程環(huán)境中的同步安全
3類加載器
虛擬機(jī)團(tuán)隊(duì)把類加載階段的“通過一個(gè)類的全限定名來獲取描述此類的二進(jìn)制字節(jié)流”這個(gè)動(dòng)作放到Java虛擬機(jī)外部去實(shí)現(xiàn),以便讓應(yīng)用程序自己決定如何去獲取所需要的類。(熱修復(fù),熱更新等等)這個(gè)動(dòng)作的代碼模塊稱為“類加載器”
絕大部分Java程序都有用到以下3種系統(tǒng)提供的類加載器
- 啟動(dòng)類加載器
- 拓展類加載器
- 應(yīng)用程序類加載器
- 比較兩個(gè)類是否“相等”只有在這兩個(gè)類是由同一個(gè)類加載器加載的前提下才有意義,否則,即使這兩個(gè)類來源以同一個(gè)Class文件,被同一個(gè)虛擬機(jī)加載,只要加載他們的類加載不同,那這兩個(gè)類就必定不相等
- 雙親委派機(jī)制:如果一個(gè)類加載器收到一個(gè)類加載請求,它首先不會(huì)自己去嘗試加載這個(gè)類,而是把這個(gè)請求委派給父類加載器去完成,每一個(gè)層次的類加載器都是如此,因此所有的類加載請求最終都應(yīng)該傳送到頂層的啟動(dòng)類的加載器中,只有當(dāng)父類加載器反饋?zhàn)约簾o法完成這個(gè)請求(它的搜索范圍沒有找到所需的類)時(shí),自加載器才會(huì)嘗試自己去加載。好處是Java類隨著它的類加載器一起具備了一種帶有優(yōu)先級(jí)的層次關(guān)系