深入理解Java虛擬機(jī)-類加載機(jī)制

什么叫類加載

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

類加載的生命周期

  • 1、加載
  • 2、驗(yàn)證
  • 3、準(zhǔn)備
  • 4、解析
  • 5、初始化
  • 6、使用
  • 7、卸載

其中2、3、4統(tǒng)稱連接階段

類的初始化

只有在以下5種場景必須立刻對類進(jìn)行初始化

  • 遇到new、getstatic、putstatic或者invokestatic這四條指令時(shí),如果類沒進(jìn)行初始化,則先觸發(fā)其初始化。具體的場景:通過new實(shí)例化對象時(shí)、或者讀取或者配置一個(gè)類的靜態(tài)變量(final關(guān)鍵字修飾的除外,此時(shí)會在編譯期將結(jié)果放入常量池)是,以及調(diào)用類的靜態(tài)方法。
  • 使用java.lang.reflect包的方法對類進(jìn)行反射調(diào)用時(shí)
  • 當(dāng)調(diào)用一個(gè)類,發(fā)現(xiàn)其父類還沒初始化,應(yīng)觸發(fā)其父類先初始化
  • 當(dāng)虛擬機(jī)初始化時(shí),用戶需要指定一個(gè)啟動(dòng)類(包含main()方法的那個(gè)類),虛擬機(jī)會先初始化這個(gè)類
  • 當(dāng)使用JDK1.7的動(dòng)態(tài)語言支持時(shí),如果一個(gè)java.lang.invoke.MethodHandle實(shí)例最后的解析結(jié)果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且這個(gè)方法句柄對應(yīng)的類沒初始化化,則需要先觸發(fā)其初始化

