JVM類加載器-原理

虛擬機(jī)把描述類的數(shù)據(jù)從Class文件加載到內(nèi)存,并對(duì)數(shù)據(jù)進(jìn)行校驗(yàn)、轉(zhuǎn)換解析和初始化,最終形成可以被虛擬機(jī)直接使用的Java類型,這就是虛擬機(jī)的類加載機(jī)制。
在Java語(yǔ)言里,類型的加載、連接和初始化過(guò)程都是在程序運(yùn)行期間完成的,這種策略雖然會(huì)令類加載的時(shí)候稍微增加一些性能開(kāi)銷,但是卻為Java應(yīng)用程序提供了高度的靈活性。

一.類加載的生命周期

類從被加載到虛擬機(jī)到卸載出內(nèi)存,整個(gè)生命周期包括:加載、驗(yàn)證、準(zhǔn)備、解析、卸載、使用、初始化。其中驗(yàn)證、準(zhǔn)備、解析三個(gè)部分統(tǒng)稱為連接。
加載、驗(yàn)證、準(zhǔn)備、初始化和卸載這五個(gè)階段的順序是確定的,類的加載過(guò)程必須按照這種順序進(jìn)行,而解析則可以在初始化之后的階段進(jìn)行(為了支持java的動(dòng)態(tài)綁定)。
對(duì)于初始化階段,虛擬機(jī)規(guī)范則嚴(yán)格規(guī)定了有且只有5中情況必須立即對(duì)類進(jìn)行初始化。

  • 遇到new/getstatic/putstatic/invokestatic這四條字節(jié)碼指令,若此時(shí)類沒(méi)有進(jìn)行過(guò)初始化,則需先觸發(fā)其初始化。這四條指令分別對(duì)應(yīng)的Java代碼場(chǎng)景是:使用new實(shí)例化對(duì)象、讀取或設(shè)置一個(gè)類的靜態(tài)字段(被final修飾且在編譯期把結(jié)果放入常量池的靜態(tài)字段除外)、調(diào)用一個(gè)類的靜態(tài)方法時(shí)。
  • 使用java.lang.reflect包的方法對(duì)類進(jìn)行反射調(diào)用的時(shí)候,若類沒(méi)有進(jìn)行過(guò)初始化,則需觸發(fā)其初始化。
  • 當(dāng)初始化一個(gè)類的時(shí)候,發(fā)現(xiàn)其父類還沒(méi)有進(jìn)行初始化,則先觸發(fā)父類的初始化。
  • 當(dāng)虛擬機(jī)啟動(dòng)的時(shí)候,用戶需指定一個(gè)要執(zhí)行的主類,虛擬機(jī)會(huì)先初始化該類。
  • 使用JDK1.7的動(dòng)態(tài)語(yǔ)言支持,如果java.lang.invoke.MethodHandle實(shí)例最后的解析結(jié)果是REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄,且該方法對(duì)應(yīng)的類沒(méi)有初始化過(guò),先觸發(fā)其初始化。
    以上五種場(chǎng)景行為稱為對(duì)類的主動(dòng)引用,除此之外的引用類的方法都不會(huì)觸發(fā)初始化,稱為被動(dòng)引用。

二.類的加載過(guò)程

