虛擬機類加載機制 深入理解Java虛擬機總結(jié)

????????虛擬機把描述類的數(shù)據(jù)從Class文件加載到內(nèi)存熙宇, 并對數(shù)據(jù)進行校驗围段、轉(zhuǎn)換解析和初始化亏栈, 最終形成可以被虛擬機直接使用的Java類型台腥, 這就是虛擬機的類加載機制。

????????與那些在編譯時需要進行連接工作的語言不同绒北, 在Java語言里而黎侈, 類型的加載、連接和初始化過程都是在程序運行期間完成的闷游, 這種策略雖然會令類加載時稍微增加一些性能開銷峻汉, 但是會為Java應(yīng)用程序提供高度的靈活性, Java里天生可以動態(tài)擴展的語言特性就是依賴運行期動態(tài)加載和動態(tài)連接這個特點實現(xiàn)的脐往。

類加載的時機

????????從類被加載到虛擬機內(nèi)存中開始休吠,到卸載出內(nèi)存為止,類的生命周期包括加載(Loading)业簿、驗證(Verification)瘤礁、準備(Preparation)、解析(Resolution)梅尤、初始化(Initialization)柜思、使用(Using)和卸載(Unloading)7個階段,其中驗證巷燥、準備和解析三部分稱為連接赡盘。

????????加載、 驗證缰揪、 準備陨享、 初始化和卸載這 5 個階段的順序是確定的, 類的加載過程必須按照這種順序按部就班地開始, 而解析階段則不一定: 它在某些情況下可以在初始化階段之后再開始霉咨, 這是為了支持 Java 語言的運行時綁定(也稱為動態(tài)綁定或晚期綁定)。 注意拍屑, 這里筆者寫的是按部就班地 “ 開始”途戒,而不是按部就班地 ” 進行 ” 或 “ 完成“,強調(diào)這點是因為這些階段通常都是互相交叉地混合式進行的僵驰,通常會在一個階段執(zhí)行的過程中調(diào)用喷斋、 激活另外一個階段。

? ??????什么情況下需要開始類加載過程的第一個階段: 加載? Java 虛擬機規(guī)范中并沒有進行強制約束蒜茴, 這點可以交給虛擬機的具體實現(xiàn)來自由把握星爪。 但是對于初始化階段, 虛擬機規(guī)范則 是嚴格規(guī)定了有且只有 5 種情況必須立即對類進行 “初始化 ” (而加載粉私、 驗證顽腾、 準備自然需 要在此之前開始):

1) 遇到new、getstatic 诺核、putstatic 或invokestatic 這4 條字節(jié)碼指令時抄肖, 如果類沒有進行過初始化. 則需要先觸發(fā)其初始化。生成這4 條指令的最常見的Java 代碼場景是:使用new關(guān)鍵字實例化對象的時候窖杀、讀取或設(shè)置一個類的靜態(tài)字段(被_final 修飾漓摩、已在編譯期把結(jié)果放入常量池的靜態(tài)字段除外)的時候, 以及調(diào)用一個類的靜態(tài)方法的時候入客。

2) 使用java.lang.reflect 包的方法對類進行反射調(diào)用的時候管毙, 如果類沒有進行過初始化,則需要先觸發(fā)其初始化桌硫。

3) 當初始化一個個類的時候夭咬, 如果發(fā)現(xiàn)其父類還沒有進行過初始化, 則需要先觸發(fā)其父類的初始化铆隘。

4) 當虛擬機啟動時皱埠, 用戶需要指定一個要執(zhí)行的主類(包含main() 方法的那個類), 虛擬機會先初始化這個主類咖驮。

5)當使用JDK l.7的動態(tài)語言支持時边器, 如果一個java.lang. invoke .Method Handle 實例最后的解析結(jié)果REF_getStatic、REF_putStatic托修、REF_invokeStatic 的方法句柄忘巧, 并且這個方法句柄所對應(yīng)的類沒有進行過初始化, 則需要先觸發(fā)其初始化睦刃。

? ??????這5 種場景中的行為稱為對一個類進行主動引用砚嘴。除此之外, 所有引用類的方式都不會觸發(fā)初始化, 稱為被動引用际长。


類加載的過程

????????就是加載耸采、 驗證、 準備 工育、 解 析和初始化這5個階段所執(zhí)行的具體動作虾宇。

加載

????????“加載 ” 是 “類加載" ( Class Loading) 過程的一個階段,在加載階段如绸, 虛擬機需要完成以下3件事情:

