欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

一文詳解Java中的類加載機(jī)制

 更新時(shí)間:2022年05月05日 10:10:42   作者:怪咖軟妹@  
Java虛擬機(jī)把描述類的數(shù)據(jù)從Class文件加載到內(nèi)存,并對數(shù)據(jù)進(jìn)行校驗(yàn)、轉(zhuǎn)換解析和初始化,最終形成可以被虛擬機(jī)直接使用的Java類型,這個(gè)過程被稱作虛擬機(jī)的類加載機(jī)制。本文將詳解Java的類加載機(jī)制,需要的可以參考一下

一、前言

Java虛擬機(jī)把描述類的數(shù)據(jù)從Class文件加載到內(nèi)存,并對數(shù)據(jù)進(jìn)行校驗(yàn)、轉(zhuǎn)換解析和初始化,最 終形成可以被虛擬機(jī)直接使用的Java類型,這個(gè)過程被稱作虛擬機(jī)的類加載機(jī)制。

通俗點(diǎn)講:new 一個(gè)對象的時(shí)候,首先得需要保證,new的這個(gè)對象的class已經(jīng)加載到內(nèi)存,疑問來了,為什么非得要加載到內(nèi)存呢?我們在寫程序的時(shí)候,new一個(gè)對象也沒說非得加到內(nèi)存當(dāng)中啊,原因是JVM自帶了類加載器這個(gè)功能,也就是在new的時(shí)候,會(huì)判斷內(nèi)存是否存在這個(gè)class類,如果有的話就不用加載,沒有的話類加載器會(huì)自動(dòng)加載,將class文件讀到內(nèi)存當(dāng)中,所以我們不深層去了解,根本感知不到類加載器的作用。

在Java語言里面,類型的加載、連接和初始化過程都是在程序運(yùn)行期間完成 的,這種策略讓Java語言進(jìn)行提前編譯會(huì)面臨額外的困難,也會(huì)讓類加載時(shí)稍微增加一些性能開銷, 但是卻為Java應(yīng)用提供了極高的擴(kuò)展性和靈活性,Java天生可以動(dòng)態(tài)擴(kuò)展的語言特性就是依賴運(yùn)行期動(dòng)態(tài)加載和動(dòng)態(tài)連接這個(gè)特點(diǎn)實(shí)現(xiàn)的。

例如,編寫一個(gè)面向接口的應(yīng)用程序,可以等到運(yùn)行時(shí)再指定其實(shí)際的實(shí)現(xiàn)類,從最基礎(chǔ)的Applet、JSP到相對復(fù)雜的OSGi技術(shù),都依賴著Java語言運(yùn)行期類加載才 得以誕生。

二、類加載的時(shí)機(jī)

2.1 類加載過程

一個(gè)類型從被加載到虛擬機(jī)內(nèi)存中開始,到卸載出內(nèi)存為止,它的整個(gè)生命周期將會(huì)經(jīng)歷加載 (Loading)、驗(yàn)證(Verification)、準(zhǔn)備(Preparation)、解析(Resolution)、初始化 (Initialization)、使用(Using)和卸載(Unloading)七個(gè)階段,其中驗(yàn)證、準(zhǔn)備、解析三個(gè)部分統(tǒng)稱 為連接(Linking)。這七個(gè)階段的發(fā)生順序如下圖所示。

加載、驗(yàn)證、準(zhǔn)備、初始化和卸載這五個(gè)階段的順序是確定的,類型的加載過程必須按 照這種順序按部就班地開始,而解析階段則不一定:它在某些情況下可以在初始化階段之后再開始, 這是為了支持Java語言的運(yùn)行時(shí)綁定特性(也稱為動(dòng)態(tài)綁定或晚期綁定)。

2.2 什么時(shí)候類初始化

關(guān)于在什么情況下需要開始類加載過程的第一個(gè)階段“加載”,《Java虛擬機(jī)規(guī)范》中并沒有進(jìn)行 強(qiáng)制約束,這點(diǎn)可以交給虛擬機(jī)的具體實(shí)現(xiàn)來自由把握。但是對于初始化階段,《Java虛擬機(jī)規(guī)范》 則是嚴(yán)格規(guī)定了有且只有六種情況必須立即對類進(jìn)行“初始化”(而加載、驗(yàn)證、準(zhǔn)備自然需要在此之 前開始):

1.遇到new、getstatic、putstatic或invokestatic這四條字節(jié)碼指令時(shí),如果類型沒有進(jìn)行過初始 化,則需要先觸發(fā)其初始化階段。能夠生成這四條指令的典型Java代碼場景有:

  • 使用new關(guān)鍵字實(shí)例化對象的時(shí)候。
  • 讀取或設(shè)置一個(gè)類型的靜態(tài)字段(被final修飾、已在編譯期把結(jié)果放入常量池的靜態(tài)字段除外) 的時(shí)候。
  • 調(diào)用一個(gè)類型的靜態(tài)方法的時(shí)候。

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

3.當(dāng)初始化類的時(shí)候,如果發(fā)現(xiàn)其父類還沒有進(jìn)行過初始化,則需要先觸發(fā)其父類的初始化。

4.當(dāng)虛擬機(jī)啟動(dòng)時(shí),用戶需要指定一個(gè)要執(zhí)行的主類(包含main()方法的那個(gè)類),虛擬機(jī)會(huì)先 初始化這個(gè)主類。

