最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

深入了解Java中的類(lèi)加載機(jī)制

 更新時(shí)間:2022年11月15日 15:15:01   作者:夕陽(yáng)醉了  
通常,在關(guān)于Java的類(lèi)加載部分會(huì)遇到以上疑問(wèn),本文將對(duì)類(lèi)加載重要部分做詳細(xì)介紹,包括重要的基礎(chǔ)概念和應(yīng)用場(chǎng)景,在編寫(xiě)過(guò)程中也幫助作者重新熟悉并加固了知識(shí)點(diǎn),希望在看完后對(duì)大家能有所幫助

一、類(lèi)加載過(guò)程

程序員編寫(xiě)的Java源程序(.java文件)在經(jīng)過(guò)編譯器編譯之后被轉(zhuǎn)換成字節(jié)代碼(.class 文件),類(lèi)加載器將.class文件中的二進(jìn)制數(shù)據(jù)讀入到內(nèi)存中,將其放在方法區(qū)內(nèi),然后在堆區(qū)創(chuàng)建一個(gè)java.lang.Class對(duì)象,用來(lái)封裝類(lèi)在方法區(qū)內(nèi)的數(shù)據(jù)結(jié)構(gòu)。

類(lèi)加載的最終產(chǎn)品是位于堆區(qū)中的Class對(duì)象,Class對(duì)象封裝了類(lèi)在方法區(qū)內(nèi)的數(shù)據(jù)結(jié)構(gòu),并且向Java程序員提供了訪問(wèn)方法區(qū)內(nèi)的數(shù)據(jù)結(jié)構(gòu)的接口。

以下是舉例說(shuō)明類(lèi)加載過(guò)程:

二、類(lèi)生命周期

類(lèi)的生命周期包括:加載、驗(yàn)證、準(zhǔn)備、解析、初始化、使用、卸載7個(gè)階段。其中加載、驗(yàn)證、準(zhǔn)備、初始化、卸載5個(gè)階段是按照這種順序按部就班的開(kāi)始,而解析階段則不一定:某些情況下,可以在初始化之后再開(kāi)始,這是為了支持Java語(yǔ)言的運(yùn)行時(shí)綁定(也稱(chēng)為動(dòng)態(tài)綁定或晚期綁定,其實(shí)就是多態(tài)),例如子類(lèi)重寫(xiě)父類(lèi)方法。

注意:這里寫(xiě)的是按部就班的開(kāi)始,而不是按部就班地進(jìn)行或完成,因?yàn)檫@些階段通常都是互相交叉混合式進(jìn)行的,通常會(huì)在一個(gè)階段執(zhí)行過(guò)程中調(diào)用、激活另外一個(gè)階段。

1、加載

加載階段會(huì)做3件事情:

  • 通過(guò)一個(gè)類(lèi)的全限定名來(lái)獲取定義此類(lèi)的二進(jìn)制字節(jié)流。
  • 將這個(gè)字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)。
  • 在Java堆中生成一個(gè)代表這個(gè)類(lèi)的java.lang.Class對(duì)象,作為對(duì)方法區(qū)中這些數(shù)據(jù)的訪問(wèn)入口。

此處第一點(diǎn)并沒(méi)指明要從哪里獲取、怎樣獲取,因此這里給開(kāi)發(fā)人員預(yù)留了擴(kuò)展空間。許多Java技術(shù)就建立在此基礎(chǔ)上,例如:

  • 從ZIP包讀取,如JAR、WAR。
  • 從網(wǎng)絡(luò)中獲取,這種場(chǎng)景最典型應(yīng)用場(chǎng)景應(yīng)用就是Applet。
  • 運(yùn)行時(shí)計(jì)算生成,使用較多場(chǎng)景是動(dòng)態(tài)代理技術(shù),如spring AOP。

加載階段完成后,虛擬機(jī)外部的二進(jìn)制字節(jié)流就按照虛擬機(jī)所需的格式存儲(chǔ)在方法區(qū)之中,而且在Java堆中也創(chuàng)建一個(gè)java.lang.Class類(lèi)的對(duì)象,這樣便可以通過(guò)該對(duì)象訪問(wèn)方法區(qū)中的這些數(shù)據(jù)。

2、驗(yàn)證

確保被加載的類(lèi)的正確性,分為4個(gè)驗(yàn)證階段:

  • 文件格式驗(yàn)證
  • 元數(shù)據(jù)驗(yàn)證
  • 字節(jié)碼驗(yàn)證
  • 符號(hào)引用驗(yàn)證

驗(yàn)證階段非常重要的,但不是必須的,它對(duì)程序運(yùn)行期沒(méi)有影響,如果所引用的類(lèi)經(jīng)過(guò)反復(fù)驗(yàn)證,那么可以考慮采用-Xverifynone參數(shù)來(lái)關(guān)閉大部分的類(lèi)驗(yàn)證措施,以縮短虛擬機(jī)類(lèi)加載的時(shí)間。