1)通過一個類的全限定名來獲取定義此類的二進制字節(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ù)組類的加載階段(準確地說扼脐,是加載階段中獲取類的二進制字節(jié)流的動作)是開發(fā)人員可控性最強的岸军, 因為加載階段既可以使用系統(tǒng)提供的引導(dǎo)類加載器來完成, 也可以由用戶自定義的類加載器去完成瓦侮, 開發(fā)人員可以通過定義自己的類加載器去控制字節(jié)流的獲取方式(即重寫一個類加載器的 loadClass() 方法)凛膏。對于數(shù)組類而言, 情況就有所不同脏榆, 數(shù)組類本身不通過類加載器創(chuàng)建猖毫, 它是由 Java 虛擬機直接創(chuàng)建的。 但數(shù)組類與類加載器仍然有很密切的關(guān)系须喂, 因為數(shù)組類的元素類型 (Element一個數(shù)組類(下面簡Type, 指的是數(shù)組去掉所有維度的類型)最終是要靠類加載器去創(chuàng)建吁断,一個數(shù)組類(簡稱為C)創(chuàng)建過程就遵循以下規(guī)則:

a.如果數(shù)組的組件類型(Component Type, 指的是數(shù)組去掉一個維度的類型)是引用類型, 那就遞歸采用本節(jié)中定義的加載過程去加載這個組件類型坞生, 數(shù)組C將在加載該組件類型的類加載器的類名稱空間上被標識( 一個類必須與類加載器一起確定唯一性).

b. 如果數(shù)組的組件類型不是引用類型(例如int[] 數(shù)組)仔役, Java 虛擬機將會把數(shù)組C 標記為與引導(dǎo)類加載器關(guān)聯(lián)。

c.數(shù)組類的可見性與它的組件類型的可見性一致是己, 如果組件類型不是引用類型又兵, 那數(shù)組類的可見性將默認為public 。

????????加載階段完成后卒废, 虛擬機外部的二進制字節(jié)流就按照虛擬機所需的格式存儲在方法區(qū)之中沛厨, 方法區(qū)中的數(shù)據(jù)存儲格式由虛擬機實現(xiàn)自行定義, 虛擬機規(guī)范未規(guī)定此區(qū)域的具體數(shù)據(jù)結(jié)構(gòu)摔认。然后在內(nèi)存中實例化一個java.lang.Class 類的對象(并沒有明確規(guī)定是在Java 堆中逆皮,對于HotSpot 虛擬機而言, Class 對象比較特殊参袱, 它雖然是對象电谣, 但是存放在方法區(qū)里面)秽梅,這個對象將作為程序訪問方法區(qū)中的這些類型數(shù)據(jù)的外部接口。

? ? ? ? 加載階段與連接階段的部分內(nèi)容(如一部分字節(jié)碼文件格式驗證動作)是交叉進行的剿牺,加載階段尚未完成企垦,連接階段可能已經(jīng)開始,但是開始階段保持固定順序



驗證

????????驗證是連接階段的第一步晒来,這一階段的目的是為了確保Class文件的字節(jié)流中包含的信息符合當前虛擬機的要求钞诡,并且不會危害虛擬機的自身安全。從整體上看潜索,驗證階段大致上會完成下面4個階段的檢驗動作:

1.文件格式驗證

(1)是否以魔數(shù)0xCAFEBABE開頭臭增。

(2)主懂酱、次版本號是否在當前虛擬機處理范圍之內(nèi)竹习。

(3)常量池的常量中是否有不被支持的常量類型(檢查常量tag標志)。

(4)指向常量的各種索引值中是否有指向不存在的常量或不符合類型的常量列牺。

(5)CONSTANT_Utf8_info型的常量中是否有不符合UTF8編碼的數(shù)據(jù)整陌。

(6)Class文件中各個部分及文件本身是否有被刪除的或附加的其他信息。

........................

????????實際且笫一階段的驗證點還遠不止面這些瞎领,該驗證階段的主要目的是保證輸入的字節(jié)流能正確地解析并存儲于方法區(qū)之內(nèi)泌辫,格式上符合描述一個Java 類型信息的要求。這階段的驗證是基于二進制字節(jié)流進行的九默,只有通過了這個階段的驗證后震放,字節(jié)流才會進入內(nèi)存的方法區(qū)中進行存儲,所以后面的3個驗證階段全部是基于方法區(qū)的存儲結(jié)構(gòu)進行的驼修,不會再直接操作字節(jié)流殿遂。

2.元數(shù)據(jù)驗證

(1)這個類是否有父類(除了java.lang.Object之外,所有類都應(yīng)當有父類)乙各。

(2)這個類是否繼承了不允許被繼承的類(被final修飾的類)墨礁。

(3)如果這個類不是抽象類,是否實現(xiàn)了其父類或接口之中所要求實現(xiàn)的所有方法耳峦。

(4)類中的字段恩静、方法是否與父類產(chǎn)生矛盾(例如覆蓋了父類的final字段,或者出現(xiàn)不符合規(guī)則的方法重載蹲坷,例如方法參數(shù)都一致驶乾,但返回值類型卻不同等等)。

................

第二階段的主要目的是對類的元數(shù)據(jù)信息進行語義校驗循签,保證不存在不符合Java 語言規(guī)范的元數(shù)據(jù)信息轻掩。

3.字節(jié)碼驗證

主要目的是通過數(shù)據(jù)流和控制流分析,確定程序語義是合法的懦底、符合邏輯的唇牧。這個階段將對類的方法體進行校驗分析罕扎,保證被校驗類的方法在運行時不會產(chǎn)生危害虛擬機安全的事件,例如:

(1)保證任意時刻操作數(shù)棧的數(shù)據(jù)類型與指令代碼序列都能配合工作丐重,例如不會出現(xiàn)類似這樣的情況:在操作數(shù)棧放置了一個int類型的數(shù)據(jù)腔召,使用時卻按long類型來加載入本地變量表中。

(2)保證跳轉(zhuǎn)指令不會跳轉(zhuǎn)到方法體以外的字節(jié)碼指令上扮惦。

(3)保證方法體中的類型轉(zhuǎn)換是有效的臀蛛,例如可以把一個子類對象賦值給父類數(shù)據(jù)類型,但是把父類對象賦值給子類數(shù)據(jù)類型崖蜜,甚至把對象賦值給與它毫無繼承關(guān)系浊仆、完全不相干的一個數(shù)據(jù)類型,則是危險不合法的豫领。

......

? ? ? ? 即使通過了抡柿,也不能說是絕對安全。通過程序去校驗程序無法做多絕對準確等恐。

4.符號引用驗證

? ??????符號引用驗證可以看作是類對自身以外(常量池中的各種符號引用)的信息進行匹配性校驗洲劣,通常需要校驗以下內(nèi)容:

(1)符號引用中通過字符串描述的全限定名是否能夠找到對應(yīng)的類。

(2)在指定類中是否存在符合方法的字段描述符以及簡單名稱所描述的方法和字段课蔬。

(3)符號引用中的類囱稽、字段、方法的訪問性(private二跋、protected战惊、public、default)是否可被當前類訪問扎即。

......

? ? ? ? 符號引用驗證的目的是確保解析動作能正常執(zhí)行吞获,如果無法通過符號引用驗證,那么將會拋出一個java.lang.IncompatibleClassChangeError異常的子類铺遂,如java.lang.IllegalAccessError衫哥、java.lang.NoSuchFieldError, java.lang.NoSuchMethodError等。

????????對于虛擬機的類加載機制來說驗證階段是一個非常重要的,但不是一定必要(因為對程序運行期沒有影響)的階段襟锐。如果所運行的全部代碼(包括自己編寫的及第三方包中的代碼)都已經(jīng)被反復(fù)使用和驗證過那么在實施階段就可以考慮使用-Xverify:none參數(shù)來關(guān)閉大部分的類驗證措施撤逢; 以縮短虛擬機類加載的時間。



準備

? ??????準備階段是正式為類變量分配內(nèi)存并設(shè)置類變量初始值的階段粮坞,這些變量所使用的內(nèi)存都將在方法區(qū)中進行分配蚊荣。這個階段中有兩介容易產(chǎn)生混淆的概念需要強調(diào)— 下,首先莫杈,這時候進行內(nèi)存分配的僅包括類變量(被static修飾的變量), 而不包括實例變量互例,實例變量將會在對象實例化時隨著對象一起分配在Java堆中。其次筝闹,這里所說的初始值“ 通常情況” 下是數(shù)據(jù)類型的零值媳叨,假設(shè)一個類變量的定義為: public static? int value = 123; 那變量value在準備階段過后的初始值為0而不是123, 因為這時候尚未開始執(zhí)行任何Java方法,而把value賦值為123的putstatic動作將在初始化階段才會執(zhí)行腥光。


? ??????上面提到,在“ 通常情況” 下初始值是零值糊秆,那相對的會有—些“ 特殊情況” :如果類字段的字段屬性表中存在ConstantValue 屬性武福,那在準備階段變量value就會被初始化為ConstantValue屬性所指定的值, 假設(shè)上而類變員value 的定義變?yōu)椋簆ublic static final int value = 123;編譯時Javac 將會為value生成ConstantValue屬性痘番,在準備階段虛擬機就會根據(jù)ConstantValue的設(shè)置將value 賦值為123?



