一夜搞懂 | JVM 類加載機(jī)制

前言

本文已經(jīng)收錄到我的Github個(gè)人博客,歡迎大佬們光臨寒舍:

我的GIthub博客

學(xué)習(xí)導(dǎo)圖

學(xué)習(xí)導(dǎo)圖

一.為什么要學(xué)習(xí)類加載機(jī)制?

今天想跟大家嘮嗑嘮嗑Java的類加載機(jī)制,這是Java的一個(gè)很重要的創(chuàng)新點(diǎn),曾經(jīng)也是Java流行的重要原因之一。

Oracle當(dāng)初引入這個(gè)機(jī)制是為了滿足Java Applet開發(fā)的需求,JVM咬咬牙引入了Java類加載機(jī)制,后來的基于Jvm的動(dòng)態(tài)部署,插件化開發(fā)包括大家熱議的熱修復(fù),總之很多后來的技術(shù)都源于在JVM中引入了類加載器。

如今,類加載機(jī)制也在各個(gè)領(lǐng)域大放異彩,在面試中,由類加載機(jī)制所衍生出來各類面試題也層出不窮。

所以,我們要了解下類加載機(jī)制,為工作中或者是面試中實(shí)際的需要打好良好的基礎(chǔ)。

二.核心知識(shí)點(diǎn)歸納

2.1 概述

Q1:JVM類加載機(jī)制定義

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

Q2:特性

運(yùn)行期類加載。即在Java語言里面,類型的加載、連接和初始化過程都是在程序運(yùn)行期完成的,從而通過犧牲一些性能開銷來換取Java程序的高度靈活性

什么是運(yùn)行期,什么是編譯期?

  • 編譯期是指編譯器將源代碼翻譯機(jī)器能識(shí)別的代碼Java被編譯為Jvm認(rèn)識(shí)的字節(jié)碼文件
  • 運(yùn)行期則是指Java代碼的運(yùn)行過程

JVM運(yùn)行期動(dòng)態(tài)加載+動(dòng)態(tài)連接->Java的動(dòng)態(tài)擴(kuò)展特性

2.2 類加載的過程

類從被加載到虛擬機(jī)內(nèi)存中開始、到卸載出內(nèi)存為止,整個(gè)生命周期包括七個(gè)階段:

  • 加載

  • 驗(yàn)證

  • 準(zhǔn)備

  • 解析

  • 初始化

  • 使用

  • 卸載

其中,驗(yàn)證、準(zhǔn)備、解析這3個(gè)部分統(tǒng)稱為連接,流程如下圖:

類加載過程

注意:

  • 『加載』->『驗(yàn)證』->『準(zhǔn)備』->『初始化』->『卸載』這五個(gè)階段的順序是確定的,而『解析』可能為了支持Java的動(dòng)態(tài)綁定會(huì)在『初始化』后才開始
  • 上述階段通常都是互相交叉地混合式進(jìn)行的,比如會(huì)在一個(gè)階段執(zhí)行的過程中調(diào)用、激活另外一個(gè)階段

想要了解Java動(dòng)態(tài)綁定和靜態(tài)綁定區(qū)別的話,可以看下這篇文章:理解靜態(tài)綁定與動(dòng)態(tài)綁定

2.2.1 加載

Q1:任務(wù)

  • 通過類的全限定名來獲取定義此類的二進(jìn)制字節(jié)流。如從ZIP包讀取、從網(wǎng)絡(luò)中獲取、通過運(yùn)行時(shí)計(jì)算生成、由其他文件生成、從數(shù)據(jù)庫中讀取等等途徑......

想要詳細(xì)了解類的全限定名的知識(shí),可以看下這篇文章:全限定名、簡單名稱和描述符是什么東西?

  • 將該二進(jìn)制字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu),該數(shù)據(jù)存儲(chǔ)數(shù)據(jù)結(jié)構(gòu)由虛擬機(jī)實(shí)現(xiàn)自行定義
  • 在內(nèi)存中生成一個(gè)代表這個(gè)類的java.lang.Class對象,它將作為程序訪問方法區(qū)中的這些類型數(shù)據(jù)的外部接口

