JVM的類加載原理

一、什么是類加載器(ClassLoader)

類加載器是指在系統(tǒng)運(yùn)行過程中動(dòng)態(tài)的將字節(jié)碼文件加載到 JVM 中的工具,是一個(gè)類?;谶@個(gè)工具的整套類加載流程,稱作類加載機(jī)制。在 IDE 中編寫的都是源代碼文件,以后綴名為.java的文件形式存在于磁盤上,經(jīng)過編譯后生成后綴名為.class的字節(jié)碼文件,類加載器加載的就是這些字節(jié)碼文件。

JVM類加載

首先 Java 源代碼(.java)文件會(huì)被 Java 編譯器編譯為字節(jié)碼(.class)文件,然后由 JVM 中的類加載器加載各個(gè)類的字節(jié)碼文件,加載完畢之后,交由 JVM 執(zhí)行。在整個(gè)程序執(zhí)行過程中,JVM 會(huì)用一段空間來存儲(chǔ)程序執(zhí)行期間需要用到的數(shù)據(jù)和相關(guān)信息,這段空間一般被稱作 Runtime Data Area (運(yùn)行時(shí)數(shù)據(jù)區(qū)),也就是常說的 JVM 內(nèi)存。因此 Java 中常說的內(nèi)存管理就是針對(duì)這段空間進(jìn)行的管理(如何分配和回收內(nèi)存空間)。

二、類加載器的種類

Java 默認(rèn)提供了三個(gè)類加載器,分別是根加載器(BootStrapClassLoader)擴(kuò)展類加載器(ExtClassLoader)應(yīng)用類加載器(AppClassLoader),依次前者分別是后者的【父加載器】。父加載器不是「父類」,三者之間沒有繼承關(guān)系,只是因?yàn)轭惣虞d的流程使三者之間形成了父子關(guān)系。還有一種是用戶自定義類加載器(java.lang.ClassLoader的子類)。
Java 2 開始,類加載過程采取了雙親委派模型(Parents Delegation Model【PDM】),PDM 更好的保證了 Java 平臺(tái)的安全性。在該機(jī)制中,JVM 自帶的 BootStrapClassLoader 是根加載器,其他的加載器都有且僅有一個(gè)父類加載器。類的加載首先請(qǐng)求父類加載器加載,父類加載器無能為力時(shí)才由其子類加載器自行加載。JVM 不會(huì)向 Java 程序提供對(duì) BootStrapClassLoader 的引用。

三、雙親委派模型工作過程

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

說明:
PDM 只是 Java 推薦的機(jī)制,并不是強(qiáng)制的??梢岳^承java.lang.ClassLoader類,實(shí)現(xiàn)自己的類加載器。如果想保持 PDM,就重寫 findClass(name);如果想破壞 PDM,就重寫 loadClass(name)。JDBC 使用線程上下文加載器打破了 PDM,原因是 JDBC 只提供了接口,并沒有提供實(shí)現(xiàn)。

四、為什么需要雙親委派模型

1??防止內(nèi)存中出現(xiàn)多份同樣的字節(jié)碼。
反向思考,如果沒有 PDM 而是由各個(gè)類加載器自行加載的話,用戶編寫了一個(gè)java.lang.Object的同名類并放在 ClassPath 中,多個(gè)類加載器都能加載這個(gè)類到內(nèi)存中,系統(tǒng)中將會(huì)出現(xiàn)多個(gè)不同的 Object 類,那么類之間的比較結(jié)果及類的唯一性將無法保證。而且如果不使用 PDM 將會(huì)給虛擬機(jī)的安全帶來隱患。所以,要讓類對(duì)象進(jìn)行比較有意義,前提是它們要被同一個(gè)類加載器加載。

2??試想一個(gè)場景:
黑客自定義一個(gè)java.lang.String類,該類具有系統(tǒng) String 類一樣的功能,只是將某個(gè)方法稍作修改。比如在 equals 方法中,加入一些“病毒代碼”,并且通過自定義類加載器加入到 JVM 中。此時(shí),如果沒有 PDM,那么 JVM 就可能誤以為黑客自定義的java.lang.String類是系統(tǒng)的 String 類,導(dǎo)致“病毒代碼”被執(zhí)行。而有了 PDM,黑客自定義的java.lang.String類永遠(yuǎn)都不會(huì)被加載進(jìn)內(nèi)存。因?yàn)槭紫仁亲铐敹说念惣虞d器加載系統(tǒng)的java.lang.String類,最終自定義的類加載器無法加載java.lang.String類。