解析

????????解析階段是虛擬機將常量池內(nèi)的符號引用替換為直接引用的過程捉片,? 在Class 文件中它以CONSTANT_Class_info 、CONSTANT _Fieldref_info 汞舱、CONSTANT_Methodref_info 等類型的常量出現(xiàn)伍纫, 那解析階段中所說的直接引用與符號引用又有什么關(guān)聯(lián)呢?

1)符號引用(Symbolic References): 符號引用以一組符號來描述所引用的目標昂芜, 符號可以是任何形式的字面量莹规, 只要使用時能無歧義地定位到目標即可。符號引用與虛擬機實現(xiàn)的內(nèi)存布局無關(guān)说铃, 引用的目標并不一定巳經(jīng)加載到內(nèi)存中访惜。各種虛擬機實現(xiàn)的內(nèi)存布局可以各不相同嘹履, 但是它們能接受的符號引用必須都是一致的腻扇, 因為符號引用的字面量形式明確定義在Java 虛擬機規(guī)范的Class 文件格式中。

2)直接引用(Direct References) : 直接引用可以是直接指向目標的指針砾嫉、相對偏移量或是一個能間接定位到目標的句柄幼苛。直接引用是和虛擬機實現(xiàn)的內(nèi)存布局相關(guān)的, 同一個符號引用在不同虛擬機實例上翻譯出來的直接引用一般不會相同焕刮。如果有了直接引用舶沿, 那引用的目標必定已經(jīng)在內(nèi)存中存在。

? ??????虛擬機規(guī)范之中并未規(guī)定解析階段發(fā)生的具體時間配并,只要求了在執(zhí)行anewarray括荡、checkcast、getfield溉旋、getstatic畸冲、instanceof、invokedynamic观腊、invokeinterface邑闲、invokespecial、invokestatic 梧油、 invokevirtual苫耸、 Ide ldc_w 、 multianewarray儡陨、 new 褪子、 putfield 和 putstatic 這 16 個用于操作符號引用的字節(jié)碼指令之前量淌, 先對它們所使用的符號引用進行解析。 所以虛擬機實現(xiàn)可以根據(jù)需要來判斷到底是在類被加載器加載時就對常呈池中的符號引用進行解析嫌褪,還是等到一個符號引用將要被使用前才去解析它.

? ??????對同一個符號引用進行多次解析請求是很常見的事情,除invokedynamic指令以外,虛擬機實現(xiàn)可以對第一次解析的結(jié)果進行緩存(在運行時常量池中記錄直接引用,并把常量標識為已解析狀態(tài))從而避免解析動作重復(fù)進行类少。無論是否真正執(zhí)行了多次解析動作,虛擬機需要保證的是在同一個實體中,如果一個符號引用之前已經(jīng)被成功解析過,那么后續(xù)的引用解析請求就應(yīng)當一直成功;同樣的,如果第一次解析失敗了,那么其他指令對這個符號的解析請求也應(yīng)該收到相同的異常。