2.2.2 驗(yàn)證

  • 連接階段的第一步,且工作量在JVM類加載子系統(tǒng)中占了相當(dāng)大的一部分
  • 目的:為了確保Class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求,并且不會(huì)危害虛擬機(jī)自身的安全

由此可見,它能直接決定JVM能否承受惡意代碼的攻擊,因此驗(yàn)證階段很重要,但由于它對程序運(yùn)行期沒有影響,并不一定必要,可以考慮使用-Xverify:none參數(shù)來關(guān)閉大部分的類驗(yàn)證措施,以縮短虛擬機(jī)類加載的時(shí)間。

  • 檢驗(yàn)過程包括下面四個(gè)階段:

    A.文件格式驗(yàn)證:

    • 內(nèi)容:驗(yàn)證字節(jié)流是否符合Class文件格式的規(guī)范、以及是否能被當(dāng)前版本的虛擬機(jī)處理

    • 目的:保證輸入的字節(jié)流能正確地解析并存儲(chǔ)于方法區(qū)之內(nèi),且格式上符合描述一個(gè)Java類型信息的要求。只有保證二進(jìn)制字節(jié)流通過了該驗(yàn)證后,它才會(huì)進(jìn)入內(nèi)存的方法區(qū)中進(jìn)行存儲(chǔ),所以后續(xù)3個(gè)驗(yàn)證階段全部是基于方法區(qū)而不是字節(jié)流了

    • 例子:

      1. 是否以魔數(shù)0xCAFEBABE開頭

      2. 主次版本號是否在JVM接受范圍內(nèi)

      3. 索引值是否有指向不存在/不符合類型的常量

        ......

    B.元數(shù)據(jù)驗(yàn)證:

    • 內(nèi)容:對字節(jié)碼描述的信息進(jìn)行語義分析,以保證其描述的信息符合Java語言規(guī)范的要求

    • 目的:對類的元數(shù)據(jù)信息進(jìn)行語義校驗(yàn),保證不存在不符合Java語言規(guī)范的元數(shù)據(jù)信息

    • 例子:

      1. 類是否有父類(除了java.lang.Object之外,所有類都應(yīng)有父類)

      2. 父類是否繼承了不允許被繼承的類(final修飾的類)

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

        ......

    ? C.字節(jié)碼驗(yàn)證:

    • 是驗(yàn)證過程中最復(fù)雜的一個(gè)階段

    • 內(nèi)容:對類的方法體進(jìn)行校驗(yàn)分析,保證被校驗(yàn)類的方法在運(yùn)行時(shí)不會(huì)做出危害虛擬機(jī)安全的事件

    • 目的:通過數(shù)據(jù)流和控制流分析,確定程序語義是合法的、符合邏輯的

    • 例子:

      1. 保證任意時(shí)刻操作數(shù)棧的數(shù)據(jù)類型與指令代碼序列都能配合工作,例如不會(huì)出現(xiàn)“在操作數(shù)棧的數(shù)據(jù)類型中放置了int類型的數(shù)據(jù),使用時(shí)卻按long類型來載入本地變量表中”

      2. 保證任何跳轉(zhuǎn)指令都不會(huì)跳轉(zhuǎn)到方法體外的字節(jié)碼指令上

        ......

    ? D.符號引用驗(yàn)證:

    • 內(nèi)容:對類自身以外(如常量池中的各種符號引用)的信息進(jìn)行匹配性校驗(yàn)
    • 目的:確保解析動(dòng)作能正常執(zhí)行,如果無法通過符號引用驗(yàn)證,那么將會(huì)拋出一個(gè)java.lang.IncompatibleClassChangeError異常的子類
    • 注意:該驗(yàn)證發(fā)生在虛擬機(jī)將符號引用轉(zhuǎn)化為直接引用的時(shí)候,即『解析』階段

2.2.3 準(zhǔn)備

Q1:任務(wù)

  • 為類變量(靜態(tài)變量)分配內(nèi)存因?yàn)檫@里的變量是由方法區(qū)分配內(nèi)存的,所以僅包括類變量而不包括實(shí)例變量,后者將會(huì)在對象實(shí)例化時(shí)隨著對象一起分配在Java堆中
  • 設(shè)置類變量初始值:通常情況下零值

