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

面試必時必問的JVM 類加載機(jī)制詳解

 更新時間:2021年08月16日 14:32:34   作者:程序員囧輝  
這篇文章主要介紹了一文讀懂Jvm類加載機(jī)制,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下

前言

本次帶來 JVM 的另一塊重要內(nèi)容,類加載機(jī)制,不廢話,直接開懟。

正文

1、類加載的過程。

類從被加載到虛擬機(jī)內(nèi)存中開始,到卸載出內(nèi)存為止,它的整個生命周期包括:加載、驗證、準(zhǔn)備、解析、初始化、使用和卸載7個階段。其中驗證、準(zhǔn)備、解析3個部分統(tǒng)稱為連接。

1)加載

“類加載”過程的一個階段,在加載階段,虛擬機(jī)需要完成以下3件事情:

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

2)驗證

連接階段的第一步,這一階段的目的是為了確保 Class 文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求,并且不會危害虛擬機(jī)自身的安全。從整體上看,驗證階段大致上會完成下面4個階段的檢驗動作:文件格式驗證、元數(shù)據(jù)驗證、字節(jié)碼驗證、符號引用驗證。

3)準(zhǔn)備

該階段是正式為類變量(static修飾的變量)分配內(nèi)存并設(shè)置類變量初始值的階段,這些變量所使用的內(nèi)存都將在方法區(qū)中進(jìn)行分配。這里所說的初始值“通常情況”下是數(shù)據(jù)類型的零值,下表列出了Java中所有基本數(shù)據(jù)類型的零值。

4)解析

該階段是虛擬機(jī)將常量池內(nèi)的符號引用替換為直接引用的過程。解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調(diào)用點限定符這7類符號引用進(jìn)行。

5)初始化

到了初始化階段,才真正開始執(zhí)行類中定義的Java程序代碼。在準(zhǔn)備階段,變量已經(jīng)賦過一次系統(tǒng)要求的初始零值,而在初始化階段,則會根據(jù)程序員通過程序制定的主觀計劃去初始化類變量和其他資源。

我們也可以從另外一種更直接的形式來表達(dá):初始化階段是執(zhí)行類構(gòu)造器<clinit>()方法的過程。<clinit>() 不是程序員在 Java 代碼中直接編寫的方法,而是由 Javac 編譯器自動生成的。

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

2、Java 虛擬機(jī)中有哪些類加載器?

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

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

另一種就是所有其他的類加載器,這些類加載器都由Java語言實現(xiàn),獨立于虛擬機(jī)外部,并且全都繼承自抽象類java.lang.ClassLoader。

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

1)啟動類加載器(Bootstrap ClassLoader):

這個類加載器負(fù)責(zé)將存放在<JAVA_HOME>\lib目錄中的,或者被-Xbootclasspath參數(shù)所指定的路徑中的,并且是虛擬機(jī)識別的(僅按照文件名識別,如rt.jar,名字不符合的類庫即使放在lib目錄中也不會被加載)類庫加載到虛擬機(jī)內(nèi)存中。

2)擴(kuò)展類加載器(Extension ClassLoader):

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

3)應(yīng)用程序類加載器(Application ClassLoader):

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

我們的應(yīng)用程序都是由這3種類加載器互相配合進(jìn)行加載的,如果有必要,還可以加入自己定義的類加載器。這些類加載器之間的關(guān)系一般如圖所示。

3、什么是雙親委派模型?

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

類加載的源碼如下:

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 1、檢查請求的類是否已經(jīng)被加載過了
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    // 2、將類加載請求先委托給父類加載器
                    if (parent != null) {
                        // 父類加載器不為空時,委托給父類加載進(jìn)行加載
                        c = parent.loadClass(name, false);
                    } else {
                        // 父類加載器為空,則代表當(dāng)前是Bootstrap,從Bootstrap中加載類
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // 如果父類加載器拋出ClassNotFoundException
                    // 說明父類加載器無法完成加載請求
                }
                if (c == null) {
                    // 3、在父類加載器無法加載的時候,再調(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;
        }
    }