1.加載
加載階段,虛擬機(jī)要完成3件事:

  • 通過(guò)一個(gè)類的全限定名來(lái)獲取定義此類的二進(jìn)制字節(jié)流。
  • 將該字節(jié)流所代表的靜態(tài)存儲(chǔ)數(shù)據(jù)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)。
  • 在內(nèi)存中生成一個(gè)代表該類的java.lang.Class對(duì)象,作為方法區(qū)該類的各種數(shù)據(jù)結(jié)構(gòu)的訪問(wèn)入口。
    2.驗(yàn)證
    驗(yàn)證是連接階段的第一步,目的是為了確保class文件的字節(jié)流中包含的信息符合虛擬機(jī)的要求,并不會(huì)危害虛擬機(jī)自身的安全。驗(yàn)證階段大致會(huì)完成4個(gè)階段的檢驗(yàn)工作:文件格式驗(yàn)證、元數(shù)據(jù)驗(yàn)證、字節(jié)碼驗(yàn)證、符號(hào)引用驗(yàn)證。
  • 文件格式驗(yàn)證
    驗(yàn)證字節(jié)流是否符合Class文件的格式規(guī)范,且能被當(dāng)前版本的虛擬機(jī)處理。只有通過(guò)該階段的驗(yàn)證,字節(jié)流才會(huì)進(jìn)入內(nèi)存的方法區(qū)進(jìn)行存儲(chǔ),后續(xù)的三個(gè)驗(yàn)證階段全部都是基于方法區(qū)的存儲(chǔ)結(jié)構(gòu)進(jìn)行的,不會(huì)在直接操作字節(jié)流。
  • 元數(shù)據(jù)驗(yàn)證
    對(duì)字節(jié)碼描述的信息進(jìn)行語(yǔ)義分析,以保證描述的信息符合Java語(yǔ)言規(guī)范要求。
  • 字節(jié)碼驗(yàn)證
    該階段的目的是通過(guò)數(shù)據(jù)流和控制流分析,確定程序語(yǔ)義是否是合法、符合邏輯的。該階段對(duì)類的方法體進(jìn)行校驗(yàn)分析,保證被校驗(yàn)類的方法在運(yùn)行時(shí)不會(huì)做出對(duì)虛擬機(jī)安全有危害的事。
  • 符號(hào)引用驗(yàn)證
    該階段校驗(yàn)發(fā)生在虛擬機(jī)將符號(hào)引用轉(zhuǎn)化為直接引用的時(shí)候,該轉(zhuǎn)化動(dòng)作將在連接的第三階段-解析階段發(fā)生。符號(hào)引用驗(yàn)證可視為對(duì)類自身以外的信息進(jìn)行匹配性校驗(yàn),它的目的是確保解析動(dòng)作能正常執(zhí)行。

3.準(zhǔn)備
準(zhǔn)備階段是正式為類變量分配內(nèi)存并設(shè)置類變量初始值的階段,這些變量使用的內(nèi)存都將在方法區(qū)中進(jìn)行分配。
同時(shí),該階段進(jìn)行內(nèi)存分配的僅包括類變量(static修飾的變量),而不包括實(shí)例變量,實(shí)例變量會(huì)在類實(shí)例化的時(shí)候隨著對(duì)象一起在堆上被分配。其次,類變量分配的初始值通常是一個(gè)零值。
private static int val = 123;
val在準(zhǔn)備階段過(guò)后的初始值是0而非123,val被賦值為123的putstatic指令被放在類構(gòu)造器<clinit>方法中,所以val賦值為123的動(dòng)作在初始化階段才會(huì)執(zhí)行。
有一種特殊情況,若類字段的屬性列表存在ConstantValue屬性,則在準(zhǔn)備階段val就會(huì)被初始化為指定的值,即val有個(gè)final修飾符的情況。
4.解析
解析階段是虛擬機(jī)將常量池內(nèi) 的符號(hào)引用替換為直接 引用的過(guò)程。
5.初始化
類初始化階段是類加載過(guò)程的最后一步。在這一步,才真正開(kāi)始執(zhí)行類中定義的Java代碼。初始化階段是執(zhí)行類構(gòu)造器<cinit>()方法的過(guò)程。

三.類加載器

類加載階段‘通過(guò)一個(gè)類的全限定名來(lái)獲取此類的二進(jìn)制字節(jié)流’是由類加載器來(lái)完成的。
1.類與類加載器
對(duì)于 任意一個(gè)類,都需由加載 它的 類加載器和這個(gè)類本身一同確立其在java虛擬機(jī) 中的唯一性 。每一個(gè)類加載器都有一個(gè)獨(dú)立的類名稱空間。即比較兩個(gè)類是否相等,只有在這兩個(gè)類是由同一個(gè)類加載器加載的前提下才有 意義(這里的相等,包括了類的Class對(duì)象的equals方法,isAssignableFrom方法、isInstance方法的返回結(jié)果,以及通過(guò)instanceof關(guān)鍵字做的對(duì)象關(guān)系判定的情況)。