2.2.4 解析

之前提過,解析階段就是虛擬機(jī)將常量池內(nèi)的符號引用替換為直接引用的過程

  • 符號引用:以一組符號來描述所引用的目標(biāo)
  • 可以是任何形式的字面量,只要使用時(shí)能無歧義地定位到目標(biāo)即可
  • 與虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局無關(guān),因?yàn)榉栆玫淖置媪啃问矫鞔_定義在Java虛擬機(jī)規(guī)范的Class文件格式中,所以即使各種虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局不同,但是能接受符號引用都是一致的
  • 直接引用:
  • 可以是直接指向目標(biāo)的指針、相對偏移量或是一個(gè)能間接定位到目標(biāo)的句柄
  • 與虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局相關(guān),同一個(gè)符號引用在不同虛擬機(jī)實(shí)例上翻譯出來的直接引用一般不同
  • 發(fā)生時(shí)間:JVM會(huì)根據(jù)需要來判斷,是在類被加載器加載時(shí)就對常量池中的符號引用進(jìn)行解析,還是等到一個(gè)符號引用將要被使用前才去解析
  • 解析動(dòng)作:有七類符號及其對應(yīng)在常量池的七種常量類型
  • 類或接口(CONSTANT_Class_info)
  • 字段(CONSTANT_Fieldref_info)
  • 類方法(CONSTANT_Methodref_info)
  • 接口方法(CONSTANT_InterfaceMethodref_info)
  • 方法類型(CONSTANT_MethodType_info)
  • 方法句柄(CONSTANT_MethodHandle_info)
  • 調(diào)用點(diǎn)限定符(CONSTANT_InvokeDynamic_info)

舉個(gè)例子,設(shè)當(dāng)前代碼所處的為類D,把一個(gè)從未解析過的符號引用N解析為一個(gè)類或接口C的直接引用,解析過程分三步:

  • C不是數(shù)組類型:JVM將會(huì)把代表N的全限定名傳遞給D類加載器去加載這個(gè)類C。在加載過程中,由于元數(shù)據(jù)驗(yàn)證、字節(jié)碼驗(yàn)證的需要,又可能觸發(fā)其他相關(guān)類的加載動(dòng)作。一旦這個(gè)加載過程出現(xiàn)了任何異常,解析過程就宣告失敗。
  • C是數(shù)組類型且數(shù)組元素類型為對象:JVM也會(huì)按照上述規(guī)則加載數(shù)組元素類型
  • 若上述步驟無任何異常:此時(shí)CJVM中已成為一個(gè)有效的類或接口,但在解析完成前還需進(jìn)行符號引用驗(yàn)證,來確認(rèn)D是否具備對C的訪問權(quán)限。如果發(fā)現(xiàn)不具備訪問權(quán)限,將拋出java.lang.IllegalAccessError異常

Q1:字段(成員變量/域)和屬性有什么區(qū)別?

  • 屬性,是指對象的屬性,對于JavaBean來說,是getXXX方法定義的
  • 字段,是成員變量
class Person{
    private String mingzi;  //mingzi是字段,一般來說字段和屬性是相同的,但是這個(gè)例子是特例
    public String getName(){  //name是屬性
        return mingzi:
    }
    public void setName(){
        mingzi= "張三";
    }
}

2.2.5 初始化

  • 是類加載過程的最后一步,會(huì)開始真正執(zhí)行類中定義的Java代碼。而之前的類加載過程中,除了在『加載』階段用戶應(yīng)用程序可通過自定義類加載器參與之外,其余階段均由虛擬機(jī)主導(dǎo)和控制
  • 與『準(zhǔn)備』階段的區(qū)分
  • 準(zhǔn)備階段:變量賦初始零值
  • 初始化階段:根據(jù)Java程序的設(shè)定去初始化類變量和其他資源,或者說是執(zhí)行類構(gòu)造器clinit的過程