5.當(dāng)使用JDK 7新加入的動(dòng)態(tài)語言支持時(shí),如果一個(gè)java.lang.invoke.MethodHandle實(shí)例最后的解 析結(jié)果為REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四種類型的方法句 柄,并且這個(gè)方法句柄對應(yīng)的類沒有進(jìn)行過初始化,則需要先觸發(fā)其初始化。

6.當(dāng)一個(gè)接口中定義了JDK 8新加入的默認(rèn)方法(被default關(guān)鍵字修飾的接口方法)時(shí),如果有 這個(gè)接口的實(shí)現(xiàn)類發(fā)生了初始化,那該接口要在其之前被初始化。

這六種場景中的行為稱為對一個(gè)類型進(jìn)行主動(dòng)引用。

2.3 被動(dòng)引用不會(huì)初始化

除此之外,所有引用類型的方 式都不會(huì)觸發(fā)初始化,稱為被動(dòng)引用。下面舉三個(gè)例子來說明何為被動(dòng)引用。

代碼示例一:

package org.fenixsoft.classloading;

/**
 * 被動(dòng)使用類字段演示一:
 * 通過子類引用父類的靜態(tài)字段,不會(huì)導(dǎo)致子類初始化
 */
public class SuperClass {
    static {
        System.out.println("父類 init!");
    }

    public static int value = 123;
}

class SubClass extends SuperClass {
    static {
        System.out.println("子類 init!");
    }
}

/**
 * 非主動(dòng)使用類字段演示
 */
class NotInitialization {
    public static void main(String[] args) {
        System.out.println(SubClass.value);
    }
}

上述代碼運(yùn)行之后,只會(huì)輸出“父類 init!”,而不會(huì)輸出“子類 init!”。

得出結(jié)論:訪問靜態(tài)屬性的時(shí)候,不管是通過子類還是父類來訪問這個(gè)靜態(tài)屬性,只有靜態(tài)屬性所呆的類會(huì)被初始化。至于是否要觸發(fā)子類的加載和驗(yàn)證階段,在《Java虛擬機(jī)規(guī)范》中并未明確規(guī)定。

代碼示例二:

/**
 * 被動(dòng)使用類字段演示二:
 * 通過數(shù)組定義來引用類,不會(huì)觸發(fā)此類的初始化
 */
class NotInitialization {
    public static void main(String[] args) {
        SuperClass[] sca = new SuperClass[10];
    }
}

為了節(jié)省版面,這段代碼復(fù)用了代碼示例一的SuperClass,運(yùn)行之后發(fā)現(xiàn)沒有輸出“父類 init!”,說明并沒有觸發(fā)類org.fenixsoft.classloading.SuperClass的初始化階段。

但是這段代碼里面觸發(fā)了 另一個(gè)名為“[Lorg.fenixsoft.classloading.SuperClass”的類的初始化階段,對于用戶代碼來說,這并不是 一個(gè)合法的類型名稱,它是一個(gè)由虛擬機(jī)自動(dòng)生成的、直接繼承于java.lang.Object的子類,創(chuàng)建動(dòng)作由 字節(jié)碼指令newarray觸發(fā)。

這個(gè)類代表了一個(gè)元素類型為org.fenixsoft.classloading.SuperClass的一維數(shù)組,數(shù)組中應(yīng)有的屬性 和方法(用戶可直接使用的只有被修飾為public的length屬性和clone()方法)都實(shí)現(xiàn)在這個(gè)類里。Java語 言中對數(shù)組的訪問要比C/C++相對安全,很大程度上就是因?yàn)檫@個(gè)類包裝了數(shù)組元素的訪問。

代碼示例三:

/**
 * 被動(dòng)使用類字段演示三:
 * 常量在編譯階段會(huì)存入調(diào)用類的常量池中,本質(zhì)上沒有直接引用到定義常量的類,因此不會(huì)觸發(fā)定義常量的 類的初始化
 */
public class ConstClass {
    static {
        System.out.println("ConstClass init!");
    }

    public static final String HELLOWORLD = "hello world";
}

/**
 * 非主動(dòng)使用類字段演示
 */
class NotInitialization {
    public static void main(String[] args) {
        System.out.println(ConstClass.HELLOWORLD);
    }
}

上述代碼運(yùn)行之后,也沒有輸出“ConstClass init!”,原因是常量在編譯階段存入了常量池,已經(jīng)徹底和類脫離了關(guān)系,也就是常量和類的關(guān)系 在編譯成 Class文件后就已不存在任何聯(lián)系了。常量已經(jīng)不再屬于這個(gè)類。

接口的加載過程與類加載過程稍有不同,針對接口需要做一些特殊說明:接口也有初始化過程, 這點(diǎn)與類是一致的,上面的代碼都是用靜態(tài)語句塊“static{}”來輸出初始化信息的,而接口中不能使 用“static{}”語句塊,但編譯器仍然會(huì)為接口生成“<clinit>()”類構(gòu)造器,用于初始化接口中所定義的 成員變量。接口與類真正有所區(qū)別的是前面講述的六種“有且僅有”需要觸發(fā)初始化場景中的第三種: 當(dāng)一個(gè)類在初始化時(shí),要求其父類全部都已經(jīng)初始化過了,但是一個(gè)接口在初始化時(shí),并不要求其父 接口全部都完成了初始化,只有在真正使用到父接口的時(shí)候(如引用接口中定義的常量)才會(huì)初始 化(被default關(guān)鍵字修飾的接口方法這種情況除外)。

三、類加載的過程

接下來我們會(huì)詳細(xì)了解Java虛擬機(jī)中類加載的全過程,即加載、驗(yàn)證、準(zhǔn)備、解析和初始化這五 個(gè)階段所執(zhí)行的具體動(dòng)作。

