? 已經(jīng)讀到《深入理解java虛擬機(jī)》第三部分了,感覺開始飄了婚陪,太枯燥了這部分族沃,不過還是跟著書上走了一遍,大概了解了其內(nèi)容泌参,這部分內(nèi)容主要類文件結(jié)構(gòu)脆淹,類加載機(jī)制,執(zhí)行引擎等組成沽一。本次筆記主要記錄類加載過程盖溺。
? 虛擬機(jī)把描述類的數(shù)據(jù)從Class文件加載到內(nèi)存,并對(duì)數(shù)據(jù)進(jìn)行校驗(yàn)铣缠、轉(zhuǎn)換解析烘嘱、初始化,最終形成可以直接被Java虛擬機(jī)使用的Java類型蝗蛙,這就是虛擬機(jī)的類加載機(jī)制蝇庭。
1丘损、類加載步驟
? 類從被加載到內(nèi)存到使用完成被卸載出內(nèi)存及老,需要經(jīng)歷加載爆惧、連接呻引、初始化沙廉、使用产徊、卸載這幾個(gè)過程员串,其中連接又可以細(xì)分為驗(yàn)證厚宰、準(zhǔn)備泰涂、解析鲫竞。
(1)加載
? 在加載階段,虛擬機(jī)主要完成三件事情:
① 通過一個(gè)類的全限定名(比如com.danny.framework.t)來獲取定義該類的二進(jìn)制流逼蒙;
② 將這個(gè)字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)存儲(chǔ)結(jié)構(gòu)从绘;
③ 在內(nèi)存中生成一個(gè)代表這個(gè)類的java.lang.Class對(duì)象,作為程序訪問方法區(qū)中這個(gè)類的外部接口。
(2)驗(yàn)證
? 驗(yàn)證的目的是為了確保class文件的字節(jié)流包含的內(nèi)容符合虛擬機(jī)的要求僵井,且不會(huì)危害虛擬機(jī)的安全陕截。從整體上看,驗(yàn)證階段大致會(huì)完成下面四個(gè)階段的檢驗(yàn)動(dòng)作:
①文件格式驗(yàn)證:主要驗(yàn)證class文件中二進(jìn)制字節(jié)流的格式批什,比如魔數(shù)是否已0xCAFEBABY開頭农曲、版本號(hào)是否正確等。
②元數(shù)據(jù)驗(yàn)證:主要對(duì)字節(jié)碼描述的信息進(jìn)行語義分析驻债,保證其符合Java語言規(guī)范乳规,比如驗(yàn)證這個(gè)類是否有父類(java.lang.Object除外),如果這個(gè)類不是抽象類合呐,是否實(shí)現(xiàn)了父類或接口中沒有實(shí)現(xiàn)的方法暮的,等等。
③字節(jié)碼驗(yàn)證:字節(jié)碼驗(yàn)證更為高級(jí)淌实,通過數(shù)據(jù)流和控制流分析冻辩,確保程序是合法的、符合邏輯的拆祈。
④符號(hào)引用驗(yàn)證:對(duì)類自身以外的信息進(jìn)行匹配性校驗(yàn)恨闪,舉個(gè)栗子,比如通過類的全限定名能否找到對(duì)應(yīng)類放坏、在類中能否找到字段名/方法名對(duì)應(yīng)的字段/方法咙咽,如果符號(hào)引用驗(yàn)證失敗,將拋出“java.lang.NoSuchFieldError”轻姿、“java.lang.NoSuchMethodError”等異常犁珠。
(3)準(zhǔn)備
? 正式為類變量分配內(nèi)存并設(shè)置類變量初始值,這些變量所使用的內(nèi)存都分配在方法區(qū)互亮。注意分配內(nèi)存的對(duì)象是“類變量”而不是實(shí)例變量犁享,而且為其分配的是“初始值”,一般數(shù)值類型的初始值都為0豹休,char類型的初始值為’\u0000’(常量池中一個(gè)表示Nul的字符串)炊昆,boolean類型初始值為false,引用類型初始值為null威根。
? 但是加上final關(guān)鍵字比如public static final int value=998;在準(zhǔn)備階段會(huì)初始化value的值為998凤巨;
(4)解析
? 解析是將常量池中符號(hào)引用替換為直接引用的過程。
? 符號(hào)引用是以一組符號(hào)來描述所引用的目標(biāo)洛搀,符號(hào)引用與虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局無關(guān)敢茁,引用的目標(biāo)不一定已經(jīng)加載到內(nèi)存中。
? 直接引用可以是直接指向目標(biāo)的指針留美、相對(duì)偏移量或是一個(gè)能間接定位到目標(biāo)的句柄彰檬。直接引用和虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局有關(guān)伸刃,如果有了直接引用,那引用的目標(biāo)一定在內(nèi)存中存在逢倍。
? 解析的時(shí)候class已經(jīng)被加載到方法區(qū)的內(nèi)存中捧颅,因此要把符號(hào)引用轉(zhuǎn)化為直接引用,也就是能直接找到該類實(shí)際內(nèi)存地址的引用较雕。
(5)初始化
? 類初始化階段是類加載階段的最后一步碉哑,前面的類加載過程中,除了在加載階段用戶應(yīng)用程序可以通過自定義類加載器參與之外亮蒋,其余動(dòng)作完全由虛擬機(jī)主導(dǎo)和控制扣典。到了初始化階段才真正執(zhí)行類中定義的Java程序代碼(或者說字節(jié)碼)。
? 在準(zhǔn)備階段慎玖,變量已經(jīng)賦過一次系統(tǒng)要求的初始值激捏,而在初始化階段則根據(jù)程序員通過程序制定的主觀計(jì)劃去初始化類變量和其他資源,或者可以從另外一個(gè)角度表達(dá):初始化階段是執(zhí)行類構(gòu)造器<clinit>()方法的過程凄吏。<clinit>()方法是由編譯器自動(dòng)收集類中所有類變量的賦值動(dòng)作和靜態(tài)語句塊(static{})中的語句合并產(chǎn)生的,編譯器收集的順序是由語句在源文件中出現(xiàn)的順序決定的闰蛔,靜態(tài)語句塊中只能訪問到定義在靜態(tài)語句塊之前的變量痕钢,定義在它之后的變量,在前面的靜態(tài)語句塊可以賦值序六,但是不能訪問.
? 下面看段代碼來理解下:
? 運(yùn)行結(jié)果如下:
? 上面的例子中可以看到一個(gè)類從加載到實(shí)例化的過程中任连,靜態(tài)代碼塊、構(gòu)造方法例诀、非靜態(tài)代碼塊的加載順序随抠。
? 虛擬機(jī)會(huì)保證一個(gè)類的<clinit>()方法在多線程環(huán)境下被正確的加鎖、同步繁涂,如果多個(gè)線程同時(shí)去初始化一個(gè)類拱她,那么只會(huì)有一個(gè)線程去執(zhí)行這個(gè)類的<clinit>()方法,其他線程都需要阻塞等待扔罪,知道活動(dòng)線程執(zhí)行<clinit>()方法完畢秉沼。如果在一個(gè)類的<clinit>()方法中有耗時(shí)很長(zhǎng)的操作,就可能造成多個(gè)線程阻塞矿酵,在實(shí)際應(yīng)用中這種阻塞往往是很隱蔽的唬复。下面代碼演示了這種場(chǎng)景。
? 運(yùn)行結(jié)果如下全肮,即一條線程在死循環(huán)以模擬長(zhǎng)時(shí)間操作敞咧,另外一條線程在阻塞等待。
2辜腺、類加載器
2.1 類與類加載器
? 虛擬機(jī)設(shè)計(jì)團(tuán)隊(duì)把類加載階段中的“通過一個(gè)類的全限定名來獲取描述此類的二進(jìn)制字節(jié)流”這個(gè)動(dòng)作放到虛擬機(jī)外部去實(shí)現(xiàn)休建,以便讓應(yīng)用程序自己決定如何去獲取所需要的類乍恐,實(shí)現(xiàn)這個(gè)動(dòng)作的代碼模塊稱為“類加載器”。類加載器除了有加載類的作用丰包,還有一個(gè)舉足輕重的作用禁熏,對(duì)于每一個(gè)類,都需要由加載它的加載器和這個(gè)類本身共同確立這個(gè)類在Java虛擬機(jī)中的唯一性邑彪。也就是說瞧毙,兩個(gè)相同的類,只有是在同一個(gè)加載器加載的情況下才“相等”寄症,這里的“相等”是指代表類的Class對(duì)象的equals()方法宙彪、isAssignableFrom()方法、isInstance()方法的返回結(jié)果有巧,也包括instanceof關(guān)鍵字對(duì)對(duì)象所屬關(guān)系的判定結(jié)果释漆。下面是演示代碼:
? 運(yùn)行結(jié)果:
? 可以看到 ,這個(gè)對(duì)象與類test.Test做所屬類型檢查的時(shí)候卻返回了false篮迎,這是因?yàn)樘摂M機(jī)中存在了兩個(gè)Test類男图,一個(gè)是由系統(tǒng)應(yīng)用程序類加載的,另一個(gè)是由我們自定義的類加載器加載的甜橱,雖然都來自同一個(gè)Class文件逊笆,但依然是兩個(gè)獨(dú)立的類,做對(duì)象所屬類型檢查時(shí)結(jié)果自然為false岂傲。
2.2 雙親委派模型
? 從虛擬機(jī)的角度來講难裆,只存在兩種不同的類加載器:一種是啟動(dòng)類加載器(Bootstrap ClassLoader),這個(gè)類加載器使用C++語言實(shí)現(xiàn)镊掖,是虛擬機(jī)自身的一部分乃戈;另一種就是所有其他的類加載器,這些類加載器都是由Java語言實(shí)現(xiàn)亩进,獨(dú)立于虛擬機(jī)外部症虑,并且全都繼承自抽象類java.lang.ClassLoader。但是從java開發(fā)人員的角度來看归薛,類加載器還可以劃分的更細(xì)一些侦讨。
① 啟動(dòng)類加載器(Bootstrap ClassLoader)是由C/C++編譯而來的,主要負(fù)責(zé)加載JAVA_HOME\lib目錄或者被-Xbootclasspath參數(shù)指定目錄中的部分類苟翻,具體加載哪些類可以通過“System.getProperty(“sun.boot.class.path”)”來查看韵卤。
② 擴(kuò)展類加載器(Extension ClassLoader)由sun.misc.Launcher.ExtClassLoader實(shí)現(xiàn),負(fù)責(zé)加載JAVA_HOME\lib\ext目錄或者被java.ext.dirs系統(tǒng)變量指定的路徑中的所有類庫(kù)崇猫,可以用通過“System.getProperty(“java.ext.dirs”)”來查看具體都加載哪些類沈条。
③ 應(yīng)用程序類加載器(Application ClassLoader)由sun.misc.Launcher.AppClassLoader實(shí)現(xiàn),負(fù)責(zé)加載用戶類路徑(我們通常指定的classpath)上的類诅炉,如果程序中沒有自定義類加載器蜡歹,應(yīng)用程序類加載器就是程序默認(rèn)的類加載器屋厘。
? 應(yīng)用程序都是由這3種類加載相互配合進(jìn)行加載的,如果有必要月而,還可以加入自定義的類加載器汗洒。
? 類加載器之間的層次關(guān)系,稱為類加載器的雙親委派模型(Parents Delegation Model)父款。雙親委派模型要求除了頂層的啟動(dòng)類加載器外溢谤,其余的類加載器都應(yīng)有自己的父類加載器。這里類加載器之間的父子關(guān)系一般不會(huì)以繼承的關(guān)系來實(shí)現(xiàn)憨攒,而是都使用組合關(guān)系來復(fù)用父類加載器的代碼世杀。
? 雙親委派模型的工作方式是:如果一個(gè)類加載器收到了子類加載的請(qǐng)求,它首先不會(huì)自己去嘗試加載這個(gè)類肝集,而是把這個(gè)請(qǐng)求委派給父類加載器去完成瞻坝,每一個(gè)層次的類加載器都是如此,因此所有的加載請(qǐng)求最后都應(yīng)該傳送到頂層的啟動(dòng)類加載器中杏瞻,只有當(dāng)父類加載器反饋?zhàn)约簾o法完成這個(gè)加載請(qǐng)求(它的搜索范圍沒有找到所需的類)時(shí)所刀,子類加載器才會(huì)嘗試去自己加載。
? 再來看它的實(shí)現(xiàn)代碼捞挥,實(shí)現(xiàn)雙親委派的代碼都集中在java.lang.ClassLoader的loadClass()方法之中勉痴,如下面代碼所示:
? 這段代碼的主要意思就是先檢查是否已經(jīng)被加載過,若沒有加載則調(diào)用父類加載器的loadClass()方法树肃,若父類加載器為空則默認(rèn)使用啟動(dòng)類加載器作為父加載器。如果父類加載器加載失敗瀑罗,拋出ClassNotFoundException異常后胸嘴,再調(diào)用自己的findClass()方法進(jìn)行加載。