clinit:由編譯器自動(dòng)收集類中的所有類變量(靜態(tài)變量)的賦值動(dòng)作和靜態(tài)語句塊static{}中的語句合并產(chǎn)生

  • 線程安全的,在多線程環(huán)境中被正確地加鎖、同步
  • 對于類或接口來說是必需的,如果一個(gè)類中沒有靜態(tài)語句塊,也沒有對變量的賦值操作,那么編譯器可以不為這個(gè)類生成 clinit
  • 接口與類不同的是,執(zhí)行接口的 clinit不需要先執(zhí)行父接口clinit,只有當(dāng)父接口中定義的變量使用時(shí),父接口才會(huì)初始化。另外,接口的實(shí)現(xiàn)類在初始化時(shí)也一樣不會(huì)執(zhí)行接口的clinit

想詳細(xì)了解clinit以及其與init的區(qū)別的讀者,可以看下這篇文章:深入理解jvm--Java中init和clinit區(qū)別完全解析

  • 在虛擬機(jī)規(guī)范中,規(guī)定了有且只有五種情況必須立即對類進(jìn)行『初始化』:
  • 遇到new、getstaticputstaticinvokestatic這4條字節(jié)碼指令時(shí)
  • 使用java.lang.reflect包的方法對類進(jìn)行反射調(diào)用的時(shí)候
  • 當(dāng)初始化一個(gè)類的時(shí)候,若發(fā)現(xiàn)其父類還未進(jìn)行初始化,需先觸發(fā)其父類的初始化
  • 在虛擬機(jī)啟動(dòng)時(shí),需指定一個(gè)要執(zhí)行的主類,虛擬機(jī)會(huì)先初始化它
  • 當(dāng)使用JDK1.7的動(dòng)態(tài)語言支持時(shí),若一個(gè)java.lang.invoke.MethodHandle實(shí)例最后的解析結(jié)果為REF_getStaticREF_putStatic、REF_invokeStatic的方法句柄,且這個(gè)方法句柄所對應(yīng)的類未進(jìn)行初始化,需先觸發(fā)其初始化。

2.3 類加載器&雙親委派模型

每個(gè)類加載器,都擁有一個(gè)獨(dú)立的命名空間,它不僅用于加載類,還和這個(gè)類本身一起作為在JVM中的唯一標(biāo)識(shí)。所以比較兩個(gè)類是否相等,只要看它們是否由同一個(gè)類加載器加載,即使它們來源于同一個(gè)Class文件且被同一個(gè)JVM加載,只要加載它們的類加載器不同,這兩個(gè)類就必定不相等

2.3.1 類加載器

JVM的角度,可將類加載器分為兩種:

  • 啟動(dòng)類加載器
  • C++語言實(shí)現(xiàn),是虛擬機(jī)自身的一部分
  • 負(fù)責(zé)加載存放在<JAVA_HOME>\lib目錄中、或被-Xbootclasspath參數(shù)所指定路徑中的、且可被虛擬機(jī)識(shí)別的類庫
  • 無法被Java程序直接引用,如果自定義類加載器想要把加載請求委派給引導(dǎo)類加載器的話,可直接用null代替
  • 其他類加載器:由Java語言實(shí)現(xiàn),獨(dú)立于虛擬機(jī)外部,并且全都繼承自抽象類java.lang.ClassLoader,可被Java程序直接引用。常見幾種:
  • 擴(kuò)展類加載器

    A.由sun.misc.Launcher$ExtClassLoader實(shí)現(xiàn)

    B.負(fù)責(zé)加載<JAVA_HOME>\lib\ext目錄中的、或者被java.ext.dirs系統(tǒng)變量所指定的路徑中的所有類庫

  • 應(yīng)用程序類加載器

    A.是默認(rèn)的類加載器,是ClassLoader#getSystemClassLoader()的返回值,故又稱為系統(tǒng)類加載器

    B.由sun.misc.Launcher$App-ClassLoader實(shí)現(xiàn)

    C.負(fù)責(zé)加載用戶類路徑上所指定的類庫

  • 自定義類加載器:如果以上類加載起不能滿足需求,可自定義

類加載器的關(guān)系

需要注意的是:雖然數(shù)組類不通過類加載器創(chuàng)建而是由JVM直接創(chuàng)建的,但仍與類加載器有密切關(guān)系,因?yàn)?strong>數(shù)組類的元素類型最終還要靠類加載器去創(chuàng)建