????????對于invokedynamic指令 ,上面規(guī)則則不成立渔扎。當碰到某個前面已經(jīng)由invokedynamic指令觸發(fā)過解析的符號引用時,并不意味著這個解析結(jié)果對于其他也invokedynamic指令也同樣生效硫狞。因為invokedynamic指令的目的本來就是用于動態(tài)語言支持(目前僅使用Java語言不會生成這條字節(jié)碼指令),它所對應(yīng)的引用稱為“動態(tài)調(diào)用點限定符” ( Dynamic Call Site Specifier ) ,這里“動態(tài)”的含義就是必須等到程序?qū)嶋H運行到這條指令的時候,解析動作才能進行。相對的,其余可觸發(fā)解析的指令都是“靜態(tài)”的?,可以在剛剛完成加載階段,還沒有開始執(zhí)行代碼時就進行解析晃痴。

????????解析動作主要針對類或接口残吩、字段、類方法倘核、接口方法泣侮、方法類型、方法句柄和調(diào)用點限定符7類符號引用進行,分別對應(yīng)于常量池的CONSTANT_Class_info紧唱、 CONSTANT_Fieldref_info活尊、CONSTANT_Methodref_info楔敌、 CONSTANT_InterfaceMethodref_info饲常、CONSTANT_MethodType_info、 CONSTANT_MethodHandle_info和CONSTANT_invokeDynamic_info 7種常量類型熔脂。下面將講解前面4種引用的解析過程,對于后面3種 ,與JDK 1.7新增的動態(tài)語言支持息息相關(guān),由于Java語言是一門靜態(tài)類型的語言绰疤,因此在沒有介紹invokedynamic指令的語義之前,沒有辦法將它們和現(xiàn)在的Java語言對應(yīng)上,筆者將在第8章介紹動態(tài)語言調(diào)用時一起分析講解铜犬。

1.類或接口的解析

? ??????假設(shè)當前代碼所處的類為D,如果要把一個從未解析過的符號引用N解析為一個類或接口C的引用轻庆,那虛擬機完成整個解析過程需要以下3個步驟:

(1)如果C不是一個數(shù)組類型癣猾,那虛擬機將會把代表N的全限定名傳遞給D的類加載器去加載這個類C。

(2)如果C是一個數(shù)組類型余爆,并且數(shù)組的元素類型為對象纷宇,那將會按照第1點的規(guī)則加載數(shù)組元素類型。

(3)如果上面的步驟沒有出現(xiàn)任何異常蛾方,那么C在虛擬機中實際上已經(jīng)成為了一個有效的類或接口了像捶,但在解析完成之前還要進行符號引用驗證,確認D是否具有對C的訪問權(quán)限转捕。如果發(fā)現(xiàn)不具備訪問權(quán)限作岖,則拋出java.lang.IllegalAccessError異常。

2.字段解析

? ??????首先解析字段表內(nèi)class_index項中索引的CONSTANT_Class_info符號引用五芝,也就是字段所屬的類或接口的符號引用痘儡,如果解析完成,將這個字段所屬的類或接口用C表示枢步,虛擬機規(guī)范要求按照如下步驟對C進行后續(xù)字段的搜索沉删。

(1)如果C 本身就包含了簡單名稱和字段描述符都與目標相匹配的字段渐尿,則返回這個字段的直接引用,查找結(jié)束矾瑰。

(2)否則砖茸,如果C中實現(xiàn)了接口,將會按照繼承關(guān)系從下往上遞歸搜索各個接口和它的父接口如果接口中包含了簡單名稱和字段描述符都與目標相匹配的字段殴穴,則返回這個字段的直接引用凉夯,查找結(jié)束。

(3)否則采幌,如果C 不是java.lang.Object的話劲够,將會按照繼承關(guān)系從下往上遞歸搜索其父類,如果在父類中包含了簡單名稱和字段描述符都與目標相匹配的字段休傍,則返回這個字段的直接引用征绎,查找結(jié)束。

(4)否則磨取,查找失敗人柿,拋出java.lang.NoSuchFieldError異常。

????????如果查找過程成功返回了引用忙厌,將會對這個字段進行權(quán)限驗證凫岖,如果發(fā)現(xiàn)不具備對字段的訪問權(quán)限,將拋出java.lang.IllegalAccessError異常慰毅。

????????如果有一個同名字段同時出現(xiàn)在C的接口和父類中隘截,或者同時在自己的父類或多個接口中出現(xiàn)扎阶,那編譯器可能拒絕編譯汹胃,并提示”The field xxx is ambiguous”。

3.類方法解析

????????首先解析類方法表內(nèi)class_index項中索引的CONSTANT_Class_info符號引用东臀,也就是方法所屬的類或接口的符號引用着饥,如果解析完成,將這個類方法所屬的類或接口用C表示惰赋,虛擬機規(guī)范要求按照如下步驟對C進行后續(xù)類方法的搜索宰掉。

(1)類方法和接口方法符號引用的常量類型定義是分開的,如果在類方法表中發(fā)現(xiàn)class_index中索引的C 是個接口赁濒,那就直接拋出java.lang.IncompatibleClassChangeError異常轨奄。

(2)如果通過了第一步,在類C 中查找是否有簡單名稱和描述符都與目標相匹配的方法拒炎,如果有則返回這個方法的直接引用挪拟,查找結(jié)束。

(3)否則击你,在類C的父類中遞歸查找是否有簡單名稱和描述符都與目標相匹配的方法玉组,如果有則返回這個方法的直接引用谎柄,查找結(jié)束。

(4)否則惯雳,在類C實現(xiàn)的接口列表以及他們的父接口中遞歸查找是否有簡單名稱和描述符都與目標相匹配的方法朝巫,如果存在相匹配的方法,說明類C是一個抽象類這時查找結(jié)束石景,拋出java.lang.AbstractMethodError異常劈猿。