2.雙親委派模型
從開(kāi)發(fā)人員角度,類加載器 分為:

  • 啟動(dòng)類加載器:該 類加載器負(fù)責(zé)將放在JAVA_HOME/lib目錄中的,或被-Xbootclasspath參數(shù)所指定的路徑中的,并且是虛擬機(jī)識(shí)別的類庫(kù)加載到虛擬機(jī)內(nèi)存中。類啟動(dòng)加載器無(wú)法被java程序 直接引用,用戶在編寫(xiě)類加載器時(shí),如果需要把加載器請(qǐng)求 委派給引導(dǎo) 類 加載器 ,則 直接用 null代替 即可。
  • 擴(kuò)展類加載器:該加載器由sun.misc.Lanucher$ExtClassLoader實(shí)現(xiàn),負(fù)責(zé) 加載JAVA_HOME/lib/ext目錄中的或者是java.ext.dirs系統(tǒng)變量所指定的路徑中的所有 類庫(kù),開(kāi)發(fā)者可以直接使用擴(kuò)展類加載器。
  • 應(yīng)用程序類加載器:該類加載器由sun.misc.Lanucher$AppClassLoader實(shí)現(xiàn)。該類加載器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一 一般 也稱為系統(tǒng)類加載器。它負(fù)責(zé)加載用戶類路徑上 所指定的 類庫(kù) ,開(kāi)發(fā)者可以直接使用該類加載器 ,若 程序中沒(méi)有自定義過(guò)自己 的類加載器,一般都是程序中 的默認(rèn)類 加載器。
    應(yīng)用程序一般就是由這 三種 類加載器相互配合進(jìn)行加載的。
    類加載器的雙親委派模型如圖示:


    classloader.png

雙親委派模型的工作過(guò)程為:如果一個(gè)類加載器收到了類加載的請(qǐng)求,他首先不會(huì)去自己加載這個(gè)類,而是把這個(gè)請(qǐng)求委派給父類加載器去完成,每一個(gè)層次的類加載器都是如此,因此所有的加載請(qǐng)求最終都應(yīng)該傳送到頂層的啟動(dòng)類加載器中,只有當(dāng)父類加載器反饋?zhàn)约簾o(wú)法完成這個(gè)加載請(qǐng)求時(shí),子類加載器才會(huì)嘗試去自己加載。

3.破壞雙親委派模型
雙親委派模型并非一個(gè)強(qiáng)制性的約束模型,而是Java設(shè)計(jì)者推薦給開(kāi)發(fā)者的類加載器實(shí)現(xiàn)方式。

  • 建議現(xiàn)在再實(shí)現(xiàn)自己的ClassLoader類,不要去覆蓋loadClass方法,而應(yīng)該把自己的類加載邏輯寫(xiě)到findClass方法里,如果在loadClass方法的邏輯里如果父類加載失敗,則會(huì)去調(diào)用自己的findClass方法來(lái)完成加載,這樣可以保證寫(xiě)出來(lái)的類加載器是符合雙親委派模型。
  • 雙親委派模型很好的解決了各個(gè)類加載器加載基礎(chǔ)列的統(tǒng)一問(wèn)題(越基礎(chǔ)的類由越上層的類加載器進(jìn)行加載)。但是可能會(huì)出現(xiàn)基礎(chǔ)類回調(diào)用戶代碼的情況,比如JNDI可能會(huì)使用線程上下文類加載器去加載所需的SPI代碼。
  • 用戶對(duì)程序動(dòng)態(tài)性的追求導(dǎo)致的。如:HotSwap,HotDeploy,OSGi這種類加載器模型不再是雙親委派模型的樹(shù)形結(jié)構(gòu)。

參考文獻(xiàn)

最后編輯于
?著作權(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),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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