3.1 加載

“加載”(Loading)階段是整個(gè)“類加載”(Class Loading)過程中的一個(gè)階段,希望讀者沒有混淆 這兩個(gè)看起來很相似的名詞。在加載階段,Java虛擬機(jī)需要完成以下三件事情:

1.通過一個(gè)類的全限定名來獲取定義此類的二進(jìn)制字節(jié)流。

2.將這個(gè)字節(jié)流所代表的靜態(tài)存儲(chǔ)結(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ù)的訪問入 口。

“通過一個(gè)類的全限定名來獲取定義此類的二進(jìn)制字節(jié)流”這條規(guī)則,它并沒有指明二 進(jìn)制字節(jié)流必須得從某個(gè)Class文件中獲取,確切地說是根本沒有指明要從哪里獲取、如何獲取。許多舉足輕重的Java技術(shù)都建立在這 一基礎(chǔ)之上,例如:

1.從ZIP壓縮包中讀取,這很常見,最終成為日后JAR、EAR、WAR格式的基礎(chǔ)。

2.運(yùn)行時(shí)計(jì)算生成,這種場景使用得最多的就是動(dòng)態(tài)代理技術(shù),在java.lang.reflect.Proxy中,就是用 了ProxyGenerator.generateProxyClass()來為特定接口生成形式為“*$Proxy”的代理類的二進(jìn)制字節(jié)流。

3.由其他文件生成,典型場景是JSP應(yīng)用,由JSP文件生成對應(yīng)的Class文件。

4.可以從加密文件中獲取,這是典型的防Class文件被反編譯的保護(hù)措施,通過加載時(shí)解密Class文 件來保障程序運(yùn)行邏輯不被窺探。

加載階段既可以使用Java虛擬機(jī)里內(nèi)置的引導(dǎo)類加 載器來完成,也可以由用戶自定義的類加載器去完成,開發(fā)人員通過定義自己的類加載器去控制字節(jié) 流的獲取方式(重寫一個(gè)類加載器的findClass()或loadClass()方法),實(shí)現(xiàn)根據(jù)自己的想法來賦予應(yīng)用 程序獲取運(yùn)行代碼的動(dòng)態(tài)性。

對于數(shù)組類而言,情況就有所不同,數(shù)組類本身不通過類加載器創(chuàng)建,它是由Java虛擬機(jī)直接在內(nèi)存中動(dòng)態(tài)構(gòu)造出來的。但數(shù)組類與類加載器仍然有很密切的關(guān)系,因?yàn)閿?shù)組類的元素類型最終還是要靠類加載器來完成加載,一個(gè)數(shù)組類(下面簡稱 為C)創(chuàng)建過程遵循以下規(guī)則:

1.如果數(shù)組的組件類型是引用類型,那就遞歸采用本節(jié)中定義的加載過程去加載這個(gè)組件類型,數(shù)組C將被標(biāo) 識(shí)在加載該組件類型的類加載器的類名稱空間上。

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

加載階段與連接階段的部分動(dòng)作(如一部分字節(jié)碼文件格式驗(yàn)證動(dòng)作)是交叉進(jìn)行的,加載階段 尚未完成,連接階段可能已經(jīng)開始,但這些夾在加載階段之中進(jìn)行的動(dòng)作,仍然屬于連接階段的一部 分,這兩個(gè)階段的開始時(shí)間仍然保持著固定的先后順序。

3.2 驗(yàn)證

驗(yàn)證是連接階段的第一步,這一階段的目的是確保Class文件的字節(jié)流中包含的信息符合《Java虛擬機(jī)規(guī)范》的全部約束要求,保證這些信息被當(dāng)作代碼運(yùn)行后不會(huì)危害虛擬機(jī)自身的安全。

編譯器驗(yàn)證: Java語言本身是相對安全的編程語言(起碼對于C/C++來說是相對安全的),將一個(gè)對象轉(zhuǎn)型為它并未實(shí)現(xiàn)的類型、跳轉(zhuǎn)到不存在的代碼 行之類的事情,如果嘗試這樣去做了,編譯器會(huì)毫不留情地拋出異常、拒絕編譯。

字節(jié)碼驗(yàn)證: 但前面也曾說過, Class文件并不一定只能由Java源碼編譯而來,Java代碼無法做到的事情在字節(jié)碼層面上都是可以實(shí)現(xiàn)的,至少 語義上是可以表達(dá)出來的。Java虛擬機(jī)如果不檢查輸入的字節(jié)流,對其完全信任的話,很可能會(huì)因?yàn)?載入了有錯(cuò)誤或有惡意企圖的字節(jié)碼流而導(dǎo)致整個(gè)系統(tǒng)受攻擊甚至崩潰,所以驗(yàn)證字節(jié)碼是Java虛擬 機(jī)保護(hù)自身的一項(xiàng)必要措施。

驗(yàn)證階段的工作量在虛擬機(jī)的類加載過程中占了相當(dāng)大 的比重。驗(yàn)證階段大致上會(huì)完成下面四個(gè)階段的檢驗(yàn)動(dòng)作:文件格式驗(yàn)證、元數(shù)據(jù)驗(yàn)證、字節(jié) 碼驗(yàn)證和符號(hào)引用驗(yàn)證。