(5)否則,宣告方法查找失敗潮孽,拋出java.lang.NoSuchMethodError糙臼。

? ??????最后,如果查找成功返回了直接引用恩商,將會對這個方法進行權(quán)限驗證变逃,如果發(fā)現(xiàn)不具備此方法的訪問權(quán)限,則拋出java.lang.IllegalAccessError異常怠堪。

4.接口方法解析

? ??????首先解析接口方法表內(nèi)class_index項中索引的CONSTANT_Class_info符號引用揽乱,也就是方法所屬的類或接口的符號引用,如果解析完成粟矿,將這個接口方法所屬的接口用C表示凰棉,虛擬機規(guī)范要求按照如下步驟對C進行后續(xù)接口方法的搜索。

(1)與類解析方法不同陌粹,如果在接口方法表中發(fā)現(xiàn)class_index中的索引C是個類而不是個接口撒犀,那就直接拋出java.lang.IncompatibleClassChangeError異常。

(2)否則掏秩,在接口C中查找是否有簡單名稱和描述符都與目標相匹配的方法或舞,如果有則返回這個方法的直接引用,查找結(jié)束蒙幻。

(3)否則映凳,在接口C的父接口中遞歸查找,直到j(luò)ava.lang.Object類(查找范圍包括Object類)為止邮破,看是否有簡單名稱和描述符都與目標相匹配的方法诈豌,如果有則返回這個方法的直接引用,查找結(jié)束抒和。

(4)否則矫渔,宣告方法查找失敗,拋出java.lang.NoSuchMethodError摧莽。

由于接口中所有的方法默認都是public的庙洼,所以不存在訪問權(quán)限的問題,因此接口方法的符號解析應(yīng)當不會拋出java.lang.IllegalAccessError異常。



初始化

????????類初始化階段是類加載過程的最后一步,前面的類加載過程中,除了在加載階段用戶應(yīng)用程序可以通過自定義類加載器參與之外,其余動作完全由虛擬機主導(dǎo)和控制送膳。到了初始化階段,才真正開始執(zhí)行類中定義的Java程序代碼(或者說是字節(jié)碼)员魏。

????????在準備階段,變量已經(jīng)賦過一次系統(tǒng)要求的初始值,而在初始化階段,則根據(jù)程序員通過程序制定的主觀計劃去初始化類變量和其他資源,或者可以從另外一個角度來表達:初始化階段是執(zhí)行類構(gòu)造器<client>()方法的過程。我們在下文會講解<client>()方法是怎么生成的,在這里,我們先看一下<client>()方法執(zhí)行過程中一些可能會影響程序運行行為的特點和細節(jié),這部分相對更貼近于普通的程序開發(fā)人員叠聋。

a.<client>()方法是由編譯器自動收集類中的所有類變量的賦值動作和靜態(tài)語句塊(static{}塊)中的語句合并產(chǎn)生的,編譯器收集的順序是由語句在源文件中出現(xiàn)的順序所決定的 ,靜態(tài)語句塊中只能訪問到定義在靜態(tài)語句塊之前的變量,定義在它之后的變量,在前面的靜態(tài)語句塊可以賦值,但是不能訪問:

b.<client>()方法與類的構(gòu)造函數(shù)(或者說實例構(gòu)造器<init>() 方法)不同,它不需要顯式地調(diào)用父類構(gòu)造器,虛擬機會保證在子類的<client>( ) 方法執(zhí)行之前,父類的<client>()方法已經(jīng)執(zhí)行完畢撕阎。因此在虛擬機中第一個被執(zhí)行的<client>()方法的類肯定是java.lang.Object。

c.由于父類的<client>()方法先執(zhí)行,也就意味著父類中定義的靜態(tài)語句塊要優(yōu)先于子類的變量賦值操作,

d.<client>() 方法對于類或接口來說并不是必需的,如果一個類中沒有靜態(tài)語句塊,也沒有對變量的賦值操作,那么編譯器可以不為這個類生成<client>() 方法碌补。

e.接口中不能使用靜態(tài)語句塊,但仍然有變量初始化的賦值操作,因此接口與類一樣都會生成<client>() 方法虏束。但接口與類不同的是,執(zhí)行接口的<client>()方法不需要先執(zhí)行父接口的<client>() 方法。只有當父接口中定義的變量使用時,父接口才會初始化厦章。另外,接口的實現(xiàn)類在初始化時也一樣不會執(zhí)行接口的<client>() 方法镇匀。

f.虛擬機會保證一個類的<client>() 方法在多線程環(huán)境中被正確地加鎖、同步 ,如果多個線程同時去初始化一個類,那么只會有一個線程去執(zhí)行這個類的()方法,其他線程都需要阻塞等待,直到活動線程執(zhí)行<client>()方法完畢袜啃。如果在一個類的<client>() 方法中有耗時很長的操作,就可能造成多個進程阻塞,在實際應(yīng)用中這種阻塞往往是很隱蔽的汗侵。



類加載器

????????虛擬機設(shè)計團隊把類加載階段中的“通過一個類的全限定名來獲取描述此類的二進制字節(jié)流”這個動作放到Java虛擬機外部去實現(xiàn),以便讓應(yīng)用程序自己決定如何去獲取所需要的類。實現(xiàn)這個動作的代碼模塊稱為“類加載器”群发。

