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

深入了解Java中的類加載機制

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

一、類加載過程

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

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

以下是舉例說明類加載過程:

二、類生命周期

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

注意:這里寫的是按部就班的開始,而不是按部就班地進行或完成,因為這些階段通常都是互相交叉混合式進行的,通常會在一個階段執(zhí)行過程中調(diào)用、激活另外一個階段。

1、加載

加載階段會做3件事情:

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

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

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

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

2、驗證

確保被加載的類的正確性,分為4個驗證階段:

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

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

3、準(zhǔn)備

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

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

4、解析

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

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

5、初始化

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

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

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

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

由于父類的<clinit>()方法首先執(zhí)行,意味著父類中的靜態(tài)語句塊要優(yōu)先于子類的變量賦值操作,如下所示,最終得出的值是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>()方法對于類和接口來說,并不是必須的,若類沒有靜態(tài)語句塊,也沒有對變量賦值操作,則不會生成<clinit>()方法。

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

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

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

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

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

三、雙親委派機制

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

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

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

為了更清楚的了解雙親委派機制,我們來看下jdk1.8源碼java.lang.ClassLoader.loadClass()方法實現(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;
        }
    }

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

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

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

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

  • 源代碼加密,防止源碼泄露
  • 隔離加載類,采用隔離加載,防止依賴沖突。
  • 修改類加載的方式。
  • 擴展加載源。

1、源代碼加密

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

2、隔離加載類

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

舉個栗子:

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

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

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

3、熱加載/熱部署

在應(yīng)用運行的時升級軟件,無需重新啟動的方式有兩種,熱部署和熱加載。

對于Java應(yīng)用程序來說,熱部署就是在服務(wù)器運行時重新部署項目,熱加載即在運行時重新加載class,從而升級應(yīng)用。

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

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

4、擴展加載源

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

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

相關(guān)文章

  • Java實現(xiàn)簡單推箱子游戲

    Java實現(xiàn)簡單推箱子游戲

    這篇文章主要為大家詳細(xì)介紹了Java實現(xiàn)推箱子游戲,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2020-06-06
  • Java提示缺少返回語句的解決辦法

    Java提示缺少返回語句的解決辦法

    在本篇文章里小編給大家分享了關(guān)于Java提示缺少返回語句的解決辦法以及相關(guān)知識點,需要的朋友們參考下。
    2019-07-07
  • Java簡單工廠模式詳細(xì)解釋

    Java簡單工廠模式詳細(xì)解釋

    本文主要介紹了JAVA簡單工廠模式(從現(xiàn)實生活角度理解代碼原理)的相關(guān)知識。具有很好的參考價值。下面跟著小編一起來看下吧
    2021-11-11
  • Java kafka如何實現(xiàn)自定義分區(qū)類和攔截器

    Java kafka如何實現(xiàn)自定義分區(qū)類和攔截器

    這篇文章主要介紹了Java kafka如何實現(xiàn)自定義分區(qū)類和攔截器,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下
    2020-06-06
  • java讀取文件里面部分漢字內(nèi)容亂碼的解決方案

    java讀取文件里面部分漢字內(nèi)容亂碼的解決方案

    這篇文章主要介紹了java讀取文件里面部分漢字內(nèi)容亂碼的解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2021-06-06
  • Java解除文件占用即Dom4j操作后實現(xiàn)xml關(guān)流

    Java解除文件占用即Dom4j操作后實現(xiàn)xml關(guān)流

    這篇文章主要介紹了Java解除文件占用即Dom4j操作后實現(xiàn)xml關(guān)流,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下
    2020-04-04
  • Java Clone深拷貝與淺拷貝的兩種實現(xiàn)方法

    Java Clone深拷貝與淺拷貝的兩種實現(xiàn)方法

    今天小編就為大家分享一篇關(guān)于Java Clone深拷貝與淺拷貝的兩種實現(xiàn)方法,小編覺得內(nèi)容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧
    2018-10-10
  • Java中String類常用類型實例總結(jié)

    Java中String類常用類型實例總結(jié)

    在我們開發(fā)中經(jīng)常會用到很多的常用的工具類,這里做一個總結(jié),下面這篇文章主要給大家介紹了關(guān)于Java中String類常用類型的相關(guān)資料,String類代表字符串,需要的朋友可以參考下
    2021-12-12
  • Java?中不全部使用?Static?方法的理由

    Java?中不全部使用?Static?方法的理由

    這篇文章主要介紹了Java?中不全部使用?Static?方法的理由,本文通過實例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2022-08-08
  • SpringBoot定時任務(wù)參數(shù)運行代碼實例解析

    SpringBoot定時任務(wù)參數(shù)運行代碼實例解析

    這篇文章主要介紹了SpringBoot定時任務(wù)運行代碼實例解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下
    2020-06-06

最新評論