《深入理解java虛擬機(jī)》-虛擬機(jī)類加載機(jī)制

代碼編譯的結(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)備在這之前開始):

  1. 遇到new径荔、getstatic督禽、putstatic或invokestatic這4條字節(jié)碼指令
  2. 使用java.lang.reflect包地方法對類進(jìn)行反射調(diào)用
  3. 初始化子類前要先初始化父類(接口不要求其父接口全部完成初始化,只有在使用父接口的時候才會初始化)
  4. 虛擬機(jī)啟動時总处,需要指定執(zhí)行地主類(包含main()方法的類)
  5. 當(dāng)使用JDK 1.7的動態(tài)語言支持時狈惫,如果一個java.lang.invoke.MethodHandle實例最后解析結(jié)果REF_getStatic、REF_putStatic鹦马、REF_invokeStatic的方法句柄胧谈,并且這個句柄所對應(yīng)的類沒有進(jìn)行過初始化

類加載的過程

加載

在加載階段,虛擬機(jī)需要完成下列3件事:

  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ù)的訪問入口
  • 非數(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個階段的檢驗動作:

  1. 文件格式驗證:第一階段要驗證字節(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é)流
  2. 元數(shù)據(jù)驗證:第二階段是對字節(jié)碼描述的信息進(jìn)行語義分析偷溺,以保證其描述的信息符合java語言規(guī)范的要求蹋辅。主要目的是對元數(shù)據(jù)信息進(jìn)行語義校驗,保證不存在不符合java語言規(guī)范的元數(shù)據(jù)信息
  3. 字節(jié)碼驗證:第三階段是整個驗證過程中最復(fù)雜的一個階段挫掏,主要目的是通過數(shù)據(jù)流控制流分析侦另,確定程序語義是合法的、符合邏輯的。如果一個類方法體的字節(jié)碼沒有通過字節(jié)碼驗證褒傅,那肯定是有問題的弃锐;但如果一個方法體通過了字節(jié)碼驗證,也不能說明其一定就是安全的
  4. 符號引用驗證:最后一個階段的驗證發(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)行

  1. 類或接口的解析
  2. 如果類或接口不是數(shù)組類型,那虛擬機(jī)將會把代表符號引用的全限定名交給類加載器加載
  3. 如果類或接口是一個數(shù)組類型弄喘,并且數(shù)組的元素類型為對象玖喘,就按第1點加載數(shù)組元素類型,接著由虛擬機(jī)生成一個代表此數(shù)組維度和元素的數(shù)組對象
  4. 如果上面的步驟沒有出現(xiàn)任何異常蘑志,還需進(jìn)行符號引用驗證累奈,確認(rèn)類是否具備對該類或接口的訪問權(quán)限
  5. 字段解析
  6. 需要先對字段表的class_index項索引的CONSTANT_Class_info符號引用進(jìn)行解析
  7. 如果類本身就包含了簡單名稱和字段描述符都與目標(biāo)相匹配的字段贬派,就返回這個字段的直接引用,查找結(jié)束
  8. 如果類實現(xiàn)了接口费尽,就按照繼承關(guān)系從下往上遞歸搜索各個接口赠群,匹配到就返回直接引用,查找結(jié)束
  9. 如果類不是Object類旱幼,就按照繼承關(guān)系從下往上遞歸搜索父類查描,匹配到就返回直接引用,查找結(jié)束
  10. 否則柏卤,查找失敗冬三,拋出java.lang.NoSuchFieldError異常
  11. 如果查找過程成功返回了引用,將會對這個字段進(jìn)行權(quán)限驗證缘缚,如果發(fā)現(xiàn)不具備對字段的訪問權(quán)限勾笆,就拋出java.lang.IllegalAccessError異常
  12. 類方法解析 ==> 實現(xiàn)方法重寫
  13. 需要先對方法表的class_index項索引的方法所屬的類或接口的符號引用進(jìn)行解析
  14. 類方法和接口方法符號引用的常量類型定義是分開的,如果類方法表中發(fā)現(xiàn)class_index中索引的類是個接口桥滨,就拋出java.lang.IncompatibleClassChangeError異常
  15. 如果在類中具有簡單名稱與描述符匹配的方法窝爪,就返回這個方法的直接引用,查找結(jié)束
  16. 在類的父類中查找齐媒,匹配到就返回方法的直接引用蒲每,查找結(jié)束
  17. 在類的接口列表及它們的父接口中查找,匹配到說明類是個抽象類喻括,拋出java.lang.AbstractMethodError異常
  18. 否則邀杏,方法查找失敗,拋出java.lang.NoSuchMethodError異常
  19. 如果查找過程成功返回了引用唬血,將會對這個方法進(jìn)行權(quán)限驗證望蜡,如果發(fā)現(xiàn)不具備此方法的訪問權(quán)限,就拋出java.lang.IllegalAccessError異常
  20. 接口方法解析
  21. 需要先對接口方法表的class_index項索引的方法所屬的類或接口的符號引用進(jìn)行解析
  22. 如果接口方法表中發(fā)現(xiàn)class_index中索引的接口是類拷恨,就拋出java.lang.IncompatibleClassChangeError異常
  23. 如果接口中存在簡單名稱與描述符都匹配的方法脖律,就返回這個方法的直接引用,查找結(jié)束
  24. 在接口的父接口中遞歸查找腕侄,匹配到就返回方法的直接引用状您,查找結(jié)束
  25. 否則,查找失敗兜挨,拋出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ā)者可以直接使用這個類加載器