????????類加載器可以說是Java語言的一項創(chuàng)新,也是Java語言流行的重要原因之一,它最初是為了滿足Java Applet的需求而開發(fā)出來的晰韵。雖然目前Java Applet技術(shù)基本上已經(jīng)“死掉”,但類加載器卻在類層次劃分、OSGi熟妓、熱部署雪猪、代碼加密等領(lǐng)域大放異彩,成為了Java技術(shù)體系中一塊重要的基石,可謂是失之桑榆,收之東隅。

類與類加載器

????????類加載器雖然只用于實現(xiàn)類的加載動作,但它在Java程序中起到的作用卻遠遠不限于類加載階段起愈。對于任意一個類,都需要由加載它的類加載器和這個類本身一同確立其在Java虛擬機中的唯一性,每一個類加載器,都擁有一個獨立的類名稱空間只恨。這句話可以表達得更通俗一些 :比較兩個類是否“相等”,只有在這兩個類是由同一個類加載器加載的前提下才有意義 ,否則,即使這兩個類來源于同一個Class文件 ,被同一個虛擬機加載,只要加載它們的類加載器不同,那這兩個類就必定不相等。

????????這里所指的“相等”,包括代表類的Class對象的equals()方法抬虽、isAssignableFrom()方法官觅、islnstance()方法的返回結(jié)果,也包括使用instanceof關(guān)鍵字做對象所屬關(guān)系判定等情況。如果沒有注意到類加載器的影響,在某些情況下可能會產(chǎn)生具有迷惑性的結(jié)果,代碼清單7-8中演示了不同的類加載器對instanceof關(guān)鍵字運算的結(jié)果的影響斥赋。

雙親委派模型

? ? ? ? 從Java虛擬機的角度來講,只存在兩種不同的類加載器:一種是啟動類加載器 ( Bootstrap ClassLoader ) ,這個類加載器使用C++語言實現(xiàn),是虛擬機自身的一部分;另—種就是所有其他的類加載器,這些類加載器都由Java語言實現(xiàn),獨立于虛擬機外部,并且全都繼承自抽象java.lang.ClassLoader缰猴。

????????從Java開發(fā)人員的角度來看,類加載器還可以劃分得更細致一些 ,絕大部分Java程序都會使用到以下3種系統(tǒng)提供的類加載器。

????????啟動類加載器(Bootstrap ClassLoader) : 前面已經(jīng)介紹過,這個類加載器負責將存放在\lib目錄中的,或者被-Xbootclasspath參數(shù)所指定的路徑中的,并且是虛擬機識別的(僅按照文件名識別,如rtjar,名字不符合的類庫即使放在lib目錄中也不會被加載)類庫加載到虛擬機內(nèi)存中疤剑。啟動類加載器無法被Java程序直接引用,用戶在編寫自定義類加載器時,如果需要把加載請求委派給引導(dǎo)類加載器,那直接使用null代替即可,如代碼清單 7-9所示為java.lang.ClassLoader.getClassLoader()方法的代碼片段。

????????擴展類加載器(Extension ClassLoader):這個加載器由sun.misc.Launcher$ExtClassLoader實現(xiàn),它負責加載\lib\ext對目錄中的類庫,或者被java.ext.dirs系統(tǒng)變量所指定的路徑中的所有類庫,開發(fā)者可以直接使用擴展類加載器闷堡。

????????應(yīng)用程序類加載器( Application ClassLoader ) : 這個類加載器由sun.misc.Launcher$AppClassLoader實現(xiàn)隘膘。由于這個類加載器是ClassLoader中的getSystemClassLoader()方法的返回值 ,所以一般也稱它為系統(tǒng)類加載器。它負責加載用戶類路徑(ClassPath)上所指定的類庫,開發(fā)者可以直接使用這個類加載器,如果應(yīng)用程序中沒有自定義過自己的類加載器,一 般情況下這個就是程序中默認的類加載器杠览。


????????圖7-2中展示的類加載器之間的這種層次關(guān)系,稱為類加載器的雙親委派模型(Parents Delegation Model ) 弯菊。 雙親委派模型要求除了頂層的啟動類加載器外,其余的類加載器都應(yīng)當有自己的父類加載器。這里類加載器之間的父子關(guān)系一般不會以繼承(Inheritance )的關(guān)系來實現(xiàn),而是都使用組合(Composition)關(guān)系來復(fù)用父加載器的代碼踱阿。

????????類加載器的雙親委派模型在JDK 1.2期間被引入并被廣泛應(yīng)用于之后幾乎所有的Java程序中 ,但它并不是一個強制性的約束模型,而是Java設(shè)計者推薦給開發(fā)者的一種類加載器實現(xiàn)方式管钳。

????????雙親委派模型的工作過程是:如果一個類加載器收到了類加載的請求钦铁,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父加載器去完成才漆,每一個層次的類加載器都是如此牛曹,因此所有的加載請求最終都應(yīng)該傳送到頂層的啟動類加載器中,只有當父加載器反饋自己無法完成這個加載請求(它的搜索范圍中沒有找到所需的類)時醇滥,子加載器才會嘗試自己去加載黎比。

????????使用雙親委派模型來組織類加載器之間的關(guān)系,有一個顯而易見的好處就是Java類隨著它的類加載器一起具備了一種帶有優(yōu)先級的層次關(guān)系。例如類java.lang.Object,它存放在rt.jar之中,無論哪一個類加載器要加載這個類,最終都是委派給處于模型最頂端的啟動類加載器進行加載,因此Object類在程序的各種類加載器環(huán)境中都是同一個類鸳玩。相反 ,如果沒有使用雙親委派模型,由各個類加載器自行去加載的話,如果用戶自己編寫了一個稱為 java.lang.Object的類 ,并放在程序的ClassPath中 ,那系統(tǒng)中將會出現(xiàn)多個不同的Object類 ,Java類型體系中最基礎(chǔ)的行為也就無法保證,應(yīng)用程序也將會變得一片混亂阅虫。如果讀者有興趣的話,可以嘗試去編寫一個與rt.jar類庫中已有類重名的Java類 ,將會發(fā)現(xiàn)可以正常編譯 ,但永遠無法被加載運行。