4、為什么使用雙親委派模式?

1)使用雙親委派模型來組織類加載器之間的關(guān)系,有一個顯而易見的好處就是 Java 類隨著它的類加載器一起具備了一種帶有優(yōu)先級的層次關(guān)系。

2)如果沒有使用雙親委派模型,由各個類加載器自行去加載的話,如果用戶自己編寫了一個java.lang.Object 的類,并放在程序的 ClassPath 中,那系統(tǒng)中將會出現(xiàn)多個不同的 Object 類,Java 類型體系中最基礎(chǔ)的行為也就無法保證,應(yīng)用程序也將會變得一片混亂。

5、有哪些場景破壞了雙親委派模型?

目前比較常見的場景主要有:

1)線程上下文類加載器,典型的:JDBC 使用線程上下文類加載器加載 Driver 實現(xiàn)類

2)Tomcat 的多 Web 應(yīng)用程序

3)OSGI 實現(xiàn)模塊化熱部署

6、為什么要破壞雙親委派模型?

原因其實很簡單,就是使用雙親委派模型無法滿足需求了,因此只能破壞它,這邊以面試常問的 Tomcat 為例。

我們知道 Tomcat 容器可以同時部署多個 Web 應(yīng)用程序,多個 Web 應(yīng)用程序很容易存在依賴同一個 jar 包,但是版本不一樣的情況。例如應(yīng)用1和應(yīng)用2都依賴了 spring ,應(yīng)用1使用的 3.2.* 版本,而應(yīng)用2使用的是 4.3.* 版本。

如果遵循雙親委派模型,這個時候使用哪個版本了?

其實使用哪個版本都不行,很容易出現(xiàn)兼容性問題。因此,Tomcat 只能選擇破壞雙親委派模型。

7、如何破壞雙親委派模型?

破壞雙親委派模型的思路都比較類似,這邊以面試中常問到的 Tomcat 為例。

其實原理非常簡單,我們可以看到上面的類加載方法源碼(loadClass)的方法修飾符是 protected,因此我們只需以下幾步就能破壞雙親委派模型。

1)繼承 ClassLoader,Tomcat 中的 WebappClassLoader 繼承 ClassLoader 的子類 URLClassLoader。

2)重寫 loadClass 方法,實現(xiàn)自己的邏輯,不要每次都先委托給父類加載,例如可以先在本地加載,這樣就破壞了雙親委派模型了。

8、Tomcat 的類加載器?

Tomcat 的類加載器如下圖所示:

1)Bootstrap ClassLoader:可以看到上圖中缺少了 Extension ClassLoader,在 Tomcat 中 Extension ClassLoader 被集成到了 Bootstrap ClassLoader 里面。

2)System ClassLoader 就是 Application ClassLoader:Tomcat 中的系統(tǒng)類加載器不會加載 CLASSPATH 環(huán)境變量的內(nèi)容,而是從以下資源庫構(gòu)建 System 類加載器。

  • $CATALINA_HOME/bin/bootstrap.jar,包含用于初始化Tomcat服務(wù)器的 main() 方法,以及它所依賴的類加載器實現(xiàn)類。
  • $CATALINA_BASE/bin/tomcat-juli.jar 或 $CATALINA_HOME/bin/tomcat-juli.jar,日志實現(xiàn)類。
  • 如果 $CATALINA_BASE/bin 中存在 tomcat-juli.jar,則使用它來代替 $CATALINA_HOME/bin中的那個。
  • $CATALINA_HOME/bin/commons-daemon.jar

3)Common ClassLoader:從名字也看出來來了,主要包含一些通用的類,這些類對 Tomcat 內(nèi)部類和所有 Web 應(yīng)用程序都可見。

該類加載器搜索的位置由 $CATALINA_BASE/conf/catalina.properties 中的 common.loader 屬性定義,默認(rèn)設(shè)置將按照順序搜索以下位置。

  • $CATALINA_BASE/lib 中未打包的類和資源
  • $CATALINA_BASE/lib 目錄下的JAR 文件
  • $CATALINA_HOME/lib 中未打包的類和資源
  • $CATALINA_HOME/lib 目錄下的JAR文件