文件格式驗(yàn)證:該驗(yàn)證階段的主要目的是保證輸入的字節(jié)流能正確地解析并存儲(chǔ)于方法區(qū)之內(nèi),格式上符 合描述一個(gè)Java類型信息的要求。這階段的驗(yàn)證是基于二進(jìn)制字節(jié)流進(jìn)行的,只有通過了這個(gè)階段的 驗(yàn)證之后,這段字節(jié)流才被允許進(jìn)入Java虛擬機(jī)內(nèi)存的方法區(qū)中進(jìn)行存儲(chǔ),所以后面的三個(gè)驗(yàn)證階段 全部是基于方法區(qū)的存儲(chǔ)結(jié)構(gòu)上進(jìn)行的,不會(huì)再直接讀取、操作字節(jié)流了。

元數(shù)據(jù)驗(yàn)證:這個(gè)階段可能包括的驗(yàn)證點(diǎn)如下(內(nèi)容比較多,只列了以下幾點(diǎn)):

1.這個(gè)類是否有父類(除了java.lang.Object之外,所有的類都應(yīng)當(dāng)有父類)。

2.這個(gè)類的父類是否繼承了不允許被繼承的類(被final修飾的類)。

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

字節(jié)碼驗(yàn)證:第三階段是整個(gè)驗(yàn)證過程中最復(fù)雜的一個(gè)階段,主要目的是通過數(shù)據(jù)流分析和控制流分析,確定 程序語義是合法的、符合邏輯的。

符號(hào)引用驗(yàn)證:主要作用是驗(yàn)證該類是否缺少或者被禁止訪問它依賴的某些外部 類、方法、字段等資源。類、字段、方法的可訪問性(private、protected、public、)是否可被當(dāng) 前類訪問。

如果 程序運(yùn)行的全部代碼(包括自己編寫的、第三方包中的、從外部加載的、動(dòng)態(tài)生成的等所有代碼)都 已經(jīng)被反復(fù)使用和驗(yàn)證過,在生產(chǎn)環(huán)境的實(shí)施階段就可以考慮使用-Xverify:none參數(shù)來關(guān)閉大部分的 類驗(yàn)證措施,以縮短虛擬機(jī)類加載的時(shí)間。

3.3 準(zhǔn)備

準(zhǔn)備階段是正式為類中定義的變量(即靜態(tài)變量,被static修飾的變量)分配內(nèi)存并設(shè)置類變量初 始值的階段,從概念上講,這些變量所使用的內(nèi)存都應(yīng)當(dāng)在方法區(qū)中進(jìn)行分配,但必須注意到方法區(qū) 本身是一個(gè)邏輯上的區(qū)域,在JDK 7及之前,HotSpot使用永久代來實(shí)現(xiàn)方法區(qū)時(shí),實(shí)現(xiàn)是完全符合這 種邏輯概念的;而在JDK 8及之后,類變量則會(huì)隨著Class對象一起存放在Java堆中。

關(guān)于準(zhǔn)備階段,還有兩個(gè)容易產(chǎn)生混淆的概念筆者需要著重強(qiáng)調(diào),首先是這時(shí)候進(jìn)行內(nèi)存分配的 僅包括類變量,而不包括實(shí)例變量,實(shí)例變量將會(huì)在對象實(shí)例化時(shí)隨著對象一起分配在Java堆中。其 次是這里所說的初始值“通常情況”下是數(shù)據(jù)類型的零值,假設(shè)一個(gè)類變量的定義為:

public static int value = 123;

那變量value在準(zhǔn)備階段過后的初始值為0而不是123,因?yàn)檫@時(shí)尚未開始執(zhí)行任何Java方法,而把 value賦值為123的putstatic指令是程序被編譯后,存放于類構(gòu)造器()方法之中,所以把value賦值 為123的動(dòng)作要到類的初始化階段才會(huì)被執(zhí)行。下圖列出了Java中所有基本數(shù)據(jù)類型的零值。

3.4 解析

解析階段是Java虛擬機(jī)將常量池內(nèi)的符號(hào)引用替換為直接引用的過程

  • 符號(hào)引用(Symbolic References):符號(hào)引用以一組符號(hào)來描述所引用的目標(biāo),符號(hào)可以是任何形式的字面量,只要使用時(shí)能無歧義地定位到目標(biāo)即可
  • 直接引用(Direct References):直接引用是可以直接指向目標(biāo)的指針、相對偏移量或者是一個(gè)能間接定位到目標(biāo)的句柄

符號(hào)引用與虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局無關(guān),直接引用是和虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局直接相關(guān)的,同一個(gè)符號(hào)引用在不同虛擬機(jī)實(shí)例上翻譯出來的直接引用一般不會(huì)相同。

如果有了直接引用,那引用的目標(biāo)必定已經(jīng)在虛擬機(jī)的內(nèi)存中存在。

解析動(dòng)作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調(diào)用點(diǎn)限定符這7類符號(hào)引用進(jìn)行

符號(hào)引用要轉(zhuǎn)換成直接引用才有效,這也說明直接引用的效率要比符號(hào)引用高。那為什么要用符號(hào)引用呢?這是因?yàn)轭惣虞d之前,javac會(huì)將源代碼編譯成.class文件,這個(gè)時(shí)候javac是不知道被編譯的類中所引用的類、方法或者變量他們的引用地址在哪里,所以只能用符號(hào)引用來表示。

3.5 初始化

類的初始化階段是類加載過程的最后一個(gè)步驟,除了在加載階 段用戶應(yīng)用程序可以通過自定義類加載器的方式局部參與外,其余動(dòng)作都完全由Java虛擬機(jī)來主導(dǎo)控 制。直到初始化階段,Java虛擬機(jī)才真正開始執(zhí)行類中編寫的Java程序代碼,將主導(dǎo)權(quán)移交給應(yīng)用程 序。