以上5中場景稱為對一個(gè)類的主動(dòng)引用,除此之外,所有引用類的方式都不會觸發(fā)初始化,稱為被動(dòng)引用

  • 父類定義類方法和類變量,子類引用父類的類方法或者類變量,此時(shí),只會觸發(fā)父類初始化而不子類初始化;
  • 類中引用數(shù)組,數(shù)組會初始化,由newarray指令觸發(fā)一個(gè)虛擬機(jī)直接生成的的類[Lorg.fenixsoft.classloading.SuperClass觸發(fā)初始化但此類不會觸發(fā)初始化。
  • 類引用某個(gè)類的final常量時(shí),不是觸發(fā)初始化

一個(gè)接口初始化時(shí),并不要求它的父接口全部都初始化,只有在真正使用時(shí)才初始化

類加載的過程

加載

加載需要完成3件事情

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

“獲取類的二進(jìn)制字節(jié)流”,獲取的來源非常靈活,可以來自、

  • 讀取jar、war包
  • 從網(wǎng)絡(luò)中來,如:applet
  • 運(yùn)行時(shí)生成,如:Java的反射機(jī)制
  • 由其他文件生成,如:jsp
  • 還有其他方式

數(shù)組類不是有類加載創(chuàng)建,它是有虛擬機(jī)直接創(chuàng)建,但是數(shù)據(jù)元素的類型由類加載創(chuàng)建

  • 如果數(shù)組的元素類型為引用類型,數(shù)組將會在加載該元素類型的類加載器的類命名空間上被標(biāo)識
  • 如果數(shù)組的元素類型為基本類型,Java虛擬機(jī)會把數(shù)組標(biāo)識為與引導(dǎo)類加載器關(guān)聯(lián)
  • 數(shù)組類的可見性與元素類型一致,如果元素類型不是引用類型,則默認(rèn)為public

加載階段完成后,虛擬機(jī)外部的二進(jìn)制字節(jié)流就按照虛擬機(jī)所需的格式存儲在方法區(qū)之中。通過在內(nèi)存中實(shí)例化一個(gè)java.lang.class對象訪問類的數(shù)據(jù)

驗(yàn)證

驗(yàn)證是確保二進(jìn)制字節(jié)流的信息符合當(dāng)前虛擬機(jī)的要求,并且不會危害虛擬機(jī)的安全

文件格式驗(yàn)證

保證輸入的字節(jié)流能正確地解析并存儲于方法區(qū)內(nèi),格式上符合描述一個(gè)Java類型信息的要求,包括以下驗(yàn)證點(diǎn)

  • 是否以魔數(shù)開頭
  • 主、次版本是否在當(dāng)前虛擬機(jī)處理的范圍內(nèi)
  • 常量池的常量是否有不被支持的常量

等等

元數(shù)據(jù)驗(yàn)證(除方法體)

保證不存在不符合Java語言規(guī)范的元數(shù)據(jù)信息,包括以下驗(yàn)證點(diǎn)

  • 這個(gè)類是否有父類
  • 如果不是抽象類,是否已經(jīng)實(shí)現(xiàn)父類或接口中的所有方法
  • 是否繼承了不允許被繼承的類(final修飾的類)

等等

字節(jié)碼驗(yàn)證(方法體)

對類的方法體進(jìn)行校驗(yàn),包括以下驗(yàn)證點(diǎn):

  • 保證操作數(shù)棧的數(shù)據(jù)類型和指令代碼一致
  • 保證跳轉(zhuǎn)指令代碼不會跳轉(zhuǎn)到方法體之外
  • 保證方法體中類型轉(zhuǎn)換是有效的,如:可以把一個(gè)子類對象賦給父類,反過來則不行。
符號引用驗(yàn)證

對類以外的信息進(jìn)行匹配性校驗(yàn),比如

  • 符號引用中根據(jù)全限定名是否找到對應(yīng)類
  • 符號引用的類、字段、方法的訪問性(protected、public等)是否可被當(dāng)前類訪問
  • 在指定類中是否存在符合方法的字段描述符以及簡單名稱所描述的字段和方法

對虛擬機(jī)的類加載機(jī)制來說,驗(yàn)證是一個(gè)非常重要的、但不一定不要的階段。如果所運(yùn)行的代碼已經(jīng)被反復(fù)驗(yàn)證,可以通過-Xverify:none參數(shù)關(guān)閉大部分的類驗(yàn)證措施,以縮短虛擬機(jī)類加載的時(shí)間

準(zhǔn)備

準(zhǔn)備階段是為類變量(static)分配內(nèi)存并設(shè)置初始值的階段,這些變量所使用的內(nèi)存都將在方法區(qū)中進(jìn)行分配。如

public static int a = 100;

此時(shí),會為a分配內(nèi)存,并賦值為0

如果類字段的字段屬性表中存在ConstantValue屬性,那在準(zhǔn)備階段變量的value就會被初始化為ConstantValue屬性所指定的值,如

public static  final int a = 100;

此時(shí),會為a分配內(nèi)存,并賦值為100

解析

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

  • 符號引用:符號引用是用以一組符號來描述所引用的目標(biāo),與內(nèi)存布局無關(guān),引用目標(biāo)并不一定已經(jīng)加載到內(nèi)存中
  • 直接引用:可以是直接指向目標(biāo)的指針、偏移量或者一個(gè)能間接定位到目標(biāo)的句柄

解析動(dòng)作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調(diào)用點(diǎn)限定符(動(dòng)態(tài)調(diào)用),分別對應(yīng)CONSTANT_Class_info、CONSTANT_Fieldref_info等7中常量類型,此次不包括實(shí)例方法的解析。

  • 類或接口的解析
  • 字段解析
  • 類方法解析
  • 接口方法解析
初始化

類初始化時(shí)類加載的最后一步,除了在加載階段用戶可以自定義類加載器之外,其余動(dòng)作都是完全由虛擬機(jī)主導(dǎo)和控制。到了初始化階段,才真正開始執(zhí)行類中定義的Java程序代碼。

在初始化階段,會根據(jù)程序員通過程序制定的主觀計(jì)劃去初始化類變量和其他資源。初始化階段是執(zhí)行類構(gòu)造器<clinit>()方法的過程

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

    public class Test{
    static{
    i = 0; //賦值可以編譯過
    System.out.print(i); //引用報(bào)錯(cuò)
    }
    static int i = 2;
    }

  • <clinit>()方法與類構(gòu)造函數(shù)(init())不同,它不需要顯式地調(diào)用父類的構(gòu)造器,虛擬機(jī)保證在子類的<clinit>()方法執(zhí)行前,父類的<clinit>方法已經(jīng)執(zhí)行完畢。在虛擬機(jī)中第一個(gè)被執(zhí)行<clinit>的類肯定是java.lang.Object
  • 由于父類的<clinit>方法先執(zhí)行,也就是父類的靜態(tài)語句要先由于子類的變量賦值
  • <clinit>()方法對類和接口來說不是必需的,如果類中沒有靜態(tài)語句或類變量,編譯器不會為這個(gè)類生產(chǎn)<clinit>方法
  • 接口中不能使用靜態(tài)語句,但仍然會有類變量賦值,因此接口也會生成<clinit>()方法。但與類不同的是,執(zhí)行接口的<clinit>()方法不用先執(zhí)行父接口中的<clinit>()方法。只有父類中的變量需要使用時(shí),才會初始化父接口。另外,實(shí)現(xiàn)類在出示化時(shí)也不會執(zhí)行接口的<clinit>()方法
  • 虛擬機(jī)會保證一個(gè)類的<clinit>()方法在多線程情況下的線程安全,如果當(dāng)前線程在執(zhí)行類的<clinit>()方法,虛擬機(jī)會阻塞另外線程對<clinit>()的執(zhí)行,直到當(dāng)前線程釋放鎖。同一個(gè)類加載器下,一個(gè)類型只會初始化一次。

類加載器

什么是類加載

虛擬機(jī)的設(shè)計(jì)團(tuán)隊(duì)把類加載階段中的“通過一個(gè)類的全限定名來獲取描述此類的二進(jìn)制字節(jié)流”這個(gè)動(dòng)作放到Java虛擬機(jī)外部實(shí)現(xiàn),以便應(yīng)用程序可以自己決定如何去獲取所需要的類。這個(gè)動(dòng)作的實(shí)現(xiàn)代碼模塊稱為“類加載器”。

類與類加載器

對應(yīng)任意一個(gè)類,都需要由加載它的類加載器和這個(gè)類本身一同確定它在虛擬機(jī)中的唯一性,每一個(gè)類加載器,都擁有一個(gè)獨(dú)立的類名稱空間。也就是說,比較兩個(gè)類是否相等,只有在這兩個(gè)類是由同一個(gè)類加載器加載的前提下才有意義。

雙親委派模型

從Java虛擬機(jī)的角度來看,只存在兩種不同的類加載器:一種是啟動(dòng)類加載器,這個(gè)類加載器使用C++實(shí)現(xiàn),是虛擬機(jī)自身的一部分;另外一種就是其他的類加載器,這個(gè)類加載器都是有Java語言實(shí)現(xiàn),獨(dú)立于虛擬機(jī)外部,并都繼承自抽象類java.lang.ClassLoader。

  • 啟動(dòng)類加載器(Bootstrap ClassLoader):這個(gè)類加載器負(fù)責(zé)將%JAVA_HOME%\lib目錄下的,或者被-Xbootclasspath參數(shù)指定的路徑中的,并且是虛擬機(jī)識別的類庫加載到虛擬機(jī)內(nèi)存中。用戶在編寫自定義類加載器時(shí),如果需要將加載請求委派給啟動(dòng)類加載器,那直接使用null代替即可。
  • 擴(kuò)展類加載器(Extension ClassLoader):這個(gè)加載器由sun.misc.Launcher$ExtClassLoader實(shí)現(xiàn),它負(fù)責(zé)加載%JAVA_HOME%\lib\ext目錄中的,或者被java.ext.dirs系統(tǒng)變量所指定的路徑中的類庫,開發(fā)者可以直接使用擴(kuò)展類加載器。
  • 應(yīng)用程序類加載器(Application ClassLoader):這個(gè)加載器由sun.misc.Launcher$App-ClassLoader實(shí)現(xiàn)。由于這個(gè)類加載器是ClassLoader中的getSystemClassLoader()方法的返回值,所有一般稱它為系統(tǒng)類加載器。它負(fù)責(zé)加載用戶類路徑(ClassPath)上所指定的類庫,開發(fā)者可以直接使用這個(gè)類加載器,如果應(yīng)用程序中沒有自定義的過自己定義的類加載器,一般情況下這個(gè)就是程序中默認(rèn)的類加載器
雙親委派模型含義和工作原理

啟動(dòng)類加載器<--擴(kuò)展類加載器<--應(yīng)用程序類加載器<--自定義類加載器(1……n)

除了頂層類加載器(啟動(dòng)類加載器)外,其余類加載器都有自己的父類加載器。這種父子關(guān)系并不是通過繼承來實(shí)現(xiàn),而是通過組合關(guān)系來復(fù)用父類加載器的代碼。

雙親委派模型的工作原理:如果一個(gè)類加載器收到了類加載的請求,它首先不會去嘗試加載這個(gè)類,而是將類加載的請求委派給父類加載器去完成,每一個(gè)層次的類加載器都是如此,因此所有的加載請求都應(yīng)該傳送到最頂層的類加載器中,只用當(dāng)父類加載器反饋?zhàn)约簾o法加載請求(路徑中不存在該類)時(shí),子加載器才是自己去加載。