3、準(zhǔn)備

為類(lèi)的靜態(tài)變量分配內(nèi)存,并初始化默認(rèn)值,這些內(nèi)存是在方法區(qū)中分配,需要注意以下幾點(diǎn):

  • 此處內(nèi)存分配的變量?jī)H包含類(lèi)變量(static),而不包括實(shí)例變量,實(shí)例變量會(huì)隨著對(duì)象實(shí)例化被分配在java堆中。
  • 這里默認(rèn)值是數(shù)據(jù)類(lèi)型的默認(rèn)值(如0、0L、null、false),而不是代碼中被顯示的賦予的值。
  • 如果類(lèi)字段的字段屬性表中存在ConstatntValue屬性,即同時(shí)被final和static修飾,那么在準(zhǔn)備階段變量value就會(huì)被初始化為ConstValue屬性所指定的值。

4、解析

解析階段是虛擬機(jī)將常量池內(nèi)的符號(hào)引用替換為直接引用的過(guò)程,解析動(dòng)作主要針對(duì)類(lèi)或接口、字段、類(lèi)方法、接口方法、方法類(lèi)型、方法句柄和調(diào)用點(diǎn)限定符7類(lèi)符號(hào)引用進(jìn)行。符號(hào)引用就是一組符號(hào)來(lái)描述目標(biāo),可以是任何字面量。

直接引用就是直接指向目標(biāo)的指針、相對(duì)偏移量或一個(gè)間接定位到目標(biāo)的句柄。

5、初始化

為類(lèi)的靜態(tài)變量賦予正確的初始值,JVM負(fù)責(zé)對(duì)類(lèi)進(jìn)行初始化,主要對(duì)類(lèi)變量進(jìn)行初始化。初始化階段是執(zhí)行類(lèi)構(gòu)造器<client>()方法的過(guò)程。

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

public class Test{
    static{
        i=0;
        System.out.print(i);
    }
    static int i=1;
}

<clinit>()方法與類(lèi)構(gòu)造函數(shù)不一樣,不需要顯示調(diào)用父類(lèi)構(gòu)造函數(shù),虛擬機(jī)會(huì)保證在子類(lèi)的<clinit>()方法執(zhí)行之前,父類(lèi)的<clinit>()方法已執(zhí)行完畢。

由于父類(lèi)的<clinit>()方法首先執(zhí)行,意味著父類(lèi)中的靜態(tài)語(yǔ)句塊要優(yōu)先于子類(lèi)的變量賦值操作,如下所示,最終得出的值是2,而不是1。

public class TestClassLoader {
    public static int A = 1;
    static {
        A = 2;
//        System.out.println(A);
    }
    
    static class Sub extends TestClassLoader {
        public static int B = A;
    }
    
    public static void main(String[] args) {
        System.out.println(Sub.B);
    }
}

<clinit>()方法對(duì)于類(lèi)和接口來(lái)說(shuō),并不是必須的,若類(lèi)沒(méi)有靜態(tài)語(yǔ)句塊,也沒(méi)有對(duì)變量賦值操作,則不會(huì)生成<clinit>()方法。

接口與類(lèi)不同的是,接口不需要先執(zhí)行父類(lèi)的<clinit>()方法,只有父接口定義的變量使用時(shí),父接口才會(huì)被初始化。另外接口的實(shí)現(xiàn)類(lèi)也不會(huì)先執(zhí)行接口的<clinit>()方法。

虛擬機(jī)保證當(dāng)多線程去初始化類(lèi)時(shí),只會(huì)有一個(gè)線程去執(zhí)行<clinit>()方法,而其他線程則被阻塞。

<clinit>()方法和<init>()方法區(qū)別:

執(zhí)行時(shí)機(jī)不同:init方法是對(duì)象構(gòu)造器方法,在new一個(gè)對(duì)象并調(diào)用該對(duì)象的constructor方法時(shí)才會(huì)執(zhí)行。clinit方法是類(lèi)構(gòu)造器方法,是在JVM加載期間的初始化階段才會(huì)調(diào)用。

執(zhí)行目的不同:init是對(duì)非靜態(tài)變量解析初始化,而clinit是對(duì)靜態(tài)變量,靜態(tài)代碼塊進(jìn)行初始化。

三、雙親委派機(jī)制