進(jìn)行準(zhǔn)備階段時(shí),變量已經(jīng)賦過一次系統(tǒng)要求的初始零值,而在初始化階段,則會(huì)根據(jù)程序員通 過程序編碼制定的主觀計(jì)劃去初始化類變量和其他資源。

初始化階段就是執(zhí)行類構(gòu)造器<clinit>()方法的過程。<clinit>()并不是程序員在Java代碼中直接編寫 的方法,它是Javac編譯器的自動(dòng)生成物。

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

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

由于父類的<clinit>()方法先執(zhí)行,也就意味著父類中定義的靜態(tài)語句塊要優(yōu)先于子類的變量賦值 操作,如下代碼示例,輸出2。

public class Test {
    static class Parent {
        public static int A = 1;

        static {
            A = 2;
        }
    }

    static class Sub extends Parent {
        public static int B = A;
    }

    public static void main(String[] args) {
        System.out.println(Sub.B);
    }
}

接口中不能使用靜態(tài)語句塊,但仍然有變量初始化的賦值操作,因此接口與類一樣都會(huì)生成 <clinit>()方法。但接口與類不同的是,執(zhí)行接口的<clinit>()方法不需要先執(zhí)行父接口的<clinit>()方法, 因?yàn)橹挥挟?dāng)父接口中定義的變量被使用時(shí),父接口才會(huì)被初始化。此外,接口的實(shí)現(xiàn)類在初始化時(shí)也 一樣不會(huì)執(zhí)行接口的<clinit>()方法。

Java虛擬機(jī)必須保證一個(gè)類的<clinit>()方法在多線程環(huán)境中被正確地加鎖同步,如果多個(gè)線程同 時(shí)去初始化一個(gè)類,那么只會(huì)有其中一個(gè)線程去執(zhí)行這個(gè)類的<clinit>()方法,其他線程都需要阻塞等 待,直到活動(dòng)線程執(zhí)行完畢<clinit>()方法。如果在一個(gè)類的<clinit>()方法中有耗時(shí)很長的操作,那就 可能造成多個(gè)進(jìn)程阻塞,在實(shí)際應(yīng)用中這種阻塞往往是很隱蔽的。代碼如下演示了這種場景。

public class Test {
    static class DeadLoopClass {
        static {
            // 如果不加上這個(gè)if語句,編譯器將提示“Initializer does not complete normally” 并拒絕編譯
            if (true) {
                System.out.println(Thread.currentThread() + "init DeadLoopClass");
                while (true) {
                }
            }
        }
    }

    public static void main(String[] args) {
        Runnable script = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread() + "start");
                DeadLoopClass dlc = new DeadLoopClass();
                System.out.println(Thread.currentThread() + " run over");
            }
        };
        Thread thread1 = new Thread(script);
        Thread thread2 = new Thread(script);
        thread1.start();
        thread2.start();
    }
}

運(yùn)行結(jié)果:

需要注意,其他線程雖然會(huì)被阻塞,但如果執(zhí)行<clinit>()方法的那條線程退出

<clinit>()方法后,其他線程喚醒后則不會(huì)再次進(jìn)入<clinit>()方法。同一個(gè)類加載器下,一個(gè)類型只會(huì)被初始化一 次。

四、父類和子類初始化過程中的執(zhí)行順序

代碼示例:

public class InitDemo {
		public static void main(String[] args) {
			System.out.println("第一次實(shí)例化子類:");
			new sub();
			System.out.println("第二次實(shí)例化子類:");
			new sub();
		}	
}

class Super{
	static {
		System.out.println("父類中的靜態(tài)塊");
	}
	{
		System.out.println("父類中的非靜態(tài)塊");
	}
	Super(){
		System.out.println("父類中的構(gòu)造方法");
	}
}

class sub extends Super{
	static {
		System.out.println("子類中的靜態(tài)塊");
	}
	{
		System.out.println("子類中的非靜態(tài)塊");
	}
	sub(){
		System.out.println("子類中的構(gòu)造方法");
	}
}

運(yùn)行結(jié)果:

五、類加載器

Java虛擬機(jī)設(shè)計(jì)團(tuán)隊(duì)有意把類加載階段中的“通過一個(gè)類的全限定名來獲取描述該類的二進(jìn)制字節(jié) 流”這個(gè)動(dòng)作放到Java虛擬機(jī)外部去實(shí)現(xiàn),以便讓應(yīng)用程序自己決定如何去獲取所需的類。實(shí)現(xiàn)這個(gè)動(dòng) 作的代碼被稱為“類加載器”(Class Loader)。

5.1 類與類加載器

對于 任意一個(gè)類,都必須由加載它的類加載器和這個(gè)類本身一起共同確立其在Java虛擬機(jī)中的唯一性,兩個(gè)類來源于同一個(gè) Class文件,被同一個(gè)Java虛擬機(jī)加載,只要加載它們的類加載器不同,那這兩個(gè)類就必定不相等。

這里所指的“相等”,包括代表類的Class對象的equals()方法、isAssignableFrom()方法、isInstance() 方法的返回結(jié)果,也包括了使用instanceof關(guān)鍵字做對象所屬關(guān)系判定等各種情況。如果沒有注意到類 加載器的影響,在某些情況下可能會(huì)產(chǎn)生具有迷惑性的結(jié)果,如下代碼演示了不同的類加載器對 instanceof關(guān)鍵字運(yùn)算的結(jié)果的影響。

代碼示例:

package com.gzl.cn;

import java.io.IOException;
import java.io.InputStream;