雙親委派模型的優(yōu)點(diǎn):提供一種具有優(yōu)先級別的層次關(guān)系(比如,基礎(chǔ)類可以放在父類加載器中),保證Java程序運(yùn)行的穩(wěn)定。

破壞雙親委派模型

雙親委派模型是Java設(shè)計(jì)者推薦給開發(fā)者的一種類加載器的實(shí)現(xiàn)方式,大部分的類加載器都遵循這個(gè)模型,但也有一個(gè)列外情況的出現(xiàn):

  • 在雙親委派模式出現(xiàn)之前,即JDK1.2發(fā)布之前。
  • 雙親委派模式自有的缺陷問題:基礎(chǔ)類調(diào)用用戶代碼,通過線程上下文類加載器(Thread Context ClassLoader)解決。線程上下文類加載器可以通過java.lang.Thread類的setContextClassLoader()方法設(shè)置,如果創(chuàng)建線程時(shí)還未設(shè)置,它將會從父進(jìn)程中繼承一個(gè),如果在應(yīng)用程序的全局范圍內(nèi)都沒有設(shè)置的話,那這個(gè)類加載器默認(rèn)就是應(yīng)用程序類加載器。通過線程上下文類加載器,JNDI服務(wù)可以加載所需要的SPI代碼,也就是父類加載器請求子類加載器去完成類加載的動(dòng)作。Java中所有涉及SPI的加載動(dòng)作基本都采用這種方式,例如:JNDI、JDBC、JCE、JAXB和JBI等
  • 用戶對程序動(dòng)態(tài)性的追求:熱部署等等
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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

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