3??在自定義的類加載器里面強(qiáng)制加載自定義的java.lang.String類,不去通過調(diào)用父加載器不就好了嗎?
確實(shí)可行。但是,在 JVM 中,判斷一個(gè)對(duì)象是否是某個(gè)類型時(shí),如果該對(duì)象的實(shí)際類型與待比較的類型的類加載器不同,那么會(huì)返回 false。任意一個(gè)類,都需要由加載它的類加載器和這個(gè)類本身一同確立其在 JVM 中的唯一性,每一個(gè)類加載器,都擁有一個(gè)獨(dú)立的類名稱空間。也就是說,判斷兩個(gè)類是否“相等”,只有在這兩個(gè)類是由同一個(gè)類加載器加載的前提下才有意義,否則即使這兩個(gè)類來源于同一個(gè) Class 文件,被同一個(gè)虛擬機(jī)加載,只要加載它們的類加載器不同,這兩個(gè)類必定不相等。例如:ClassLoader1、ClassLoader2 都加載java.lang.String類,對(duì)應(yīng) Class1、Class2 對(duì)象。那么 Class1 對(duì)象不屬于 ClassLoader2 對(duì)象加載的java.lang.String類型。

五、線程上下文類加載器

并非所有的類加載機(jī)制都遵循這個(gè)模型,這個(gè)模型是被破壞過的。PDM 很好的解決了各個(gè)類加載器的基礎(chǔ)類的同一問題,基礎(chǔ)類是總被用戶代碼所調(diào)用的 API,但是基礎(chǔ)類要調(diào)用用戶的代碼的時(shí)候,PDM 就出現(xiàn)了缺陷。比如 JNDI 服務(wù),屬于 rt.jar,它需要調(diào)用應(yīng)用程序的代碼來實(shí)現(xiàn)資源管理,但是啟動(dòng)類加載器并不能識(shí)別應(yīng)用程序代碼,因此出現(xiàn)了線程上下文類加載器。這個(gè)類加載器由 Thread 類的 setContextClassLoaser() 進(jìn)行設(shè)置,如果線程未創(chuàng)建,它將會(huì)從主線程中繼承一個(gè)。JNDI 可以使用線程上下文加載器來加載所需要的 SPI 代碼,也就是父類加載器去請(qǐng)求子類加載器加載 Class。Java 中所有涉及 SPI 加載的基本上都采用這個(gè)方法。

六、關(guān)于幾個(gè)類加載器的說明

1??BootStrapClassLoader:根加載器,它是脫離 Java 語言,使用 C/C++ 編寫的類加載器,所以當(dāng)嘗試使用 ExtClassLoader 的實(shí)例調(diào)用 getParent(),獲取其父加載器時(shí)會(huì)得到一個(gè) null 值,比如調(diào)用String.class.getClassLoader()。根加載器會(huì)默認(rèn)加載系統(tǒng)變量 sun.boot.class.path 指定的類庫(jar 文件和 .class 文件),默認(rèn)是 $JRE_HOME/lib 下的類庫,如 rt.jar、resources.jar 等,具體可以輸出該環(huán)境變量的值來查看。除了加載這些默認(rèn)的類庫外,也可以使用 JVM 參數(shù) -Xbootclasspath/a 來追加額外需要讓根加載器加載的類庫。

總之,對(duì)于 BootStrapClassLoader 這個(gè)根加載器需要知道三點(diǎn):

  1. 根加載器使用 C/C++ 編寫,無法在 Java 中獲得其實(shí)例。
  2. 根加載器默認(rèn)加載系統(tǒng)變量 sun.boot.class.path 指定的類庫。
  3. 可以使用 -Xbootclasspath/a 參數(shù)追加根加載器的默認(rèn)加載類庫。