????????雙親委派模型對于保證Java程序的穩(wěn)定運作很重要,但它的實現(xiàn)卻非常簡單,實現(xiàn)雙親委派的代碼都集中在java.lang.ClassLoader的loadClass()方法之中,如代碼清單7-10所示, 邏輯清晰易懂:先檢查是否已經(jīng)被加載過,若沒有加載則調(diào)用父加載器的loadClass()方法 ,若父加載器為空則默認使用啟動類加載器作為父加載器不跟。如果父類加載失敗,拋出 ClassNotFoundException異常后,再調(diào)用自己的findClass()方法進行加載颓帝。

????????這里只限于HotSpot , 像MRP、Maxife等虛擬機,整個虛擬機本身都是由Java編寫的,自然Bootstrap ClassLoader也是由Java語言而不是C++實現(xiàn)的窝革。退一步講,除了HotSpot以外的其他兩個高能虛擬機JRockit和J9都有一個代表Bootstrap ClassLoader的Java類存在,但是關(guān)鍵方法的實現(xiàn)仍然是使用JNI回調(diào)到C ( 注意不是C++ ) 的實現(xiàn)上,這個Bootstrap ClassLoader的實例也無法被用戶獲取到躲履。

????????即使自定義了自己的類加載器,強行用defineClass()方法去加載以“java.lang”開頭的類也不會成功。如果嘗試這樣做的話,將會收到一個由虛擬機自己拋出的“java.lang.SecurityException : Prohibited package name :java.lang”異常聊闯。



破壞雙親委派模型

????????上文提到過雙親委派模型并不是一個強制性的約束模型,而是Java設(shè)計者推薦給開發(fā)者的類加載器實現(xiàn)方式工猜。在Java的世界中大部分的類加載器都遵循這個模型,但也有例外,到目前為止,雙親委派模型主要出現(xiàn)過3較大規(guī)模的“被破壞”情況。

????????雙親委派模型的第一次“被破壞”其實發(fā)生在雙親委派模型出現(xiàn)之前——即JDK1.2發(fā)布之前菱蔬。由于雙親委派模型在JDK 1.2之后才被引入,而類加載器和抽象類java.lang.ClassLoader則在JDK 1.0時代就已經(jīng)存在,面對已經(jīng)存在的用戶自定義類加載器的實現(xiàn)代碼,Java設(shè)計者引入雙親委派模型時不得不做出一些妥協(xié)篷帅。為了向前兼容, JDK 1.2之后的java.lang.ClassLoader添加了一個新的protected方法findClass()(用戶自定義加載類邏輯), 在此之前,用戶去繼承java.lang.ClassLoader的唯一目的就是為了重寫loadClass()方法(實現(xiàn)雙親委派模型) ,因為虛擬機在進行類加載的時候會調(diào)用加載器的私有方法loadClassInternal() 這個方法的唯一邏輯就是去調(diào)用自己的loadClass()。

????????上一節(jié)我們已經(jīng)看過loadClass()方法的代碼,雙親委派的具體邏輯就實現(xiàn)在這個方法之中, JDK 1.2之后已不提倡用戶再去覆蓋loadClass()方法,而應(yīng)當把自己的類加載邏輯寫到findClass()方法中,在loadClass()方法的邏輯里如果父類加載失敗,則會調(diào)用自己的findClass()方法來完成加載,這樣就可以保證新寫出來的類加載器是符合雙親委派規(guī)則的拴泌。

????????雙親委派模型的第二次“被破壞”是由這個模型自身的缺陷所導(dǎo)致的,雙親委派很好地解決了各個類加載器的基礎(chǔ)類的統(tǒng)一問題(越基礎(chǔ)的類由越上層的加載器進行加載),基礎(chǔ)類之所以稱為“基礎(chǔ)” ,是因為它們總是作為被用戶代碼調(diào)用的API,但世事往往沒有絕對的完美 ,如果基礎(chǔ)類又要調(diào)用回用戶的代碼,那該怎么辦?

????????這并非是不可能的事情,一個典型的例子便是JNDI服務(wù) ,JNDI現(xiàn)在已經(jīng)是Java的標準服務(wù),它的代碼由啟動類加載器去加載(在JDK 1.3時放進去的rt.jar) ,但JNDI的目的就是對資源進行集中管理和查找,它需要調(diào)用由獨立廠商實現(xiàn)并部署在應(yīng)用程序的ClassPath下的JNDI接口提供者(SH,Service Provider Interface)的代碼,但啟動類加載器不可能“認識”這些 代碼啊!那該怎么辦?

????????為了解決這個問題,Java設(shè)計團隊只好引入了一個不太優(yōu)雅的設(shè)計:線程上下文類加載器(Thread Context ClassLoader)魏身。這個類加載器可以通過java.lang.Thread類的setContextClassLoaser()方法進行設(shè)置,如果創(chuàng)建線程時還未設(shè)置,它將會從父線程中繼承一個 ,如果在應(yīng)用程序的全局范圍內(nèi)都沒有設(shè)置過的話,那這個類加載器默認就是應(yīng)用程序類加載器。