類加載器雙親委派機(jī)制模型

雙親委派模型的工作過程是:如果一個類加載器收到了類加載的請求羔挡,它首先不會自己去嘗試加載這個類洁奈,而是把這個請求委派給父類加載器去完成间唉,每一個層次的類加載器都是如此,因此所有的加載請求最終都應(yīng)該傳送到頂層的啟動類加載器中利术,只有當(dāng)父加載器反饋自己無法完成這個加載請求(它的搜索范圍中沒有找到所需的類)時呈野,子加載器才會嘗試自己去加載

雙親委派機(jī)制之外

雙親委派模型并不是一個強制性的約束模型,而是java設(shè)計者推薦給開發(fā)者的類加載器實現(xiàn)方式印叁。到目前為止被冒,雙親委派模型主要出現(xiàn)過3次較大規(guī)模的“被破壞”情況

  1. 第一次是為了jdk1.2向上兼容,添加了一個新的findClass()方法
  2. 第二次是由于這個模型本身的缺陷導(dǎo)致喉钢,當(dāng)基礎(chǔ)類需要回調(diào)用戶代碼時姆打,例如JNDI、JDBC肠虽、JCE幔戏、JAXB、JBI等税课,為此在Thread類中添加了線程上下文類加載器(Thread Context ClassLoader)
  3. 第三次是由于“熱加載”闲延,即追求即插即用的效果,為此出現(xiàn)了OSGi環(huán)境韩玩,在這個環(huán)境下類加載器發(fā)展為網(wǎng)狀結(jié)構(gòu)垒玲,OSGi的搜索順序:
  4. 將以java.* 開頭的類委派給父類加載器加載
  5. 將委派列表名單內(nèi)的類委派給父類加載器加載
  6. 將Import列表中的類委派給Export這個類的Bundle的類加載器加載
  7. 查找當(dāng)前Bundle的ClassPath,使用自己的類加載加載
  8. 查找類是否在自己的Fragment Bundle中找颓,如果在合愈,則委派給Fragment Bundle的類加載器加載
  9. 查找Dynamic Import列表的Bundle,委派給對應(yīng)的Bundle的類加載器加載
  10. 加載失敗
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末击狮,一起剝皮案震驚了整個濱河市佛析,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌彪蓬,老刑警劉巖寸莫,帶你破解...
    沈念sama閱讀 221,635評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異档冬,居然都是意外死亡膘茎,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評論 3 399
  • 文/潘曉璐 我一進(jìn)店門酷誓,熙熙樓的掌柜王于貴愁眉苦臉地迎上來披坏,“玉大人,你說我怎么就攤上這事盐数」蚊龋” “怎么了?”我有些...
    開封第一講書人閱讀 168,083評論 0 360
  • 文/不壞的土叔 我叫張陵娘扩,是天一觀的道長着茸。 經(jīng)常有香客問我,道長琐旁,這世上最難降的妖魔是什么涮阔? 我笑而不...
    開封第一講書人閱讀 59,640評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮灰殴,結(jié)果婚禮上敬特,老公的妹妹穿的比我還像新娘。我一直安慰自己牺陶,他們只是感情好伟阔,可當(dāng)我...
    茶點故事閱讀 68,640評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著掰伸,像睡著了一般皱炉。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上狮鸭,一...
    開封第一講書人閱讀 52,262評論 1 308
  • 那天合搅,我揣著相機(jī)與錄音,去河邊找鬼歧蕉。 笑死灾部,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的惯退。 我是一名探鬼主播赌髓,決...
    沈念sama閱讀 40,833評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼催跪!你這毒婦竟也來了锁蠕?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,736評論 0 276
  • 序言:老撾萬榮一對情侶失蹤叠荠,失蹤者是張志新(化名)和其女友劉穎匿沛,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體榛鼎,經(jīng)...
    沈念sama閱讀 46,280評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡逃呼,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,369評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了者娱。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片抡笼。...
    茶點故事閱讀 40,503評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖黄鳍,靈堂內(nèi)的尸體忽然破棺而出推姻,到底是詐尸還是另有隱情,我是刑警寧澤框沟,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布藏古,位于F島的核電站增炭,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏拧晕。R本人自食惡果不足惜隙姿,卻給世界環(huán)境...
    茶點故事閱讀 41,870評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望厂捞。 院中可真熱鬧输玷,春花似錦、人聲如沸靡馁。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽臭墨。三九已至赔嚎,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間裙犹,已是汗流浹背尽狠。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留叶圃,地道東北人袄膏。 一個月前我還...
    沈念sama閱讀 48,909評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像掺冠,于是被迫代替她去往敵國和親沉馆。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,512評論 2 359

推薦閱讀更多精彩內(nèi)容