大家知道澳骤,我們的Java程序被編譯器編譯成class文件谆棺,在class文件中描述的各種信息睁搭,最終都需要加載到虛擬機內(nèi)存才能運行和使用砸逊,那么虛擬機是如何加載這些class文件的呢悉罕?在加載class文件的過程中虛擬機又干了哪些事呢赤屋?今天我們來解密虛擬機的類加載機制。
虛擬機把class文件加載到內(nèi)存壁袄,并對數(shù)據(jù)進行校驗类早、解析和初始化,最終形成可以被虛擬機直接使用的Java類型(Class對象)嗜逻,這就是虛擬機的類加載機制涩僻。
類從被加載到虛擬機內(nèi)存開始,到卸載出內(nèi)存為止栈顷,它的整個生命周期包括:加載逆日、驗證、準(zhǔn)備萄凤、解析室抽、初始化、使用和卸載7個階段靡努。坪圾,其中驗證、準(zhǔn)備和解析3個階段統(tǒng)稱為連接階段惑朦。如圖:
前面的5個階段就是類加載的過程兽泄。其中加載、驗證漾月、準(zhǔn)備和初始化這幾個階段的順序是確定的病梢,而解析階段則不一定,在某些情況下它可以在初始化階段以后才進行栅屏。那么飘千,在類加載的每一個步驟中,虛擬機都進行了那些工作呢栈雳?
加載
加載是類加載過程的第一個階段护奈,在這一階段,虛擬機主要完成了3件事:
1哥纫、通過類的全限定名來獲取定義這個類的二進制字節(jié)流霉旗。簡單來說就是,通過類的包名加類名來定位到此類的class文件的位置,相當(dāng)于一個資源定位的過程厌秒。
2读拆、將這個字節(jié)流代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu)。也就是將類中定義的靜態(tài)變量鸵闪、常量等信息存儲在方法區(qū)中檐晕。
3、在堆內(nèi)存中生成一個代表這個類的java.lang.Class對象蚌讼,作為方法區(qū)中這個類的各種數(shù)據(jù)的訪問入口辟灰。
總結(jié)一下,加載階段的主要工作就是篡石,把class二進制文件加載到內(nèi)存后芥喇,將類中定義的靜態(tài)變量、常量凰萨、類信息等數(shù)據(jù)存放到方法區(qū)继控,并在堆內(nèi)存中創(chuàng)建一個代表這個類的Class對象,作為方法區(qū)中這個類的數(shù)據(jù)信息的訪問入口胖眷,程序猿可以持有這個Class對象武通。
驗證
驗證是連接階段的第一步,驗證階段的目的是確保class文件中包含的信息符合虛擬機的要求瘦材,并且不會危害到虛擬機自身的安全厅须。驗證的內(nèi)容主要包含以下幾個方面:
1、文件格式驗證食棕。主要目的是保證輸入的字節(jié)流能正確的解析并存儲在方法區(qū)中朗和,格式上符合一個Java類型信息的要求。這個階段的驗證是基于二進制字節(jié)流進行的簿晓,只有通過了這個階段的驗證眶拉,字節(jié)流才能進入方法區(qū)進行存儲,所有后面的3個階段的驗證都是基于方法區(qū)的存儲結(jié)構(gòu)進行的憔儿,不會直接操作字節(jié)流忆植。
2、元數(shù)據(jù)驗證谒臼。這一階段的主要目的是對類的元數(shù)據(jù)(定義數(shù)據(jù)的數(shù)據(jù))信息進行語義校驗朝刊,確保不存在不符合Java語言規(guī)范的元數(shù)據(jù)信息。包括:該類是否有父類蜈缤、該類的父類是否繼承了不允許被繼承的類拾氓、該類中的字段和方法是否與父類產(chǎn)生矛盾等等。
3底哥、字節(jié)碼驗證咙鞍。目的是通過數(shù)據(jù)流和控制流分析房官,確定程序語義是否合法、符合邏輯续滋。在第二階段對元數(shù)據(jù)信息的數(shù)據(jù)類型做完校驗后翰守,這個階段將對類的方法體進行分析,保證被校驗的類的方法在運行時不會危害虛擬機的安全疲酌。
4蜡峰、符號引用驗證。符號引用驗證發(fā)生在虛擬機將符號引用轉(zhuǎn)化為直接引用的時候徐勃,這個轉(zhuǎn)化動作是在連接的第三階段——解析階段中進行的事示。符號引用驗證的目的是確保解析動作能夠正常執(zhí)行早像。
對于類加載機制而言僻肖,驗證階段是一個非常重要、但不是一定必要的階段卢鹦。如果所運行的全部代碼都已經(jīng)被反復(fù)使用和驗證過了臀脏,那么就可以使用虛擬機參數(shù)-Xverify:none來關(guān)閉大部分的類驗證措施,以縮短類加載的時間冀自。
準(zhǔn)備
準(zhǔn)備階段的主要工作是為類的靜態(tài)變量分配內(nèi)存并設(shè)置變量的初始默認(rèn)值揉稚。這些變量所使用的內(nèi)存都在方法區(qū)中分配。這里有兩個問題需要說明:
1熬粗、這一階段進行內(nèi)存分配的僅包括靜態(tài)變量搀玖,而不包括實例變量(靜態(tài)變量是所有對象共有的,實例變量是對象私有的)驻呐,實例變量將會在對象實例化時隨著對象一起分配在Java堆中灌诅。
2、這里說的為對象賦初始值是各數(shù)據(jù)類型對應(yīng)的零值含末。假設(shè)有一個靜態(tài)變量定義為public static int a = 1; 那變量a的初始值就是0而不是1猜拾,初始值1在初始化階段賦給變量a。如果是引用類型初始默認(rèn)值就是null佣盒。
解析
解析階段是虛擬機將常量池內(nèi)的符號引用替換為直接引用的過程挎袜。
符號引用:符號引用以一組符號來描述所引用的目標(biāo),符號可以使任何形式的字面量肥惭,只要使用時能無歧義的定位到目標(biāo)即可盯仪。符號引用的字面量形式在Java虛擬機規(guī)范的Class文件格式中有明確定義。
直接引用:直接引用可以是直接指向目標(biāo)的指針蜜葱、相對偏移量或者是一個能間接定位到目標(biāo)的句柄全景。直接引用是和虛擬機實現(xiàn)的內(nèi)存布局有關(guān)的,同一個符號引用在不同虛擬機實例上翻譯出來的直接引用一般不會相同笼沥。如果有了直接引用蚪燕,那么引用的目標(biāo)必定已經(jīng)在內(nèi)存中存在娶牌。
解析動作主要針對類或接口、字段馆纳、靜態(tài)方法诗良、接口方法、方法類型鲁驶、方法句柄和調(diào)用點限定符這幾類符號引用進行鉴裹。
初始化
初始化階段是類加載過程的最后一步。在前面的類加載過程中钥弯,除了加載階段我們可以通過自定義的類加載器參與之外径荔,其余的階段都是虛擬機自動完成的。到了初始化階段脆霎,才真正開始執(zhí)行我們程序中定義的Java代碼总处。初始化階段的主要工作是給類的靜態(tài)變量賦予我們程序中指定的初始值。也就是上面準(zhǔn)備階段提到的睛蛛,變量a的值從0變?yōu)?的過程鹦马。這個階段我們程序指定初始值包括兩種手段:
1、聲明靜態(tài)變量時顯式的復(fù)制忆肾。例如:public static int a = 1; 在初始化階段會把1賦給變量a荸频。
2、通過靜態(tài)代碼塊賦值客冈。例如:static { a = 2 }; 變量a 的初始值賦為2旭从。
這兩種方式的賦值順序是由語句在源文件中出現(xiàn)的順序來決定的。
以上就是Java虛擬機類加載機制的整個過程以及在每個階段虛擬機所執(zhí)行的動作场仲。
雙親委派模型
前面提到過和悦,在類加載的整個過程中,除了加載階段我們可以通過自定義的類加載器參與之外燎窘,其他的階段都是虛擬機幫我們完成的摹闽。虛擬機設(shè)計團隊把加載這個動作放到Java虛擬機外部去實現(xiàn),實現(xiàn)這個動作的代碼模塊稱為“類加載器”褐健。這樣做的目的是讓應(yīng)用程序自己去決定如何獲取所需要的類付鹿。
除了我們自己可以定義類加載器,Java虛擬機也為我們提供了系統(tǒng)自帶的類加載器蚜迅。主要可以分為以下三種:
根類加載器(Bootstrap ClassLoader):這個類加載器負(fù)責(zé)加載存放在<JAVA_HOME>\lib目錄中的舵匾,或者通過參數(shù)-Xbootclasspath所指定的路徑中的類。
擴展類加載器(Extension ClassLoader):這個加載器負(fù)責(zé)加載<JAVA_HOME>\lib\ext目錄中的谁不,或者被java.ext.dirs系統(tǒng)變量所指定的路徑中的所有類庫坐梯。
應(yīng)用類加載器(Application ClassLoader):它負(fù)責(zé)加載用戶設(shè)置的ClassPath路徑上所指定的類庫。如果應(yīng)用程序中沒有自定義的類加載器刹帕,一般情況下這個就是程序默認(rèn)的類加載器吵血。
我們的應(yīng)用程序都是由這3種類加載器相互配合進行加載的谎替,如果有必要,還可以定義自己的類加載器蹋辅。這些類加載器之間的關(guān)系如下:
上圖中展示的類加載器之間的層次關(guān)系钱贯,稱為類加載器的雙親委派模型。雙親委派模型要求除了頂層的根類加載器外侦另,其余的類加載器都應(yīng)當(dāng)有自己的父類加載器秩命。雙親委派模型的工作過程是:如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類褒傅,而是把這個請求委派給父類加載器去完成弃锐,每一個層次的類加載器都是如此,因此所有的類加載請求最終都應(yīng)該傳送到根類加載器中殿托,只有當(dāng)父加載器反饋自己無法完成這個加載請求(搜索范圍中沒有找到所需的類)時霹菊,子加載器才會嘗試自己去加載。
類加載器雖然只用于實現(xiàn)類的加載動作碌尔,但是它在Java程序中起的作用卻不僅僅是進行類加載浇辜。對于任意一個類,都需要由加載它的類加載器和這個類本身一同確立它在Java虛擬機中的唯一性唾戚。簡單來說就是,一個類的class文件被不同的兩個類加載器加載待诅,那么加載后的這兩個類就不“相等”叹坦,不是相同的類。
使用雙親委派模型卑雁,有一個顯而易見的好處就是Java類隨著它的類加載器一起具備了一種帶有優(yōu)先級的層次關(guān)系募书。例如java.lang.Object類,它存放在rt.jar中测蹲,無論哪一個類加載器要加載這個類莹捡,最終都會委派給模型最頂端的根類加載器進行加載,因此Object類在程序的各種類加載器環(huán)境中都是同一個類(始終被根類加載器加載)扣甲。相反篮赢,如果不使用雙親委派模型,由各個類加載器自己去加載的話琉挖,假如用戶編寫了一個稱為java.lang.Object的類启泣,并放在ClassPath中,那系統(tǒng)中會出現(xiàn)多個不同的Object類示辈,應(yīng)用程序也會變的一片混亂寥茫。
以上內(nèi)容總結(jié)了Java類加載機制的整個過程以及雙親委派模型的原理,歡迎交流矾麻。
作者:風(fēng)中程序猿
出處:https://www.cnblogs.com/fangfuhai/p/7230179.html