代碼編譯的結(jié)果從本地機(jī)械碼轉(zhuǎn)變?yōu)樽止?jié)碼,是存儲格式發(fā)展的一小步全景,卻是編程語言發(fā)展的一大步
類加載的時機(jī)
類從被加載到虛擬機(jī)內(nèi)存中開始耀石,到卸載出內(nèi)存為止,它的整個生命周期包括:加載(Loading)爸黄、驗證(Verificatio)帕胆、準(zhǔn)備(Preparation)屠阻、解析(Resolution)湿滓、初始化(Initialization)糠亩、使用(Using)和卸載(Unloading)7個階段
上述階段通常都是互相交叉地混合式進(jìn)行的,會在一個階段執(zhí)行地過程中調(diào)用鲁驶、激活另一個階段鉴裹。下面5種情況必須立即對類進(jìn)行“初始化”(加載、驗證、準(zhǔn)備在這之前開始):
- 遇到new径荔、getstatic督禽、putstatic或invokestatic這4條字節(jié)碼指令
- 使用java.lang.reflect包地方法對類進(jìn)行反射調(diào)用
- 初始化子類前要先初始化父類(接口不要求其父接口全部完成初始化,只有在使用父接口的時候才會初始化)
- 虛擬機(jī)啟動時总处,需要指定執(zhí)行地主類(包含main()方法的類)
- 當(dāng)使用JDK 1.7的動態(tài)語言支持時狈惫,如果一個java.lang.invoke.MethodHandle實例最后解析結(jié)果REF_getStatic、REF_putStatic鹦马、REF_invokeStatic的方法句柄胧谈,并且這個句柄所對應(yīng)的類沒有進(jìn)行過初始化
類加載的過程
加載
在加載階段,虛擬機(jī)需要完成下列3件事:
- 通過一個類的全限定名來獲取定義此類的二進(jìn)制字節(jié)流
- 將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu)
- 在內(nèi)存中生成一個代表這個類的java.lang.Class對象荸频,作為方法區(qū)這個類的各種數(shù)據(jù)的訪問入口
- 非數(shù)組類:可控性最強菱肖,加載階段既可以使用系統(tǒng)提供的引導(dǎo)類加載器,也可以使用自定義的類加載器
-
數(shù)組類:由java虛擬機(jī)直接創(chuàng)建旭从,數(shù)組類的元素類型(Element Type)最終是靠類加載器去創(chuàng)建
- 如果數(shù)組類的組件類型(Component Type稳强,即數(shù)組去掉一個維度的類型)是引用類型,那就遞歸加載這個組件類型和悦,數(shù)組將在加載該組件類型的類加載器的類名稱空間上被標(biāo)識
- 如果數(shù)組的組件類型不是引用類型退疫,java虛擬機(jī)將會把數(shù)組標(biāo)記為與引導(dǎo)類加載器關(guān)聯(lián)
- 數(shù)組類的可見性與它的組件類型的可見性一致,如果組件類型不是引用類型鸽素,那數(shù)組類的可見性將默認(rèn)為public
加載階段與連接階段的部分內(nèi)容(如一部分字節(jié)碼文件格式驗證動作)是交叉進(jìn)行的褒繁,加載階段尚未完成,連接階段可能已經(jīng)開始
驗證
驗證是連接階段的第一步付鹿,這一階段的目的是為了確保Class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求澜汤,并且不會危害虛擬機(jī)自身的安全。驗證階段大致上會完成下面4個階段的檢驗動作:
- 文件格式驗證:第一階段要驗證字節(jié)流是否符合Class文件格式的規(guī)范舵匾,并且能被當(dāng)前版本的虛擬機(jī)處理。主要目的是保證輸入的字節(jié)流能正確解析并存儲于方法區(qū)內(nèi)谁不,格式上符合描述一個java類型信息的要求坐梯。這個階段的驗證是基于二進(jìn)制字節(jié)流進(jìn)行的,只有通過了這個階段的驗證后刹帕,字節(jié)流才會存儲到方法區(qū)中吵血,所以后面的3個驗證階段全部是基于方法區(qū)的存儲結(jié)構(gòu)進(jìn)行的,不會再直接操作字節(jié)流
- 元數(shù)據(jù)驗證:第二階段是對字節(jié)碼描述的信息進(jìn)行語義分析偷溺,以保證其描述的信息符合java語言規(guī)范的要求蹋辅。主要目的是對元數(shù)據(jù)信息進(jìn)行語義校驗,保證不存在不符合java語言規(guī)范的元數(shù)據(jù)信息
- 字節(jié)碼驗證:第三階段是整個驗證過程中最復(fù)雜的一個階段挫掏,主要目的是通過數(shù)據(jù)流和控制流分析侦另,確定程序語義是合法的、符合邏輯的。如果一個類方法體的字節(jié)碼沒有通過字節(jié)碼驗證褒傅,那肯定是有問題的弃锐;但如果一個方法體通過了字節(jié)碼驗證,也不能說明其一定就是安全的
- 符號引用驗證:最后一個階段的驗證發(fā)生在虛擬機(jī)符號引用轉(zhuǎn)化為直接引用的時候殿托,這個轉(zhuǎn)化動作將在連接的解析階段中發(fā)生霹菊,可以看做是對類自身以外的信息進(jìn)行匹配性校驗。目的是確保解析動作能正常執(zhí)行
準(zhǔn)備
準(zhǔn)備階段是正式為類變量分配內(nèi)存并設(shè)置類變量初始值的階段支竹,這些變量所使用的內(nèi)存都將在方法區(qū)中進(jìn)行分配旋廷。有兩個要點:
- 類變量是指被static修飾的變量,不包括實例變量
- 初始值在通常情況下是數(shù)據(jù)類型的零值礼搁。ConnstantValue屬性(final)存在時饶碘,變量就會被初始化為ConstantValue屬性所指定的值
解析
解析階段是虛擬機(jī)將常量池內(nèi)的符號引用替換為直接引用的過程
- 符號引用(Symbolic References):符號引用以一組符號來描述所引用的目標(biāo),符號可以是任何形式的字面量叹坦,只要使用是能無歧義地定位到目標(biāo)即可熊镣。符號引用與虛擬機(jī)實現(xiàn)的內(nèi)存布局無關(guān),引用地目標(biāo)不一定已經(jī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)在內(nèi)存中存在
符號引用更普適,直接引用更迅速启泣。java虛擬機(jī)會對除invokedynamic指令外的其它指令的解析結(jié)果進(jìn)行緩存涣脚,從而避免解析動作重復(fù)進(jìn)行。如果一個符號引用之前已經(jīng)被成功解析過寥茫,那么后續(xù)的引用解析就應(yīng)當(dāng)一直成功遣蚀;如果第一次解析失敗,那么其它指令對這個符號的解析也應(yīng)該收到相同的異常纱耻。invokedynamic指令是等到程序?qū)嶋H運行到這條指令的時候芭梯,解析動作才能進(jìn)行
- 類或接口的解析
- 如果類或接口不是數(shù)組類型,那虛擬機(jī)將會把代表符號引用的全限定名交給類加載器加載
- 如果類或接口是一個數(shù)組類型弄喘,并且數(shù)組的元素類型為對象玖喘,就按第1點加載數(shù)組元素類型,接著由虛擬機(jī)生成一個代表此數(shù)組維度和元素的數(shù)組對象
- 如果上面的步驟沒有出現(xiàn)任何異常蘑志,還需進(jìn)行符號引用驗證累奈,確認(rèn)類是否具備對該類或接口的訪問權(quán)限
- 字段解析
- 需要先對字段表的class_index項索引的CONSTANT_Class_info符號引用進(jìn)行解析
- 如果類本身就包含了簡單名稱和字段描述符都與目標(biāo)相匹配的字段贬派,就返回這個字段的直接引用,查找結(jié)束
- 如果類實現(xiàn)了接口费尽,就按照繼承關(guān)系從下往上遞歸搜索各個接口赠群,匹配到就返回直接引用,查找結(jié)束
- 如果類不是Object類旱幼,就按照繼承關(guān)系從下往上遞歸搜索父類查描,匹配到就返回直接引用,查找結(jié)束
- 否則柏卤,查找失敗冬三,拋出java.lang.NoSuchFieldError異常
- 如果查找過程成功返回了引用,將會對這個字段進(jìn)行權(quán)限驗證缘缚,如果發(fā)現(xiàn)不具備對字段的訪問權(quán)限勾笆,就拋出java.lang.IllegalAccessError異常
- 類方法解析 ==> 實現(xiàn)方法重寫
- 需要先對方法表的class_index項索引的方法所屬的類或接口的符號引用進(jìn)行解析
- 類方法和接口方法符號引用的常量類型定義是分開的,如果類方法表中發(fā)現(xiàn)class_index中索引的類是個接口桥滨,就拋出java.lang.IncompatibleClassChangeError異常
- 如果在類中具有簡單名稱與描述符匹配的方法窝爪,就返回這個方法的直接引用,查找結(jié)束
- 在類的父類中查找齐媒,匹配到就返回方法的直接引用蒲每,查找結(jié)束
- 在類的接口列表及它們的父接口中查找,匹配到說明類是個抽象類喻括,拋出java.lang.AbstractMethodError異常
- 否則邀杏,方法查找失敗,拋出java.lang.NoSuchMethodError異常
- 如果查找過程成功返回了引用唬血,將會對這個方法進(jìn)行權(quán)限驗證望蜡,如果發(fā)現(xiàn)不具備此方法的訪問權(quán)限,就拋出java.lang.IllegalAccessError異常
- 接口方法解析
- 需要先對接口方法表的class_index項索引的方法所屬的類或接口的符號引用進(jìn)行解析
- 如果接口方法表中發(fā)現(xiàn)class_index中索引的接口是類拷恨,就拋出java.lang.IncompatibleClassChangeError異常
- 如果接口中存在簡單名稱與描述符都匹配的方法脖律,就返回這個方法的直接引用,查找結(jié)束
- 在接口的父接口中遞歸查找腕侄,匹配到就返回方法的直接引用状您,查找結(jié)束
- 否則,查找失敗兜挨,拋出java.lang.NoSuchMethodError異常
初始化
類初始化是類加載過程的最后一步,在這個階段才真正開始執(zhí)行類中的字節(jié)碼眯分。初始化階段是執(zhí)行類構(gòu)造器<clinit>()
方法的過程拌汇。
-
<clinit>()
方法與類的構(gòu)造函數(shù)(<init>()方法
)不同,它不需要顯式調(diào)用父類構(gòu)造器弊决,虛擬機(jī)會保證在子類的<clinit>()
方法執(zhí)行之前噪舀,父類的<clinit>()
方法已經(jīng)執(zhí)行完畢 - 由于父類的
<clinit>()
方法先執(zhí)行魁淳,因此父類中定義的靜態(tài)語句塊要先于子類執(zhí)行 -
<clinit>()
方法對于類或接口來說不是必需的,如果一個類中沒有靜態(tài)語句塊与倡,也沒有對變量賦值操作界逛,那么編譯器可以不為這個類生成<clinit>()
方法 - 接口中不能使用靜態(tài)語句塊,但仍然由變量初始化的賦值操作纺座,因此接口與類一樣都會生成
<clinit>()
方法息拜,但與類不同的是,執(zhí)行接口的<clinit>()
方法不需要先執(zhí)行父接口的<clinit>()
方法净响,只有當(dāng)父接口中定義的變量使用時少欺,父接口才會初始化。另外馋贤,接口的實現(xiàn)類在初始化時也一樣不會執(zhí)行接口的<clinit>()
方法 -
虛擬機(jī)會保證一個類的
<clinit>()
方法在多線程環(huán)境中被正確地加鎖赞别、同步
類加載器
類與類加載器
類加載器雖然只用于實現(xiàn)類的加載動作,但在java程序中起到的作用卻遠(yuǎn)不止類加載階段配乓。對于任意一個類仿滔,都需要由加載它的類加載器和這個類本身一同確立其在java虛擬機(jī)中的唯一性,每個類加載器犹芹,都擁有一個獨立的類命名空間崎页。當(dāng)一個Class文件被不同的類加載器加載時,加載生成的兩個類必定不相等(equals()羽莺、isAssignableFrom()实昨、isInstance()、instanceof關(guān)鍵字的結(jié)果為false)
雙親委派機(jī)制
從java虛擬機(jī)的角度來看盐固,只存在兩種不同的類加載器:一種是啟動類加載器(Bootstrap ClassLoader)荒给,這個類加載器使用c++實現(xiàn),是虛擬機(jī)的一部分刁卜;另一種是所有其他的類加載器志电,這些類加載器都由java實現(xiàn),獨立于虛擬機(jī)外部蛔趴,并且全部繼承自抽象類java.lang.ClassLoader
從java開發(fā)人員的角度看挑辆,絕大部分java程序都會使用到以下3中系統(tǒng)提供得加載器:
-
啟動類加載器(Bootstrap ClassLoader):這個類負(fù)責(zé)將存放在
<JAVA_HOME>\lib
目錄中,或者被-Xbootclasspath參數(shù)所指定的路徑中的類庫加載到虛擬機(jī)內(nèi)存中 -
擴(kuò)展類加載器(Extension ClassLoader):這個加載器由sun.misc.Launcher$ExtClassLoader實現(xiàn)孝情,它負(fù)責(zé)加載
<JAVA_HOME>\lib\ext
目錄中或者被java.ext.dirs系統(tǒng)變量所指定的路徑中的所有類庫鱼蝉,開發(fā)者可以直接使用擴(kuò)展類加載器 - 應(yīng)用程序類加載器(Application ClassLoader):這個類加載器由sun.misc.Launcher$AppClassLoader實現(xiàn)。由于這個類加載器是ClassLoader中的getSystemClassLoader()方法的返回值箫荡,所以一般也稱為系統(tǒng)類加載器魁亦,負(fù)責(zé)加載用戶類路徑(ClassPath)上所指定的類庫,開發(fā)者可以直接使用這個類加載器
雙親委派模型的工作過程是:如果一個類加載器收到了類加載的請求羔挡,它首先不會自己去嘗試加載這個類洁奈,而是把這個請求委派給父類加載器去完成间唉,每一個層次的類加載器都是如此,因此所有的加載請求最終都應(yīng)該傳送到頂層的啟動類加載器中利术,只有當(dāng)父加載器反饋自己無法完成這個加載請求(它的搜索范圍中沒有找到所需的類)時呈野,子加載器才會嘗試自己去加載
雙親委派機(jī)制之外
雙親委派模型并不是一個強制性的約束模型,而是java設(shè)計者推薦給開發(fā)者的類加載器實現(xiàn)方式印叁。到目前為止被冒,雙親委派模型主要出現(xiàn)過3次較大規(guī)模的“被破壞”情況
- 第一次是為了jdk1.2向上兼容,添加了一個新的findClass()方法
- 第二次是由于這個模型本身的缺陷導(dǎo)致喉钢,當(dāng)基礎(chǔ)類需要回調(diào)用戶代碼時姆打,例如JNDI、JDBC肠虽、JCE幔戏、JAXB、JBI等税课,為此在Thread類中添加了線程上下文類加載器(Thread Context ClassLoader)
- 第三次是由于“熱加載”闲延,即追求即插即用的效果,為此出現(xiàn)了OSGi環(huán)境韩玩,在這個環(huán)境下類加載器發(fā)展為網(wǎng)狀結(jié)構(gòu)垒玲,OSGi的搜索順序:
- 將以java.* 開頭的類委派給父類加載器加載
- 將委派列表名單內(nèi)的類委派給父類加載器加載
- 將Import列表中的類委派給Export這個類的Bundle的類加載器加載
- 查找當(dāng)前Bundle的ClassPath,使用自己的類加載加載
- 查找類是否在自己的Fragment Bundle中找颓,如果在合愈,則委派給Fragment Bundle的類加載器加載
- 查找Dynamic Import列表的Bundle,委派給對應(yīng)的Bundle的類加載器加載
- 加載失敗