public class ClassLoaderTest {
    public static void main(String[] args) throws Exception {
        ClassLoader myLoader = new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                try {
                    String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
                    InputStream is = getClass().getResourceAsStream(fileName);
                    if (is == null) {
                        return super.loadClass(name);
                    }
                    byte[] b = new byte[is.available()];
                    is.read(b);
                    return defineClass(name, b, 0, b.length);
                } catch (IOException e) {
                    throw new ClassNotFoundException(name);
                }
            }
        };
        Object obj = myLoader.loadClass("com.gzl.cn.ClassLoaderTest").newInstance();
        System.out.println(obj.getClass());
        System.out.println(obj instanceof com.gzl.cn.ClassLoaderTest);
    }
}

上面示例當(dāng)中構(gòu)造了一個(gè)簡單的類加載器,盡管它極為簡陋,但是對于這個(gè)演示來說已經(jīng)足夠。它可以加載與自己在同一路徑下的Class文件,我們使用這個(gè)類加載器去加載了一個(gè)名 為“com.gzl.cn.ClassLoaderTest”的類,并實(shí)例化了這個(gè)類的對象。但在第二行的輸出中卻發(fā)現(xiàn)這個(gè)對象與類com.gzl.cn.ClassLoaderTest做所屬 類型檢查的時(shí)候返回了false。

這是因?yàn)镴ava虛擬機(jī)中同時(shí)存在了兩個(gè)ClassLoaderTest類,一個(gè)是由虛擬 機(jī)的應(yīng)用程序類加載器所加載的,另外一個(gè)是由我們自定義的類加載器加載的,雖然它們都來自同一 個(gè)Class文件,但在Java虛擬機(jī)中仍然是兩個(gè)互相獨(dú)立的類,做對象所屬類型檢查時(shí)的結(jié)果自然為 false。

注意:同一個(gè)類加載器下,一個(gè)類型只會(huì)被初始化一 次。

5.2 雙親委派模型

站在Java虛擬機(jī)的角度來看,只存在兩種不同的類加載器:

1.一種是啟動(dòng)類加載器(Bootstrap ClassLoader),這個(gè)類加載器使用C++語言實(shí)現(xiàn),是虛擬機(jī)自身的一部分;

2.另外一種就是由Java語言實(shí)現(xiàn)的類加載器,獨(dú)立存在于虛擬機(jī)外部,并且全都繼承自抽象類 java.lang.ClassLoader。

站在Java開發(fā)人員的角度來看,類加載器就應(yīng)當(dāng)劃分得更細(xì)致一些。自JDK 1.2以來,Java一直保 持著三層類加載器、雙親委派的類加載架構(gòu)。

下圖中展示的各種類加載器之間的層次關(guān)系被稱為類加載器的“雙親委派模型(Parents Delegation Model)”。雙親委派模型要求除了頂層的啟動(dòng)類加載器外,其余的類加載器都應(yīng)有自己的父類加載器。不過這里類加載器之間的父子關(guān)系一般不是以繼承(Inheritance)的關(guān)系來實(shí)現(xiàn)的,而是通常使用 組合(Composition)關(guān)系來復(fù)用父加載器的代碼。

雙親委派模型的工作過程是:

如果一個(gè)類加載器收到了類加載的請求,它首先不會(huì)自己去嘗試加 載這個(gè)類,而是把這個(gè)請求委派給父類加載器去完成,每一個(gè)層次的類加載器都是如此,因此所有的 加載請求最終都應(yīng)該傳送到最頂層的啟動(dòng)類加載器中,只有當(dāng)父加載器反饋?zhàn)约簾o法完成這個(gè)加載請 求(它的搜索范圍中沒有找到所需的類)時(shí),子加載器才會(huì)嘗試自己去完成加載。

為什么使用雙親委派模型?

簡單的來說:一個(gè)是安全性,另一個(gè)就是性能;(避免重復(fù)加載 和 避免核心類被篡改)

用戶自定義一個(gè)java.lang.String類,該String類具有系統(tǒng)的String類一樣的功能,只是在某個(gè)函數(shù)稍作修改。比如equals函數(shù),這個(gè)函數(shù)經(jīng)常使用,如果在這這個(gè)函數(shù)中,黑客加入一些“病毒代碼”。并且通過自定義類加載器加入到JVM中。此時(shí),如果沒有雙親委派模型,那么JVM就可能誤以為黑客自定義的java.lang.String類是系統(tǒng)的String類,導(dǎo)致“病毒代碼”被執(zhí)行。

而有了雙親委派模型,黑客自定義的java.lang.String類永遠(yuǎn)都不會(huì)被加載進(jìn)內(nèi)存。因?yàn)槭紫仁亲铐敹说念惣虞d器加載系統(tǒng)的java.lang.String類,最終自定義的類加載器無法加載java.lang.String類。