????????有了線程上下文類加載器,就可以做一些“舞弊” 的事情了,JNDI服務(wù)使用這個線程上下文類加載器去加載所需要的SPI代碼 ,也就是父類加載器請求子類加載器去完成類加載的動作 ,這種行為實際上就是打通了雙親委派模型的層次結(jié)構(gòu)來逆向使用類加載器,實際上已經(jīng)違背了雙親委派模型的一般性原則,但這也是無可奈何的事情蚪腐。Java中所有涉及SPI的加載動作基本上都采用這種方式,例如JNDI箭昵、JDBC、JCE回季、JAXB和JBI等家制。

????????雙親委派模型的第三次“被破壞”是由于用戶對程序動態(tài)性的追求而導(dǎo)致的,這里所說的“動態(tài)性”指的是當前一些非常“熱門”的名詞:代碼熱替換(HotSwap) 泡一、模塊熱部署(Hot Deployment)等 ,說白了就是希望應(yīng)用程序能像我們的計算機外設(shè)那樣,接上鼠標颤殴、U盤 ,不用重啟就能立即使用,鼠標有問題或要升級就換個鼠標,不用停機也不用重啟鼻忠。對于個人計算機來說,重啟一次其實沒有什么大不了的,但對于一些生產(chǎn)系統(tǒng)來說,關(guān)機重啟一次可能就要被列為生產(chǎn)事故,這種情況下熱部署就對軟件開發(fā)者,尤其是企業(yè)級軟件開發(fā)者具有很大的吸引力涵但。

????????Sun公司所提出的JSR-294、JSR-277規(guī)范在與JCP組織的模塊化規(guī)范之爭中落敗給JSR-291(即OSGi R4.2),雖然Sun不甘失去Java模塊化的主導(dǎo)權(quán),獨立在發(fā)展Jigsaw項目,但目前OSGi已經(jīng)成為了業(yè)界“事實上”的Java模塊化標準,而OSGi實現(xiàn)模塊化熱部署的關(guān)鍵則是它自定義的類加載器機制的實現(xiàn)。每一個程序模塊(OSGi中稱為Bundle)都有一個自己的類加載器,當需要更換一個Bundle時,就把Bundle連同類加載器一起換掉以實現(xiàn)代碼的熱替換矮瘟。

????????在OSGi環(huán)境下,類加載器不再是雙親委派模型中的樹狀結(jié)構(gòu),而是進一步發(fā)展為更加復(fù)雜的網(wǎng)狀結(jié)構(gòu),當收到類加載請求時,OSGi將按照下面的順序進行類搜索:

1 ) 將以java.*開頭的類委派給父類加載器加載瞳脓。

2 )否則,將委派列表名單內(nèi)的類委派給父類加載器加載。

3 )否則,將Import列表中的類委派給Export這個類的Bundle的類加載器加載澈侠。

4 ) 否則,查找當前Bundle的ClassPath, 使用自己的類加載器加載劫侧。

5 ) 否則,查找類是否在自己的Fragment Bundle中,如果在,則委派給Fragment Bundle的類加載器加載。

6 ) 否則,查找Dynamic Import列表的Bundle, 委派給對應(yīng)Bundle的類加載器加載埋涧。

7 ) 否則,類查找失敗板辽。

上面的查找順序中只有開頭兩點仍然符合雙親委派規(guī)則,其余的類查找都是在平級的類加載器中進行的。

筆者雖然使用了“被破壞”這個詞來形容上述不符合雙親委派模型原則的行為,但這里“被破壞”并不帶有貶義的感情色彩棘催。只要有足夠意義和理由,突破已有的原則就可認為是一種創(chuàng)新劲弦。正如OSGi中的類加載器并不符合傳統(tǒng)的雙親委派的類加載器,并且業(yè)界對其為了實現(xiàn)熱部署而帶來的額外的高復(fù)雜度還存在不少爭議,但在Java程序員中基本有一個共識 :OSGi中對類加載器的使用是很值得學(xué)習(xí)的,弄懂了OSGi的實現(xiàn),就可以算是掌握了類加載器的精髓。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末醇坝,一起剝皮案震驚了整個濱河市邑跪,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌呼猪,老刑警劉巖画畅,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異宋距,居然都是意外死亡轴踱,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進店門谚赎,熙熙樓的掌柜王于貴愁眉苦臉地迎上來淫僻,“玉大人,你說我怎么就攤上這事壶唤■椋” “怎么了?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵闸盔,是天一觀的道長悯辙。 經(jīng)常有香客問我,道長迎吵,這世上最難降的妖魔是什么躲撰? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮钓觉,結(jié)果婚禮上茴肥,老公的妹妹穿的比我還像新娘。我一直安慰自己荡灾,他們只是感情好,可當我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著批幌,像睡著了一般础锐。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上荧缘,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天皆警,我揣著相機與錄音,去河邊找鬼截粗。 笑死信姓,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的绸罗。 我是一名探鬼主播意推,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼珊蟀!你這毒婦竟也來了菊值?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤育灸,失蹤者是張志新(化名)和其女友劉穎腻窒,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體磅崭,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡儿子,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了砸喻。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片柔逼。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖恩够,靈堂內(nèi)的尸體忽然破棺而出卒落,到底是詐尸還是另有隱情,我是刑警寧澤蜂桶,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布儡毕,位于F島的核電站,受9級特大地震影響扑媚,放射性物質(zhì)發(fā)生泄漏腰湾。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一疆股、第九天 我趴在偏房一處隱蔽的房頂上張望费坊。 院中可真熱鬧,春花似錦旬痹、人聲如沸附井。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽永毅。三九已至把跨,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間沼死,已是汗流浹背着逐。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留意蛀,地道東北人耸别。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像县钥,于是被迫代替她去往敵國和親秀姐。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,901評論 2 345

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