2??ExtClassLoader:擴(kuò)展類加載器,它是一個(gè)使用 Java 實(shí)現(xiàn)的類加載器(sun.misc.Launcher.ExtClassLoader),用于加載系統(tǒng)所需要的擴(kuò)展類庫。默認(rèn)加載系統(tǒng)變量 java.ext.dirs 指定位置下的類庫,通常是 $JRE_HOME/lib/ext 目錄下的類庫。
可以在啟動(dòng)時(shí)修改 java.ext.dirs 變量的值來修改擴(kuò)展類加載器的默認(rèn)類庫加載目錄,但通常并不建議這樣做。如果真的有需要擴(kuò)展類加載器在啟動(dòng)時(shí)加載的類庫,可以將其放置在默認(rèn)的加載目錄下。

總之,對(duì)于 ExtClassLoader 這個(gè)擴(kuò)展類加載器需要知道兩點(diǎn):

  1. 擴(kuò)展類加載器是使用 Java 實(shí)現(xiàn)的類加載器,可以在程序中獲得它的實(shí)例并使用。
  2. 通常不建議修改java.ext.dirs參數(shù)的值來修改默認(rèn)加載目錄,如有需要,可以將要加載的類庫放到這個(gè)默認(rèn)目錄下。

3??AppClassLoader:應(yīng)用類加載器,它和 ExtClassLoader 一樣,也是使用 Java 實(shí)現(xiàn)的類加載器(sun.misc.Launcher.AppClassLoader)。它的作用是加載應(yīng)用程序 classpath 下所有的類庫。它是應(yīng)用最廣泛的類加載器,是最常用的類加載器,在程序中調(diào)用的很多 getClassLoader() 返回的都是它的實(shí)例。在自定義類加載器時(shí)如果沒有特別指定,那么自定義的類加載器的默認(rèn)父加載器也是這個(gè)應(yīng)用類加載器。

總之,對(duì)于 AppClassLoader 這個(gè)應(yīng)用類加載器需要知道三點(diǎn):

  1. 應(yīng)用類加載器是使用 Java 實(shí)現(xiàn)的類加載器,負(fù)責(zé)加載應(yīng)用程序 classpath 下的類庫。
  2. 應(yīng)用類加載器是最常用的類加載器。

4??自定義類加載器:
除了上述三種 Java 默認(rèn)提供的類加載器外,還可以通過繼承java.lang.ClassLoader自定義類加載器。如果在創(chuàng)建自定義類加載器時(shí)沒有指定父加載器,那么默認(rèn)使用 AppClassLoader 作為父加載器。

七、類加載器的啟動(dòng)順序

BootStrapClassLoader 是一個(gè)使用 C/C++ 編寫的類加載器,它已經(jīng)嵌入到了 JVM 的內(nèi)核之中。當(dāng) JVM 啟動(dòng)時(shí),BootStrapClassLoader 也會(huì)隨之啟動(dòng)并加載核心類庫。當(dāng)核心類庫加載完成后,BootStrapClassLoader 會(huì)創(chuàng)建 ExtClassLoader 和 AppClassLoader 的實(shí)例,兩個(gè) Java 實(shí)現(xiàn)的類加載器將會(huì)加載自己負(fù)責(zé)路徑下的類庫,這個(gè)過程可以在 sun.misc.Launcher 中窺見。

原理:
JVM 中類的加載是由類加載器和它的子類來實(shí)現(xiàn)的。Java 中的類加載器是一個(gè)重要的 Java 運(yùn)行時(shí)系統(tǒng)組件,它負(fù)責(zé)在運(yùn)行時(shí)查找和裝入類文件中的類。由于 Java 的跨平臺(tái)性,經(jīng)過編譯的 Java 源程序并不是一個(gè)可執(zhí)行程序,而是一個(gè)或多個(gè)類文件。當(dāng) Java 程序需要使用某個(gè)類時(shí),JVM 會(huì)確保這個(gè)類已經(jīng)被加載、連接(驗(yàn)證、準(zhǔn)備和解析)初始化。

JVM類的加載
類的正常加載過程

八、關(guān)于Java靜態(tài)代碼塊執(zhí)行時(shí)機(jī)的解析

加載與類加載是兩個(gè)截然不同的過程。