4)WebappX ClassLoader:Tomcat 為每個部署的 Web 應(yīng)用程序創(chuàng)建一個單獨的類加載器,這樣保證了不同應(yīng)用之間是隔離的,類和資源對其他 Web 應(yīng)用是不可見的。加載的路徑如下:

  • Web應(yīng)用的 /WEB-INF/classes 目錄下的所有未打包的類和資源
  • Web應(yīng)用的 /WEB-INF/lib 目錄下的 JAR 文件中的類和資源

9、Tomcat 的類加載過程?

Tomcat 的類加載過程,也就是 WebappClassLoaderBase#loadClass 的邏輯如下。

1)首先本地緩存 resourceEntries,如果已經(jīng)被加載過則直接返回緩存中的數(shù)據(jù)。

2)檢查 JVM 是否已經(jīng)加載過該類,如果是則直接返回。

3) 檢查要加載的類是否是 Java SE 的類,如果是則使用 BootStrap 類加載器加載該類,以防止 webapp 的類覆蓋了 Java SE 的類。

例如你寫了一個 java.lang.String 類,放在當(dāng)前應(yīng)用的 /WEB-INF/classes 中,如果沒有此步驟的保證,那么之后項目中使用的 String 類都是你自己定義的,而不是 rt.jar 下面的,可能會導(dǎo)致很多隱患。

4)針對委托屬性 delegate 顯示設(shè)置為 true、或者一些特殊的類(javax、org 包下的部分類),使用雙親委派模式加載,只有很少部分使用雙親委派模型來加載。

5)嘗試從本地加載類,如果步驟5中加載失敗也會走到本步驟,這邊打破了雙親委派模型,優(yōu)先從本地進(jìn)行加載。

7)走到這,代表步驟6加載失敗,如果之前不是使用雙親委派模式,則在這邊會委托給父類加載器來嘗試加載。

8)走到這邊代表所有的嘗試都加載失敗,拋出 ClassNotFoundException。

10、JDBC 使用線程上下文類加載器的原理

JDBC 功能相關(guān)的基礎(chǔ)類是由 Java 統(tǒng)一定義的,在 rt.jar 里面,例如 DriverManager,也就是由 Bootstrap ClassLoader 來加載,而 JDBC 的實現(xiàn)類是在各廠商的實現(xiàn) jar 包里,例如 MySQL 是在 mysql-connector-java 里,oracle、sqlserver 也會有各自的實現(xiàn) jar。

此時需要 JDBC 的基礎(chǔ)類調(diào)用其他廠商實現(xiàn)并部署在應(yīng)用程序的 ClassPath 下的 JDBC 服務(wù)提供接口(SPI,Service Provider Interface)的代碼。當(dāng)類A調(diào)用類B時,此時類B是由類A的類加載器來負(fù)責(zé)加載,而 JDBC 的基礎(chǔ)類都是由 Bootstrap ClassLoader 來加載,但是 Bootstrap ClassLoader 是不認(rèn)識也不會去加載這些廠商實現(xiàn)的代碼的。

因此,Java 提供了線程上下文類加載器,允許通過 Thread#setContextClassLoader/Thread#getContextClassLoader() 來設(shè)置和獲取當(dāng)前線程的上下文類加載器。如果創(chuàng)建線程時沒有設(shè)置,則會繼承父線程的,如果在應(yīng)用程序的全局范圍內(nèi)都沒有設(shè)置過的話,那這個類加載器默認(rèn)就是應(yīng)用程序類加載器(Application ClassLoader)。

綜上,JDBC 可以通過線程上下文類加載器,來實現(xiàn)父類加載器“委托”子類加載器完成類加載的行為,這個就明顯不遵守雙親委派模型了,不過這也是雙親委派模型自身的缺陷導(dǎo)致的。

總結(jié)

本篇文章就到這里了,希望能給你帶來幫助,也希望您能夠多多關(guān)注腳本之家的更多內(nèi)容!

相關(guān)文章

最新評論