在介紹雙親委派機(jī)制前,先來(lái)看下類(lèi)加載器的層次關(guān)系圖,如下:

  • 啟動(dòng)類(lèi)加載器(Bootstrap ClassLoader),負(fù)責(zé)加載存放在$JAVA_HOME\jre\lib下,或被-Xbootclasspath參數(shù)指定的路徑中的,并且能被虛擬機(jī)識(shí)別的類(lèi)庫(kù)(如rt.jar,所有的java.*開(kāi)頭的類(lèi)均被Bootstrap ClassLoader加載)。啟動(dòng)類(lèi)加載器是無(wú)法被Java程序直接引用的。
  • 擴(kuò)展類(lèi)加載器(Extension ClassLoader),該加載器由sun.misc.Launcher$ExtClassLoader實(shí)現(xiàn),它負(fù)責(zé)加載$JAVA_HOME\jre\lib\ext目錄中,或者由java.ext.dirs系統(tǒng)變量指定的路徑中的所有類(lèi)庫(kù)(如javax.*開(kāi)頭的類(lèi)),開(kāi)發(fā)者可以直接使用擴(kuò)展類(lèi)加載器。
  • 應(yīng)用程序類(lèi)加載器(Application ClassLoader),該類(lèi)加載器由sun.misc.Launcher$AppClassLoader來(lái)實(shí)現(xiàn),它負(fù)責(zé)加載用戶(hù)類(lèi)路徑(ClassPath)所指定的類(lèi),開(kāi)發(fā)者可以直接使用該類(lèi)加載器,如果應(yīng)用程序中沒(méi)有自定義過(guò)自己的類(lèi)加載器,一般情況下這個(gè)就是程序中默認(rèn)的類(lèi)加載器。
  • 自定義類(lèi)加載器(User ClassLoader),如果有必要,我們還可以加入自定義的類(lèi)加載器。因?yàn)镴VM自帶的ClassLoader只是懂得從本地文件系統(tǒng)加載標(biāo)準(zhǔn)的java class文件。

雙親委派機(jī)制是指如果一個(gè)類(lèi)加載器收到了類(lèi)加載的請(qǐng)求,它首先不會(huì)自己去嘗試加載這個(gè)類(lèi),而是把請(qǐng)求委托給父加載器去完成,依次向上,因此,所有的類(lèi)加載請(qǐng)求最終都應(yīng)該被傳遞到頂層的啟動(dòng)類(lèi)加載器中,只有當(dāng)父加載器在它的搜索范圍中沒(méi)有找到所需的類(lèi)時(shí),即無(wú)法完成該加載,子加載器才會(huì)嘗試自己去加載該類(lèi)。

為了更清楚的了解雙親委派機(jī)制,我們來(lái)看下jdk1.8源碼java.lang.ClassLoader.loadClass()方法實(shí)現(xiàn):

    public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }

    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            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 thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    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;
        }
    }

上面代碼注釋寫(xiě)的很清楚,首先調(diào)用findLoadedClass方法檢查是否已加載過(guò)這個(gè)類(lèi),如果沒(méi)有就調(diào)用parent的loadClass方法,從底層一級(jí)級(jí)往上。如果所有ClassLoader都沒(méi)有加載過(guò)這個(gè)類(lèi),就調(diào)用findClass方法查找這個(gè)類(lèi),然后又從頂層逐級(jí)向下調(diào)用findClass方法,最終都沒(méi)找到就拋出ClassNotFoundException。這樣設(shè)計(jì)的目的是保證安全性,防止系統(tǒng)類(lèi)被偽造。

為了便于理解,以下是加載邏輯示意圖:

四、自定義類(lèi)加載器的應(yīng)用

自定義類(lèi)加載器通常有以下四種應(yīng)用場(chǎng)景:

  • 源代碼加密,防止源碼泄露
  • 隔離加載類(lèi),采用隔離加載,防止依賴(lài)沖突。
  • 修改類(lèi)加載的方式。
  • 擴(kuò)展加載源。

1、源代碼加密

源代碼加密的本質(zhì)是對(duì)字節(jié)碼文件進(jìn)行操作。我們可以在打包的時(shí)候?qū)lass進(jìn)行加密操作,然后在加載class文件之前通過(guò)自定義classloader先進(jìn)行解密操作,然后再按照標(biāo)準(zhǔn)的class文件標(biāo)準(zhǔn)進(jìn)行加載,這樣就完成了class文件正常的加載。因此這個(gè)加密的jar包只有能夠?qū)崿F(xiàn)解密方法的classloader才能正常加載。

2、隔離加載類(lèi)

我們常常遇到頭疼的事情就是jar包版本的依賴(lài)沖突,寫(xiě)代碼五分鐘,排包一整天。

舉個(gè)栗子:

工程里面同時(shí)引入了 A、B 兩個(gè) jar 包,以及 C 的 v0.1、v0.2 版本,v2 版本的 Log 類(lèi)比 v1 版本新增了 error 方法,,打包的時(shí)候 maven 只能選擇 C 的一個(gè)版本,假設(shè)選擇了 v1 版本。到了運(yùn)行的時(shí)候,默認(rèn)情況下一個(gè)項(xiàng)目的所有類(lèi)都是用同一個(gè)類(lèi)加載器加載的,所以不管你依賴(lài)了多少個(gè)版本的 C,最終只會(huì)有一個(gè)版本的 C 被加載到 JVM 中。當(dāng) B 要去訪問(wèn) Log.error,就會(huì)發(fā)現(xiàn) Log 壓根就沒(méi)有 error 方法,然后就拋異常 java.lang.NoSuchMethodError。這就是類(lèi)沖突的一個(gè)典型案例。

類(lèi)隔離技術(shù)就是用來(lái)解決這個(gè)問(wèn)題。讓不同模塊的 jar 包用不同的類(lèi)加載器加載。

JVM 提供了一種非常簡(jiǎn)單有效的方式,我把它稱(chēng)為類(lèi)加載傳導(dǎo)規(guī)則:JVM 會(huì)選擇當(dāng)前類(lèi)的類(lèi)加載器來(lái)加載所有該類(lèi)的引用的類(lèi)。例如我們定義了 TestA 和 TestB 兩個(gè)類(lèi),TestA 會(huì)引用 TestB,只要我們使用自定義的類(lèi)加載器加載 TestA,那么在運(yùn)行時(shí),當(dāng) TestA 調(diào)用到 TestB 的時(shí)候,TestB 也會(huì)被 JVM 使用 TestA 的類(lèi)加載器加載。依此類(lèi)推,只要是 TestA 及其引用類(lèi)關(guān)聯(lián)的所有 jar 包的類(lèi)都會(huì)被自定義類(lèi)加載器加載。通過(guò)這種方式,我們只要讓模塊的 main 方法類(lèi)使用不同的類(lèi)加載器加載,那么每個(gè)模塊的都會(huì)使用 main 方法類(lèi)的類(lèi)加載器加載的,這樣就能讓多個(gè)模塊分別使用不同類(lèi)加載器。這也是 OSGi 和 SofaArk 能夠?qū)崿F(xiàn)類(lèi)隔離的核心原理。

3、熱加載/熱部署

在應(yīng)用運(yùn)行的時(shí)升級(jí)軟件,無(wú)需重新啟動(dòng)的方式有兩種,熱部署和熱加載。

對(duì)于Java應(yīng)用程序來(lái)說(shuō),熱部署就是在服務(wù)器運(yùn)行時(shí)重新部署項(xiàng)目,熱加載即在運(yùn)行時(shí)重新加載class,從而升級(jí)應(yīng)用。

熱加載可以概括為在容器啟動(dòng)的時(shí)候起一條后臺(tái)線程,定時(shí)的檢測(cè)類(lèi)文件的時(shí)間戳變化,如果類(lèi)的時(shí)間戳變掉了,則將類(lèi)重新載入。對(duì)比反射機(jī)制,反射是在運(yùn)行時(shí)獲取類(lèi)信息,通過(guò)動(dòng)態(tài)的調(diào)用來(lái)改變程序行為。而熱加載則是在運(yùn)行時(shí)通過(guò)重新加載改變類(lèi)信息,直接改變程序行為。

熱部署原理類(lèi)似,但它是直接重新加載整個(gè)應(yīng)用,這種方式會(huì)釋放內(nèi)存,比熱加載更加干凈徹底,但同時(shí)也更費(fèi)時(shí)間。

4、擴(kuò)展加載源

字節(jié)碼文件可以從數(shù)據(jù)庫(kù)、網(wǎng)絡(luò)、移動(dòng)設(shè)備、甚至是電視機(jī)機(jī)頂盒進(jìn)行加載,可以與源代碼加密方式搭配使用。比如部分關(guān)鍵代碼可以通過(guò)移動(dòng)U盤(pán)讀取再加載到JVM。

到此這篇關(guān)于深入了解Java中的類(lèi)加載機(jī)制的文章就介紹到這了,更多相關(guān)Java類(lèi)加載機(jī)制內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評(píng)論

通山县| 景宁| 沂水县| 海阳市| 瑞昌市| 台北市| 陕西省| 佛山市| 于田县| 吴江市| 抚州市| 大理市| 海盐县| 花莲市| 嘉兴市| 临高县| 麻城市| 泗水县| 海伦市| 万年县| 将乐县| 吕梁市| 新平| 合川市| 沙坪坝区| 额尔古纳市| 原平市| 洱源县| 天长市| 益阳市| 吉林市| 南乐县| 敖汉旗| 乐平市| 余庆县| 惠来县| 新和县| 木兰县| 政和县| 荣昌县| 两当县|