2.3.2 雙親委派模型

  • 定義:表示類加載器之間的層次關(guān)系
  • 前提:除了頂層啟動(dòng)類加載器外,其余類加載器都應(yīng)當(dāng)有自己的父類加載器,且它們之間關(guān)系一般不會(huì)以繼承關(guān)系來實(shí)現(xiàn),而是通過組合關(guān)系來復(fù)用父加載器的代碼
  • 工作過程:若一個(gè)類加載器收到了類加載的請求,它先會(huì)把這個(gè)請求委派給父類加載器,并向上傳遞,最終請求都傳送到頂層的啟動(dòng)類加載器中。只有當(dāng)父加載器反饋?zhàn)约簾o法完成這個(gè)加載請求時(shí),子加載器才會(huì)嘗試自己去加載
  • 注意:不是一個(gè)強(qiáng)制性的約束模型,而是Java設(shè)計(jì)者推薦給開發(fā)者的一種類加載器實(shí)現(xiàn)方式
  • 優(yōu)點(diǎn):類會(huì)隨著它的類加載器一起具備帶有優(yōu)先級的層次關(guān)系,可保證Java程序的穩(wěn)定運(yùn)作;實(shí)現(xiàn)簡單,所有實(shí)現(xiàn)代碼都集中在java.lang.ClassLoader的loadClass()

比如,某些類加載器要加載java.lang.Object類,最終都會(huì)委派給最頂端的啟動(dòng)類加載器去加載,這樣Object類在程序的各種類加載器環(huán)境中都是同一個(gè)類。

相反,系統(tǒng)中將會(huì)出現(xiàn)多個(gè)不同的Object類,Java類型體系中最基礎(chǔ)的行為也就無法保證,應(yīng)用程序也將會(huì)變得一片混亂

三.課堂小測試

恭喜你!已經(jīng)看完了前面的文章,相信你對JVM類加載機(jī)制已經(jīng)有一定深度的了解,下面,進(jìn)行一下課堂小測試,驗(yàn)證一下自己的學(xué)習(xí)成果吧!

Q1:類加載的全過程是怎樣的?

Q2:什么是雙親委派模型?

Q3:String類如何被加載的

上面問題的答案,在前文都提到過,如果還不能回答出來的話,建議回顧下前文

Q4:請你談?wù)勵(lì)惣虞d過程,以Person a = new Person();為例進(jìn)行說明

這道題是在??偷氖罴賹?shí)習(xí)Tencent一面的面筋上找的,附上標(biāo)準(zhǔn)答案:類的加載過程,Person person = new Person();為例進(jìn)行說明


如果文章對您有一點(diǎn)幫助的話,希望您能點(diǎn)一下贊,您的點(diǎn)贊,是我前進(jìn)的動(dòng)力

本文參考鏈接:

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

  • 代碼編譯的結(jié)果從本地機(jī)器碼轉(zhuǎn)變?yōu)樽止?jié)碼,是存儲(chǔ)格式發(fā)展的一小步,確實(shí)編譯語言發(fā)展的一大步。 虛擬機(jī)把描述類的數(shù)據(jù)從...
    胡二囧閱讀 1,051評論 0 0
  • 虛擬機(jī)把描述類的數(shù)據(jù)從Class文件加載到內(nèi)存,并對數(shù)據(jù)進(jìn)行校驗(yàn)、轉(zhuǎn)換解析和初始化,最終形成可以被虛擬機(jī)直接使用的...
    丑人林宗己閱讀 653評論 0 2
  • 原文地址[http://blog.csdn.net/ns_code/article/details/1788158...
    期待現(xiàn)在閱讀 335評論 0 2
  • Java虛擬機(jī)整體篇幅如下: Java虛擬機(jī)基礎(chǔ)——1Java的內(nèi)存模型Java虛擬機(jī)基礎(chǔ)——2JVM運(yùn)行時(shí)數(shù)據(jù)區(qū)...
    隔壁老李頭閱讀 2,941評論 1 16
  • 文/南熠姑梁 在文章開頭,引用一段百度百科文字——2016年底以來,國內(nèi)共享單車突然就火爆了起來。在街頭,仿佛一夜...
    南熠姑梁閱讀 382評論 5 4

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