雙親委派模型對于保證Java程序的穩(wěn)定運(yùn)作極為重要,但它的實(shí)現(xiàn)卻異常簡單,用以實(shí)現(xiàn)雙親委 派的代碼只有短短十余行,全部集中在java.lang.ClassLoader的loadClass()方法之中,代碼如下:

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // 首先,檢查請求的類是否已經(jīng)被加載過了
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // 如果父類加載器拋出ClassNotFoundException 
                // 說明父類加載器無法完成加載請求,那就繼續(xù)往下走
            }

            if (c == null) {
                // 在父類加載器無法加載時(shí) 
                // 再調(diào)用本身的findClass方法來進(jìn)行類加載
                long t1 = System.nanoTime();
                c = findClass(name);

                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

啟動(dòng)類加載器(Bootstrap Class Loader):引導(dǎo)類加載器,又叫啟動(dòng)類加載器。這個(gè)類加載器負(fù)責(zé)加載存放在 <JAVA_HOME>\lib目錄,或者被-Xbootclasspath參數(shù)所指定的路徑中存放的,而且是Java虛擬機(jī)能夠 識(shí)別的(按照文件名識(shí)別,如rt.jar、tools.jar,名字不符合的類庫即使放在lib目錄中也不會(huì)被加載)類庫加載到虛擬機(jī)的內(nèi)存中。

擴(kuò)展類加載器(Extension Class Loader):

Java語言編寫,由sun.misc.Launcher類的內(nèi)部類ExtClassLoader類實(shí)現(xiàn),派生于ClassLoader類,父加載器為引導(dǎo)類加載器。

從java.ext.dirs系統(tǒng)屬性所指定的目錄中加載類庫,或從JDK的安裝目錄的jre/lib/ext子目錄(擴(kuò)展目錄)下加載類庫。如果用戶創(chuàng)建的jar包放在此目錄下,也會(huì)自動(dòng)由擴(kuò)展類加載器加載。

應(yīng)用程序類加載器(Application Class Loader):

這個(gè)類加載器由 sun.misc.Launcher$AppClassLoader來實(shí)現(xiàn)。由于應(yīng)用程序類加載器是ClassLoader類中的getSystemClassLoader()方法的返回值,所以有些場合中也稱它為“系統(tǒng)類加載器”。

它負(fù)責(zé)加載用戶類路徑 (ClassPath)上所有的類庫,開發(fā)者同樣可以直接在代碼中使用這個(gè)類加載器。如果應(yīng)用程序中沒有 自定義過自己的類加載器,一般情況下這個(gè)就是程序中默認(rèn)的類加載器。

5.3 破壞雙親委派模型

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

這里所說的破壞指的是沒有遵循自下而上的規(guī)則。也就是剛剛提到的雙親委派模型的工作過程。

第一次破壞:

在 jdk 1.2 之前,那時(shí)候還沒有雙親委派模型,不過已經(jīng)有了 ClassLoader 這個(gè)抽象類,所以已經(jīng)有人繼承這個(gè)抽象類,重寫 loadClass 方法來實(shí)現(xiàn)用戶自定義類加載器。

而在 1.2 的時(shí)候要引入雙親委派模型,為了向前兼容, loadClass 這個(gè)方法還得保留著使之得以重寫,新搞了個(gè) findClass 方法讓用戶去重寫,并呼吁大家不要重寫 loadClass 只要重寫 findClass。

這就是第一次對雙親委派模型的破壞,因?yàn)殡p親委派的邏輯在 loadClass 上,但是又允許重寫 loadClass,重寫了之后就可以破壞委派邏輯了。

第二次破壞:

第二次破壞指的是 JNDI、JDBC 之類的情況。

首先得知道什么是 SPI(Service Provider Interface),它是面向拓展的,也就是說我定義了個(gè)規(guī)矩,就是 SPI ,具體如何實(shí)現(xiàn)由擴(kuò)展者實(shí)現(xiàn)。

像我們比較熟的 JDBC 就是如此。

MySQL 有 MySQL 的 JDBC 實(shí)現(xiàn),Oracle 有 Oracle 的 JDBC 實(shí)現(xiàn),我 Java 不管你內(nèi)部如何實(shí)現(xiàn)的,反正你們這些數(shù)據(jù)庫廠商都得統(tǒng)一按我這個(gè)來,這樣我們 Java 開發(fā)者才能容易的調(diào)用數(shù)據(jù)庫操作,所以在 Java 核心包里面定義了這個(gè) SPI。

而核心包里面的類都是由啟動(dòng)類加載器去加載的,但它的手只能摸到<JAVA_HOME>\lib或Xbootclasspath指定的路徑中,其他的它鞭長莫及。

而 JDBC 的實(shí)現(xiàn)類在我們用戶定義的 classpath 中,只能由應(yīng)用類加載器去加載,所以啟動(dòng)類加載器只能委托子類來加載數(shù)據(jù)庫廠商們提供的具體實(shí)現(xiàn),這就違反了自下而上的委托機(jī)制。

具體解決辦法是搞了個(gè)線程上下文類加載器 (Thread Context ClassLoader)。這個(gè)類加載器可以通過java.lang.Thread類的setContext-ClassLoader()方 法進(jìn)行設(shè)置,如果創(chuàng)建線程時(shí)還未設(shè)置,它將會(huì)從父線程中繼承一個(gè),如果在應(yīng)用程序的全局范圍內(nèi) 都沒有設(shè)置過的話,那這個(gè)類加載器默認(rèn)就是應(yīng)用程序類加載器。

如下圖就是JDBC加載驅(qū)動(dòng)當(dāng)中用到的上下文類加載器。

其實(shí)這里說的打破雙親規(guī)則,就是說用來加載spi實(shí)現(xiàn)用的是線程中的加載器(其實(shí)就是AppclassLoader),當(dāng)加載spi實(shí)現(xiàn)類時(shí)就沒有繼續(xù)調(diào)用父類加載器了,因?yàn)樗栏割惥褪钦也坏讲耪宜ゼ虞d的,說這里違反了雙親規(guī)則。

第三次破壞:

這次破壞是為了滿足熱部署的需求,不停機(jī)更新這對企業(yè)來說至關(guān)重要,畢竟停機(jī)是大事。

