虛擬機(jī)把描述類的數(shù)據(jù)從Class文件加載到內(nèi)存,并對數(shù)據(jù)進(jìn)行校驗(yàn)茫陆、轉(zhuǎn)換解析和初始化,最終形成可以被虛擬機(jī)直接使用的Java類型刃鳄,這就是虛擬機(jī)的類加載機(jī)制盅弛。
在Java里,類型的加載、連接和初始化過程都是在程序運(yùn)行期間完成的挪鹏。
一见秽、基礎(chǔ)知識
類的整個(gè)生命周期:加載、驗(yàn)證讨盒、準(zhǔn)備解取、初始化和卸載這5個(gè)階段的順序是確定的。
解析階段不一定返顺,有可能會在初始化之后開始禀苦,這是為了支持Java的動態(tài)綁定。
加載階段的時(shí)機(jī):Java虛擬機(jī)規(guī)范中沒有強(qiáng)制約束遂鹊,各虛擬機(jī)自己實(shí)現(xiàn)振乏。
初始化階段的時(shí)機(jī)(此時(shí)加載、驗(yàn)證秉扑、準(zhǔn)備已在此之前開始)(有且僅有5種情況):
(1)當(dāng)虛擬機(jī)啟動時(shí)慧邮,用戶需要指定一個(gè)要執(zhí)行的主類(main()方法所在類),虛擬機(jī)會先初始化這個(gè)類舟陆。
(2)當(dāng)初始化一個(gè)類的時(shí)候误澳,如果發(fā)現(xiàn)其父類還沒有進(jìn)行過初始化,則需要先觸發(fā)其父類的初始化秦躯。
(3)遇到new
忆谓、getstatic
、putstatic
或invokestatic
這4條字節(jié)碼指令時(shí)踱承,如果類沒有進(jìn)行過初始化倡缠,則需要先觸發(fā)其初始化。
-- new
:使用new關(guān)鍵字實(shí)例化對象茎活;
-- getstatic
/putstatic
:讀取/設(shè)置一個(gè)類的靜態(tài)字段(被final修飾毡琉、已在編譯期把結(jié)果放入常量池的靜態(tài)字段除外);
-- invokestatic
:調(diào)用一個(gè)類的靜態(tài)方法的時(shí)候妙色。
(4)使用java.lang.reflect包的方法對類進(jìn)行反射調(diào)用的時(shí)候桅滋,如果類沒有進(jìn)行過初始化,則需要先觸發(fā)其初始化身辨。
(5)當(dāng)使用動態(tài)語言支持時(shí)丐谋,如果一個(gè)java.lang.invoke.MethodHandle實(shí)例最后的解析結(jié)果REF_getStatic、REF_putStatic煌珊、REF_invokeStatic的方法句柄所對應(yīng)的類沒有進(jìn)行過初始化号俐,則需要先觸發(fā)其初始化。
接口與類初始化的不同:
接口在初始化的時(shí)候定庵,不要求其父接口全部完成了初始化吏饿,只有在真正使用到父接口時(shí)才會初始化踪危。
二、類加載過程
1. 加載階段
(1)通過一個(gè)類的全限定名來獲取定義此類的二進(jìn)制字節(jié)流猪落。
(2)將這個(gè)字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)贞远。
(3)在內(nèi)存中生成一個(gè)代表這個(gè)類的java.lang.Class對象,作為方法區(qū)這個(gè)類的各種數(shù)據(jù)的訪問入口笨忌。
對于(1)蓝仲,虛擬機(jī)規(guī)范沒有指明要從哪里獲取、怎么獲取二進(jìn)制字節(jié)流官疲,所以:
-- 如果從ZIP包中獲取袱结,就是JAR、EAR途凫、WAR格式垢夹;
-- 如果從網(wǎng)絡(luò)中獲取,典型的是Applet维费;
-- 如果是運(yùn)行時(shí)計(jì)算生成的二進(jìn)制字節(jié)流棚饵,典型的是動態(tài)代理技術(shù)(*$Proxy代理類);
-- 如果由其他文件生成二進(jìn)制字節(jié)流掩完,典型的是JSP;
等等硼砰。
對于(1)且蓬,非數(shù)組類既可以使用系統(tǒng)提供的引導(dǎo)類加載器完成,也可以使用自定義的類加載器完成题翰。
但是數(shù)組類不同恶阴,數(shù)組類本身不通過類加載器創(chuàng)建,它是由Java虛擬機(jī)直接創(chuàng)建的豹障;數(shù)組類的元素類型是通過類加載器創(chuàng)建的冯事。
對于(2),方法區(qū)中的數(shù)據(jù)結(jié)構(gòu)是由虛擬機(jī)自行實(shí)現(xiàn)的(虛擬機(jī)規(guī)范未規(guī)定)血公,虛擬機(jī)獲取到字節(jié)流后按照虛擬機(jī)所需要的格式存儲在方法區(qū)中昵仅。
對于(3),當(dāng)字節(jié)流按照格式存儲到方法區(qū)中后累魔,會在內(nèi)存中實(shí)例化一個(gè)java.lang.Class類的對象(注意:并沒有明確規(guī)定是在Java堆中摔笤,這和一般對象在堆內(nèi)存產(chǎn)生不同,在HotSpot中垦写,Class對象存放在方法區(qū)中吕世,盡管它是一個(gè)對象),這個(gè)Class對象將作為程序訪問方法區(qū)中這個(gè)類的各種數(shù)據(jù)的外部接口梯投。
加載階段和連接階段的部分內(nèi)容(如一部分字節(jié)碼文件格式驗(yàn)證)是交叉進(jìn)行的命辖。加載階段還未完成况毅,連接階段可能已經(jīng)開始。
2. 驗(yàn)證階段
驗(yàn)證階段是為了確保Class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求尔艇,并且不會危害虛擬機(jī)自身的安全尔许。
Java語言本身是相對安全的語言,但是Class文件不一定由Java源碼編譯而來漓帚,所以必須對Class文件字節(jié)流進(jìn)行驗(yàn)證母债。
文件格式驗(yàn)證
第一階段驗(yàn)證字節(jié)流是否符合Class文件格式的規(guī)范,并且能被當(dāng)前版本的虛擬機(jī)處理尝抖。
(1)是否以魔數(shù)0xCAFEBABE
開頭毡们;
(2)主次版本號是否在當(dāng)前虛擬機(jī)處理范圍內(nèi);
(3)常量池的常量中是否有不被支持的常量類型昧辽;
(4)指向常量的各種索引值中是否有指向不存在的常量或不符合類型的常量衙熔;
(5)CONSTANT_Utf8_info型的常量中是否有不符合UTF8編碼的數(shù)據(jù);
等等搅荞。
元數(shù)據(jù)驗(yàn)證
第二階段對字節(jié)碼描述的信息(類的元數(shù)據(jù)信息)進(jìn)行語義分析红氯,以保證其描述的信息符合Java語言規(guī)范要求。
(1)這個(gè)類是否有父類(java.lang.Object除外)咕痛;
(2)這個(gè)類的父類是否繼承了不允許被繼承的類(被final修飾的類)痢甘;
(3)如果這個(gè)類不是抽象類,是否實(shí)現(xiàn)了其父類或接口中要求實(shí)現(xiàn)的所有方法茉贡;
(4)類中的字段塞栅、方法是否與父類產(chǎn)生矛盾;
等等腔丧。
字節(jié)碼驗(yàn)證
第三階段對類的方法體進(jìn)行檢驗(yàn)放椰,通過數(shù)據(jù)流和控制流分析,確定程序語義是否合法愉粤、符合邏輯砾医。
(1)保證任意時(shí)刻操作數(shù)棧的數(shù)據(jù)類型與指令代碼序列能配合工作;
(2)保證跳轉(zhuǎn)指令不會跳轉(zhuǎn)到方法體以外的字節(jié)碼指令上衣厘;
(3)保證方法體中的類型轉(zhuǎn)換是有效的如蚜;
等等。
符號引用驗(yàn)證
第四階段對類自身以外(常量池中的各種符號引用)的信息進(jìn)行匹配性校驗(yàn)影暴,此驗(yàn)證發(fā)生在解析階段(將符號引用轉(zhuǎn)化為直接引用)時(shí)怖亭,以確保解析動作正常執(zhí)行。
(1)符號引用中通過字符串描述的全限定名是否能找到對應(yīng)的類坤检;
(2)在指定類中是否存在符合方法的字段描述符兴猩;
(3)符號引用中的類、字段早歇、方法的訪問性是否可被當(dāng)前類訪問倾芝;
等等讨勤。
可以使用-Xverify:none
參數(shù)來關(guān)閉大部分的類驗(yàn)證措施,縮短虛擬機(jī)類加載時(shí)間晨另。
3. 準(zhǔn)備階段
為類變量(被static修飾)分配內(nèi)存并設(shè)置初始值潭千,這些類變量所使用的內(nèi)存都在方法區(qū)中進(jìn)行分配。
通常情況下借尿,類變量被設(shè)置的初始值都是零值刨晴,因?yàn)槌绦虮痪幾g后,對類變量進(jìn)行賦值的操作putstatic指令
存放在類構(gòu)造器<clinit>()方法中路翻,因此類變量真正賦值在初始化階段執(zhí)行狈癞,準(zhǔn)備階段只設(shè)置零值。
但是如果類變量被final
修飾茂契,那么類字段的字段屬性表中存在ConstantValue屬性蝶桶,準(zhǔn)備階段會將ConstantValue屬性的值直接賦給類變量,而不是零值掉冶。
4. 解析階段
解析階段虛擬機(jī)將常量池內(nèi)的符號引用轉(zhuǎn)化為直接引用真竖。
符號引用:
符號引用是Class文件中常量池的常量(CONSTANT_Class_info、CONSTANT_Fieldref_info厌小、CONSTANT_Methodref_info等)恢共。
各種虛擬機(jī)能接受的符號引用必須是一致的(因?yàn)榉栆玫淖置媪啃问矫鞔_定義在Java虛擬機(jī)規(guī)范的Class文件格式中),但是各虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局和符號引用無關(guān)璧亚,且引用的目標(biāo)不一定已經(jīng)加載到內(nèi)存了讨韭。
直接引用:
直接引用可以是直接指向目標(biāo)的指針、相對偏移量或能間接定位到目標(biāo)的句柄涨岁。
各虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局與直接引用相關(guān),同一符號引用在各虛擬機(jī)上翻譯出來的直接引用一般不同吉嚣,直接引用的目標(biāo)必定已經(jīng)存在于內(nèi)存中了梢薪。
類或接口的解析
假設(shè)當(dāng)前代碼所處的類為D,要把一個(gè)未解析過的符號引用N解析為類或接口C的直接引用尝哆,過程如下:
(1)如果C不是一個(gè)數(shù)組秉撇,那么虛擬機(jī)會把代表N的全限定名傳遞給D的類加載器去加載這個(gè)類C。加載過程中秋泄,由于元數(shù)據(jù)驗(yàn)證琐馆、字節(jié)碼驗(yàn)證的需要,可能觸發(fā)其他相關(guān)類的加載動作恒序。
(2)如果C是一個(gè)數(shù)組且數(shù)組的元素類型是對象([Ljava/lang/Integer
)瘦麸,那么按照1中規(guī)則加載數(shù)組的元素類型(java.lang.Integer
),然后由虛擬機(jī)生成一個(gè)代表此數(shù)組維度和元素的數(shù)組對象歧胁。
(3)如果上面兩步?jīng)]有出現(xiàn)任何異常滋饲,則C已經(jīng)被加載到虛擬機(jī)中了厉碟,但是在解析完成前還需要進(jìn)行符號引用驗(yàn)證(驗(yàn)證階段的第四階段),確認(rèn)D是否有對C的訪問權(quán)限屠缭,如果不具備則拋出java.lang.IllegalAccessError異常箍鼓。
字段解析
(1)要解析一個(gè)未被解析過的字段符號引用,首先要先解析字段所屬類或接口的符號引用呵曹,即解析字段表對應(yīng)常量池內(nèi)CONSTANT_Fieldref_info的class_index索引的CONSTANT_Class_info符號引用款咖。如果字段所屬類或接口的符號引用解析成功(記為C),然后才能對C進(jìn)行后續(xù)字段的搜索:
(2)如果C本身包含了與簡單名稱奄喂、字段描述符都匹配的字段铐殃,則返回該字段的直接引用,查找結(jié)束砍聊。
(3)如果在C中實(shí)現(xiàn)了接口背稼,將會按照繼承關(guān)系從下往上遞歸搜索各接口、父接口玻蝌,如果找到與簡單名稱蟹肘、字段描述符都匹配的字段,則返回該字段的直接引用俯树,查找結(jié)束帘腹。
(4)如果C不是java.lang.Object的話,將會按照繼承關(guān)系從下往上遞歸搜索其父類许饿,如果找到與簡單名稱阳欲、字段描述符都匹配的字段,則返回該字段的直接引用陋率,查找結(jié)束球化。
(5)如果還沒有找到,則拋出java.lang.NoSuchFieldError異常瓦糟。
(6)如果查找成功筒愚,將對這個(gè)返回字段進(jìn)行權(quán)限驗(yàn)證,如果不具備對字段的訪問權(quán)限菩浙,則拋出java.lang.IllegalAccessError異常巢掺。
類方法解析
(1)要解析一個(gè)未被解析過的類方法符號引用,首先要先解析類方法所屬類或接口的符號引用劲蜻,即解析類方法表對應(yīng)常量池內(nèi)CONSTANT_Methodref_info的class_index索引的CONSTANT_Class_info符號引用陆淀。由于類方法符號引用(CONSTANT_Methodref_info)和接口方法符號引用(CONSTANT_InterfaceMethodref_info)是不同的,因此類方法解析出來屬于類才算成功(記為C)先嬉,否則直接拋出java.lang.IncompatibleClassChangeError異常轧苫。
(2)在類C中查找是否有簡單名稱和描述符都匹配的方法,有則返回該方法的直接引用疫蔓,查找結(jié)束浸剩。
(3)在類C的父類中遞歸查找是否有簡單名稱和描述符都匹配的方法钾军,有則返回該方法的直接引用,查找結(jié)束绢要。
(4)在類C的接口吏恭、父接口中遞歸查找是否有簡單名稱和描述符都匹配的方法,有則說明類C是個(gè)抽象類重罪,拋出java.lang.AbstractMethodError樱哼,查找結(jié)束。
(5)還未找到剿配,拋出java.lang.NoSuchMethodError搅幅,查找失敗。
(6)如果查找成功呼胚,將對這個(gè)返回方法進(jìn)行權(quán)限驗(yàn)證茄唐,如果不具備對方法的訪問權(quán)限,則拋出java.lang.IllegalAccessError異常蝇更。
接口方法解析
(1)要解析一個(gè)未被解析過的接口方法符號引用沪编,首先要先解析接口方法所屬類或接口的符號引用,即解析接口方法表對應(yīng)常量池內(nèi)CONSTANT_InterfaceMethodref_info的class_index索引的CONSTANT_Class_info符號引用年扩。由于類方法符號引用(CONSTANT_Methodref_info)和接口方法符號引用(CONSTANT_InterfaceMethodref_info)是不同的蚁廓,因此接口方法解析出來屬于接口才算成功(記為C),否則直接拋出java.lang.IncompatibleClassChangeError異常厨幻。
(2)在接口C中查找是否有簡單名稱和描述符都匹配的方法相嵌,有則返回該方法的直接引用,查找結(jié)束况脆。
(3)在接口C的父接口中遞歸查找直到j(luò)ava.lang.Object類為止饭宾,看是否有簡單名稱和描述符都匹配的方法,有則返回該方法的直接引用格了,查找結(jié)束看铆。
(4)還未找到,拋出java.lang.NoSuchMethodError笆搓,查找失敗性湿。
(5)由于接口中所有方法默認(rèn)都是public的纬傲,所以不需要對查找成功后返回的方法進(jìn)行權(quán)限驗(yàn)證满败,也不會拋出java.lang.IllegalAccessError異常。
5. 初始化階段
除了加載階段用戶應(yīng)用程序可以通過自定義類加載器參與之外叹括,其余的各階段動作均由虛擬機(jī)控制算墨。
到了初始化階段,才真正開始執(zhí)行類中定義的Java程序代碼(javac編譯而成的字節(jié)碼)汁雷。
初始化階段執(zhí)行類構(gòu)造器<clinit>()方法净嘀。
(1)<clinit>()方法由編譯器自動收集類中的所有類變量的賦值動作和靜態(tài)語句塊(static{})中的語句合并產(chǎn)生报咳。編譯器收集的順序由語句在源文件中出現(xiàn)的順序決定,定義在靜態(tài)語句塊前的變量挖藏,靜態(tài)語句塊中可以訪問暑刃;定義在靜態(tài)語句塊后的變量,前面的靜態(tài)語句塊中可以賦值膜眠,但不能訪問岩臣。
(2)<clinit>()方法與實(shí)例構(gòu)造器<init>()方法不同,不需要顯示的調(diào)用父類構(gòu)造器宵膨,因?yàn)樵谧宇?lt;clinit>()方法之前架谎,父類的<clinit>()方法肯定執(zhí)行完畢了。
(3)<clinit>()方法對于類或接口來說不是必須的辟躏,如果一個(gè)類中沒有靜態(tài)語句塊谷扣,也沒有對類變量的賦值操作,那么編譯器可以不為這個(gè)類生成<clinit>()方法捎琐,注意不是<init>()方法会涎。
(4)接口中不能使用靜態(tài)語句塊,但可以有變量初始化操作野哭,因此接口也會生成<clinit>()方法在塔,但是接口的<clinit>()方法執(zhí)行的時(shí)候不需要先執(zhí)行父接口的<clinit>()方法。此外拨黔,接口的實(shí)現(xiàn)類在初始化時(shí)也不會執(zhí)行接口的<clinit>()蛔溃。
(5)虛擬機(jī)會保證一個(gè)類的<clinit>()方法在多線程環(huán)境下被正確的加鎖、同步篱蝇,只會有一個(gè)線程去執(zhí)行這個(gè)類的<clinit>()方法贺待,其他線程都需要阻塞等待。
三零截、類加載器
類加載器用于實(shí)現(xiàn)類加載階段中的“通過一個(gè)類的全限定名來獲取描述此類的二進(jìn)制字節(jié)流”這個(gè)動作麸塞。
對于任意一個(gè)類,都需要由加載它的類加載器和類本身一同確立其在Java虛擬機(jī)中的唯一性涧衙,每個(gè)類加載器哪工,都擁有一個(gè)獨(dú)立的類名稱空間。
1. 類加載器分類
從Java虛擬機(jī)角度來看弧哎,類加載器分為兩類:
- 啟動類加載器(Bootstrap ClassLoader):虛擬機(jī)自身的一部分雁比。
- 其他類加載器:獨(dú)立于虛擬機(jī)外部,均繼承自抽象類java.lang.ClassLoader撤嫩。
從Java開發(fā)人員角度來看偎捎,類加載器分為三類:
(1)啟動類加載器
負(fù)責(zé)將存放在<JAVA_HOME>\lib目錄中的,或被-Xbootclasspath參數(shù)所指定的路徑中的,且被虛擬機(jī)識別的類庫加載到虛擬機(jī)內(nèi)存中茴她。啟動類加載器無法被Java程序直接引用寻拂,但是可以委派。
(2)擴(kuò)展類加載器
負(fù)責(zé)加載<JAVA_HOME>\lib\ext目錄中的丈牢,或被java.ext.dirs系統(tǒng)變量指定的路徑中的所有類庫祭钉,開發(fā)者可直接使用擴(kuò)展類加載器。
(3)應(yīng)用程序類加載器
這個(gè)類加載器是ClassLoader.getSystemClassLoader()
方法的返回值己沛,它負(fù)責(zé)加載用戶類路徑上所指定的類庫朴皆,開發(fā)者可直接使用這個(gè)類加載器,如果應(yīng)用程序中沒有自定義類加載器泛粹,默認(rèn)使用的就是這個(gè)類加載器遂铡。
2. 雙親委派模型
類加載器之間的層次關(guān)系,被稱為類加載器的雙親委派模型(Parents Delegation Model)晶姊。
工作過程:
(1)如果一個(gè)類加載器收到了類加載請求扒接,它會把這個(gè)請求委派給父類加載器去完成,每一層的類加載器都是如此们衙,直至啟動類加載器钾怔。
(2)當(dāng)父加載器反饋?zhàn)约簾o法完成這個(gè)加載請求(根據(jù)類全限定名無法在自己的搜索范圍內(nèi)找到所需的類)時(shí),子加載器才會嘗試自己去加載蒙挑。
雙親委派模型好處:
由于類的唯一性需要通過加載該類的類加載器和類本身這兩個(gè)因素確定宗侦,因此當(dāng)類加載器具有優(yōu)先級關(guān)系時(shí),類也就具有了優(yōu)先級關(guān)系(對于同一個(gè)類而言忆蚀,父類加載器加載的類的優(yōu)先級更高)矾利,這樣可以保證Java程序的穩(wěn)定運(yùn)行。
3. 破壞雙親委派模型
Java中大部分情況下馋袜,類加載器都遵循雙親委派模型男旗,但也有例外。
- 第一次破壞:雙親委派模型出現(xiàn)之前(JDK 1.2發(fā)布之前)
- 第二次破壞:Java中所有涉及SPI(Service Provider Interface)的加載動作基本上都是使用線程上下文類加載器實(shí)現(xiàn)的欣鳖,例如JNDI察皇、JDBC等。利用線程上下文類加載器去加載SPI代碼其實(shí)是父類加載器請求子類加載器去完成類加載動作泽台,破壞了雙親委派模型什荣。
- 第三次破壞:代碼熱替換、模塊熱部署怀酷。OSGi可實(shí)現(xiàn)模塊化熱部署稻爬,每個(gè)模塊(Bundle)都有一個(gè)自己的類加載器,當(dāng)需要更換一個(gè)Bundle時(shí)胰坟,會把該Bundle連同類加載器一起換掉以實(shí)現(xiàn)代碼的熱替換因篇。在OSGi環(huán)境下,類加載器不再是雙親委派模型中的樹狀結(jié)構(gòu)笔横,而是更為復(fù)雜的網(wǎng)狀結(jié)構(gòu)竞滓。