Java的類加載是指類從被加載到虛擬機(jī)內(nèi)存中開始,到卸載出虛擬機(jī)內(nèi)存為止的整個(gè)生命周期中的整個(gè)過程,包括加載、驗(yàn)證、準(zhǔn)備、解析和初始化五個(gè)階段。加載指的是類加載的第一個(gè)階段。加載階段,虛擬機(jī)需要完成以下3件事情:

  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對(duì)象,作為方法區(qū)這個(gè)類的各種數(shù)據(jù)結(jié)構(gòu)的訪問入口。

類中的靜態(tài)塊會(huì)在整個(gè)類加載過程中的初始化階段執(zhí)行,而不是在類加載過程中的加載階段執(zhí)行。初始化階段是類加載過程中的最后一個(gè)階段,該階段就是執(zhí)行類構(gòu)造器<clinit>方法的過程,<clinit>方法由編譯器自動(dòng)收集類中所有類變量(靜態(tài)變量)的賦值動(dòng)作和靜態(tài)語句塊中的語句合并生成,一個(gè)類一旦進(jìn)入初始化階段,必然會(huì)執(zhí)行靜態(tài)語句塊。所以說,靜態(tài)塊一定會(huì)在類加載過程中被執(zhí)行,但不會(huì)在加載階段被執(zhí)行。

九、類初始化的條件

Java 虛擬機(jī)規(guī)范中嚴(yán)格規(guī)定了有且只有五種情況必須對(duì)類進(jìn)行初始化:

  1. 使用 new 字節(jié)碼指令創(chuàng)建類的實(shí)例,或者使用 getstatic、putstatic 讀取或設(shè)置一個(gè)靜態(tài)字段的值(放入常量池中的常量除外),或者調(diào)用一個(gè)靜態(tài)方法的時(shí)候,對(duì)應(yīng)類必須進(jìn)行過初始化。
  2. 通過java.lang.reflect包的方法對(duì)類進(jìn)行反射調(diào)用的時(shí)候,如果類沒有進(jìn)行過初始化,則要首先進(jìn)行初始化。
  3. 當(dāng)初始化一個(gè)類的時(shí)候,如果發(fā)現(xiàn)其父類沒有進(jìn)行過初始化,則首先觸發(fā)父類初始化。
  4. 當(dāng)虛擬機(jī)啟動(dòng)時(shí),用戶需要指定一個(gè)主類(包含main()的類),虛擬機(jī)會(huì)首先初始化這個(gè)類。
  5. 使用 jdk1.7 的動(dòng)態(tài)語言支持時(shí),如果一個(gè)java.lang.invoke.MethodHandle實(shí)例最后的解析結(jié)果 REF_getStatic、REF_putStatic、RE_invokeStatic 的方法句柄,并且這個(gè)方法句柄對(duì)應(yīng)的類沒有進(jìn)行初始化,則需要先觸發(fā)其初始化。

除了以上這五種情況,其他任何情況都不會(huì)觸發(fā)類的初始化。比如下面這幾種情況就不會(huì)觸發(fā)類初始化:

  1. 通過子類調(diào)用父類的靜態(tài)字段。此時(shí)父類符合情況一,而子類不符合任何情況。所以只有父類被初始化。
  2. 通過數(shù)組來引用類,不會(huì)觸發(fā)類的初始化。因?yàn)?new 的是數(shù)組,而不是類。
  3. 調(diào)用類的靜態(tài)常量不會(huì)觸發(fā)類的初始化,因?yàn)殪o態(tài)常量在編譯階段就會(huì)被存入調(diào)用類的常量池中,不會(huì)引用到定義常量的類。

十、為什么靜態(tài)方法不能調(diào)用非靜態(tài)方法和變量

靜態(tài)方法的內(nèi)存分配時(shí)間與實(shí)例方法不同。

  1. 靜態(tài)方法屬于類,在類加載的時(shí)候就會(huì)分配內(nèi)存,有了入口地址,可以通過“類名.方法名”直接調(diào)用。
  2. 非靜態(tài)成員(變量和方法)屬于類的對(duì)象,所以只有該對(duì)象初始化之后才會(huì)分配內(nèi)存,然后通過類的對(duì)象去訪問。
  3. 也就是說在靜態(tài)方法中調(diào)用非靜態(tài)成員變量,該變量可能還未初始化。因此編譯器會(huì)報(bào)錯(cuò)。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容