OSGI 就是利用自定義的類加載器機(jī)制來完成模塊化熱部署,而它實(shí)現(xiàn)的類加載機(jī)制就沒有完全遵循自下而上的委托,有很多平級(jí)之間的類加載器查找,具體就不展開了,有興趣可以自行研究一下。

六、Java模塊化系統(tǒng)

在JDK 9中引入的Java模塊化系統(tǒng)(Java Platform Module System,JPMS)是對Java技術(shù)的一次重 要升級(jí),此前,如果類路徑中缺失了運(yùn)行時(shí)依賴的類型,那就只能等程序運(yùn)行到發(fā)生該類型的加載、鏈接 時(shí)才會(huì)報(bào)出運(yùn)行的異常。而在JDK 9以后,如果啟用了模塊化進(jìn)行封裝,模塊就可以聲明對其他模塊 的顯式依賴,這樣Java虛擬機(jī)就能夠在啟動(dòng)時(shí)驗(yàn)證應(yīng)用程序開發(fā)階段設(shè)定好的依賴關(guān)系在運(yùn)行期是否 完備,如有缺失那就直接啟動(dòng)失敗,從而避免了很大一部分由于類型依賴而引發(fā)的運(yùn)行時(shí)異常。

JDK 9中 的public類型不再意味著程序的所有地方的代碼都可以隨意訪問到它們,模塊提供了更精細(xì)的可訪問性 控制,必須明確聲明其中哪一些public的類型可以被其他哪一些模塊訪問,這種訪問控制也主要是在類 加載過程中完成的。

其次是是擴(kuò)展類加載器(Extension Class Loader)被平臺(tái)類加載器(Platform Class Loader)取代。由于咱們一時(shí)半會(huì)也不會(huì)用JDK9,更多具體的我就不整理了。

以上就是一文詳解Java中的類加載機(jī)制的詳細(xì)內(nèi)容,更多關(guān)于Java類加載機(jī)制的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • java單機(jī)接口限流處理方案詳解

    java單機(jī)接口限流處理方案詳解

    這篇文章主要為大家詳細(xì)介紹了java單機(jī)接口限流處理方案,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2021-11-11
  • Java四位電話號(hào)碼的加密方法

    Java四位電話號(hào)碼的加密方法

    這篇文章主要為大家詳細(xì)介紹了Java四位電話號(hào)碼的加密方法,數(shù)據(jù)是四位的整數(shù),在傳遞過程中進(jìn)行加密,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2017-07-07
  • 如何更好的使用Java8中方法引用詳解

    如何更好的使用Java8中方法引用詳解

    在Java8中,我們可以直接通過方法引用來簡寫lambda表達(dá)式中已經(jīng)存在的方法,這種特性就叫做方法引用(Method Reference)。下面這篇文章主要給大家介紹了關(guān)于如何更好的使用Java8中方法引用的相關(guān)資料,需要的朋友可以參考下。
    2017-09-09
  • JAVA并發(fā)中VOLATILE關(guān)鍵字的神奇之處詳解

    JAVA并發(fā)中VOLATILE關(guān)鍵字的神奇之處詳解

    這篇文章主要給大家介紹了關(guān)于JAVA并發(fā)中VOLATILE關(guān)鍵字的神奇之處的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2021-05-05
  • Java利用Strategy模式實(shí)現(xiàn)堆排序

    Java利用Strategy模式實(shí)現(xiàn)堆排序

    策略設(shè)計(jì)模式(Strategy):可以整體的替換一個(gè)算法的實(shí)現(xiàn)部分,能夠整體的替換算法,能讓我們輕松地用不同方法解決同一個(gè)問題。本文將利用Strategy模式實(shí)現(xiàn)堆排序,感興趣的可以學(xué)習(xí)一下
    2022-09-09
  • java使用for循環(huán)輸出楊輝三角

    java使用for循環(huán)輸出楊輝三角

    楊輝三角形由數(shù)字排列,可以把它看做一個(gè)數(shù)字表,其基本特性是兩側(cè)數(shù)值均為1,其他位置的數(shù)值是其正上方的數(shù)字與左上角數(shù)值之和,下面是java使用for循環(huán)輸出包括10行在內(nèi)的楊輝三角形
    2014-02-02
  • Java OOM原因以及解決方案

    Java OOM原因以及解決方案

    這篇文章主要介紹了Java OOM原因以及解決方案,本篇文章通過簡要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下
    2021-09-09
  • 詳解在Java中如何優(yōu)雅的停止線程

    詳解在Java中如何優(yōu)雅的停止線程

    線程,作為并發(fā)編程的基礎(chǔ)單元,允許程序同時(shí)執(zhí)行多個(gè)任務(wù),在Java中,線程可以理解為程序中的獨(dú)立執(zhí)行路徑,通過使用線程,開發(fā)者可以創(chuàng)建更加響應(yīng)靈敏、效率更高的應(yīng)用程序,本文小編將給大家介紹一下Java中如何優(yōu)雅的停止線程,需要的朋友可以參考下
    2023-11-11
  • Java import static及import原理區(qū)別解析

    Java import static及import原理區(qū)別解析

    這篇文章主要介紹了Java import static及import原理區(qū)別解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2020-10-10
  • JAVA實(shí)現(xiàn)用戶抽獎(jiǎng)功能(附完整代碼)

    JAVA實(shí)現(xiàn)用戶抽獎(jiǎng)功能(附完整代碼)

    這篇文章主要給大家介紹了關(guān)于JAVA實(shí)現(xiàn)用戶抽獎(jiǎng)功能的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2020-11-11

最新評(píng)論