深入JVM(四)虛擬機類加載機制

代碼編譯的結(jié)果從本地機器碼轉(zhuǎn)變?yōu)樽止?jié)碼,是存儲格式發(fā)展的一小步,卻是編程語言發(fā)展的一大步。

虛擬機類加載機制

虛擬機把描述”類“的數(shù)據(jù)從Class文件加載到內(nèi)存,并對數(shù)據(jù)進行校驗,轉(zhuǎn)換解析和初始化,最終形成可以被虛擬機直接使用的java類型,這就是虛擬機的類加載機制。
在java語言里,類型的加載,連接和初始化都是在程序運行期間完成的,這種策略雖然會令類加載時稍加一些性能開銷,但是會為java應(yīng)用程序提供高度的靈活性。java的動態(tài)擴展就是依據(jù)運行期動態(tài)加載和動態(tài)連接這個特點實現(xiàn)的。
其實拆開了講就是編寫一個面向接口的應(yīng)用程序,運行的時候再指定實現(xiàn)類。用戶通過預(yù)定義和自定義類加載器,讓應(yīng)用程序再運行時再加載那個實現(xiàn)類的二進制流作為程序代碼的一部分。(這個是我的理解。原話比較復(fù)雜而且不太好理解。我反正個人覺得我這么說簡單多了。如果理解的有不對的地方歡迎指出。)

類加載的時機

類從被加載到虛擬機內(nèi)存中開始,到卸載出內(nèi)存為止。它的整個生命周期包括:

  • 加載
  • 驗證
  • 準(zhǔn)備
  • 解析
  • 初始化
  • 使用
  • 卸載

七個階段。其中驗證,準(zhǔn)備,解析三個部分統(tǒng)稱為連接。


類的生命周期

加載,驗證,準(zhǔn)備,初始化和卸載這五個順序的確定的,類的加載過程必須按照這種順序按部就班的開始。而解析階段卻不一定。在某些情況下可以在初始化之后進行。這是為了支持java語言的動態(tài)綁定。
什么情況下開始類的加載呢?java虛擬機規(guī)范沒有強制約束,一般由虛擬機的具體實現(xiàn)來自由把握。但是對于初始化階段,虛擬機規(guī)范是嚴格規(guī)定了有且只有五種情況必須立即對類進行初始化:

  1. new關(guān)鍵字實例化對象,讀取或設(shè)置一個類的靜態(tài)字段以及調(diào)用一個類的靜態(tài)方法時。如果這個類沒有進行初始化,則要先初始化。

  2. 使用java.lang.reflect進行反射調(diào)用的時候,如果這個類沒有進行初始化,則要先初始化。

  3. 當(dāng)初始化一個類,發(fā)現(xiàn)其父類沒有進行初始化,則要先初始化其父類。

  4. 虛擬機啟動時,用戶需要指定一個執(zhí)行的主類(包含main方法的那個類),虛擬機會先初始化這個主類。

  5. (這個我沒太看懂,查了一些資料差不多就是類似反射的Method調(diào)用類的靜態(tài)塊要先初始化)JDK1.7的動態(tài)語言支持時,一個java.lang.invoke.MethodHandle實例最后的解析結(jié)果是REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,并且這個方法對應(yīng)的類沒有進行初始化,則要先初始化。

java7在JSR 292中增加了對動態(tài)類型語言的支持,使java也可以像C語言那樣將方法作為參數(shù)傳遞,其實現(xiàn)在lava.lang.invoke包中。MethodHandle作用類似于反射中的Method類,但它比Method類要更加靈活和輕量級。通過MethodHandle進行方法調(diào)用一般需要以下幾步:
(1)創(chuàng)建MethodType對象,指定方法的簽名;
(2)在MethodHandles.Lookup中查找類型為MethodType的MethodHandle;
(3)傳入方法參數(shù)并調(diào)用MethodHandle.invoke或者MethodHandle.invokeExact方法。

MethodType
可以通過MethodHandle類的type方法查看其類型,返回值是MethodType類的對象。也可以在得到MethodType對象之后,調(diào)用MethodHandle.asType(mt)方法適配得到MethodHandle對象。可以通過調(diào)用MethodType的靜態(tài)方法創(chuàng)建MethodType實例,有三種創(chuàng)建方式:
(1)methodType及其重載方法:需要指定返回值類型以及0到多個參數(shù);
(2)genericMethodType:需要指定參數(shù)的個數(shù),類型都為Object;
(3)fromMethodDescriptorString:通過方法描述來創(chuàng)建。

創(chuàng)建好MethodType對象后,還可以對其進行修改,MethodType類中提供了一系列的修改方法,比如:changeParameterType、changeReturnType等。
Lookup
MethodHandle.Lookup相當(dāng)于MethodHandle工廠類,通過findxxx方法可以得到相應(yīng)的MethodHandle,還可以配合反射API創(chuàng)建MethodHandle,對應(yīng)的方法有unreflect、unreflectSpecial等。
invoke
在得到MethodHandle后就可以進行方法調(diào)用了,有三種調(diào)用形式:
(1)invokeExact:調(diào)用此方法與直接調(diào)用底層方法一樣,需要做到參數(shù)類型精確匹配;
(2)invoke:參數(shù)類型松散匹配,通過asType自動適配;
(3)invokeWithArguments:直接通過方法參數(shù)來調(diào)用。其實現(xiàn)是先通過genericMethodType方法得到MethodType,再通過MethodHandle的asType轉(zhuǎn)換后得到一個新的MethodHandle,最后通過新MethodHandle的invokeExact方法來完成調(diào)用。
原文:https://blog.csdn.net/aesop_wubo/article/details/48858931

然后對于這物種出發(fā)類進行初始化的場景,是有且只有!除了這五種之外所有引用類的方式都不會出發(fā)初始化,稱之為被動引用。
接口的加載過程與類的加載過程有一些不同。針對接口需要做一些特殊說明:
接口也有初始化過程,這點與類是一致的。接口與類初始化的最大的不同是上面第三點:當(dāng)初始化一個類,發(fā)現(xiàn)其父類沒有進行初始化,則要先初始化其父類。接口不要求初始化其父類接口。只有在真正使用到父接口的時候才會初始化。

類加載過程

加載:
加載是”類加載“過程的一個階段。加載階段虛擬機做三件事:

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

(我個人簡單的理解:1,通過名字找到類,2,把這個類存起來。3,有人訪問這個類根據(jù)名字去找存起來的。)
這個從哪里獲取沒有明確的規(guī)定。從zip包讀取,就是我們jar,war,ear的基礎(chǔ)。從網(wǎng)絡(luò)中獲取典型的應(yīng)用Applet,運行時計算生成就是動態(tài)代理技術(shù)的原理。。。還有好多例子,我就不一一說了。
數(shù)組類而言,本身不通過類加載器創(chuàng)建。它是由于java虛擬機直接創(chuàng)建的。但數(shù)組與類加載器仍然有很密切的關(guān)系。因為數(shù)組類的元素類型要通過類加載器去創(chuàng)建啊。
驗證:
驗證是連接階段的第一步。這一階段的目的是為了確保Class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機的要求。并且不會危害虛擬機自身的安全。
java語言本身是相對安全的 語言。但是Class文件不一定是java源碼編譯來的,所以虛擬機會檢查輸入流。他要做的檢查:

  1. 文件格式驗證
  2. 元數(shù)據(jù)驗證
  3. 字節(jié)碼驗證
  4. 符號引用驗證

準(zhǔn)備:
準(zhǔn)備階段是正式為類變量分配內(nèi)存并設(shè)置類變量初始值的階段。這些變量所使用的內(nèi)存都會在方法去中進行分配。(注意,這里說的變量都是類變量。也就是static修飾的)
這里有個很有意思的事:比如一個類中private static int value = 123;在準(zhǔn)備階段這個value的值是0而不是123.因為在初始化的時候才會把value賦值為123.但是如果這個value是final的,則準(zhǔn)備階段直接賦值123 了。
解析:
解析階段是虛擬機將常量池內(nèi)的符號引用替換為直接引用的過程。

在java中,一個java類將會編譯成一個class文件。在編譯時,java類并不知道引用類的實際內(nèi)存地址,因此只能使用符號引用來代替。比如org.simple.People類引用org.simple.Tool類,在編譯時People類并不知道Tool類的實際內(nèi)存地址,因此只能使用符號org.simple.Tool(假設(shè))來表示Tool類的地址。而在類裝載器裝載People類時,此時可以通過虛擬機獲取Tool類 的實際內(nèi)存地址,因此便可以既將符號org.simple.Tool替換為Tool類的實際內(nèi)存地址,及直接引用地址。
總結(jié):JVM對于直接引用和符號引用的處理是有區(qū)別的,可以看到符號引用時,JVM將使用StringBuilder來完成字符串的 添加,而直接引用時則直接使用String來完成;直接引用永遠比符號引用效率更快,但實際應(yīng)用開發(fā)中不可能全用直接引用,要提高效能可以考慮按虛擬機的思維來編寫你的程序。
參考地址

初始化:
類初始化階段是類加載過程的最后一步。前面類加載過程中,除了在加載階段用戶應(yīng)用程序可以通過自定義類加載器參與外,其余動作都是虛擬機控制和主導(dǎo)。到了初始化階段,才真正開始執(zhí)行類中定義的java程序代碼。
在準(zhǔn)備階段我們的變量已經(jīng)被賦一次值了。但是這個是系統(tǒng)要求的初始值。而在初始化階段,變量的值變成我們程序中指定的值。

類加載器

虛擬機中的“通過一個類的全限定名來獲取定義此類的二進制字節(jié)流。這個動作放到j(luò)ava虛擬機外部去實現(xiàn)。以便應(yīng)用程序自己決定如何去獲取所需要的類。實現(xiàn)這個動作的代碼模塊稱為”類加載器“。
類與類加載器
類加載器只用于實現(xiàn)類的加載動作。但是他在java程序中的作用卻遠遠不限于類加載階段。對于任意一個類,都需要由加載它的類加載器和這個類本身一同確立其在java虛擬機中的唯一性。每一個類加載器都有獨立的類名稱空間。這句話換個說法:比較兩個類是否相等只有在這兩個類是由同一個類加載器加載的前提下才有意義。否則都沒必要比較,必定不等。
雙親委派模型
從java虛擬機的角度來講只存在兩種不同的類加載器:一種是啟動類加載器,另一種就是所有其他的類加載器。
其他的類加載器都是java語言實現(xiàn),獨立于虛擬機外部,并且全都繼承自抽象類:java.long.ClassLoader。

絕大部分Java程序都會使用到以下3種系統(tǒng)提供的類加載器:

  • 啟動類加載器(Bootstrap ClassLoader)
  • 應(yīng)用程序類加載器(Application ClassLoader)
  • 擴展類加載器(Extension ClassLoader)

雙親委派模型要求除了頂層的啟動類加載器外,其余的類加載器都應(yīng)當(dāng)有自己的父類加載器。
雙親委派模型的工作過程是:
如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成。
每一個層次的類加載器都是如此。因此,所有的加載請求最終都應(yīng)該傳送到頂層的啟動類加載器中。
只有當(dāng)父加載器反饋自己無法完成這個加載請求時(搜索范圍中沒有找到所需的類),子加載器才會嘗試自己去加載。
使用雙親委派模型來組織類加載器之間的關(guān)系,有一個顯而易見的好處:類隨著它的類加載器一起具備了一種帶有優(yōu)先級的層次關(guān)系。
例如類java.lang.Object,它由啟動類加載器加載。雙親委派模型保證任何類加載器收到的對java.lang.Object的加載請求,最終都是委派給處于模型最頂端的啟動類加載器進行加載,因此Object類在程序的各種類加載器環(huán)境中都是同一個類。
相反,如果沒有使用雙親委派模型,由各個類加載器自行去加載的話,如果用戶自己編寫了一個稱為java.lang.Object的類,并用自定義的類加載器加載,那系統(tǒng)中將會出現(xiàn)多個不同的Object類,Java類型體系中最基礎(chǔ)的行為也就無法保證,應(yīng)用程序也將會變得一片混亂。
雙薪委派模型對于保證java小恒徐的穩(wěn)定運做很重要,但是它的實現(xiàn)很簡單:

  1. 首先,檢查目標(biāo)類是否已在當(dāng)前類加載器的命名空間中加載。
  2. 如果沒有找到,則嘗試將請求委托給父類加載器(如果指定父類加載器為null,則將啟動類加載器作為父類加載器;如果沒有指定父類加載器,則將應(yīng)用程序類加載器作為父類加載器),最終所有類都會委托到啟動類加載器。
  3. 如果父類加載器加載失敗,則自己加載。
  4. 默認resolve取false,不需要解析,直接返回。

破壞雙親委派模型
上文說到了雙親委派模型不是一個強制性的約束模型。而是java設(shè)計者推薦給開發(fā)者的類加載器實現(xiàn)方式。在java的世界大部分類加載器都遵循這個模型。但是也有例外的。 雙親委派模型主要出現(xiàn)過三次較大規(guī)模的“被破壞”情況。

  • 雙親委派模型的第一次“被破壞”其實發(fā)生在雙親委派模型出現(xiàn)之前--即JDK1.2發(fā)布之前。由于雙親委派模型是在JDK1.2之后才被引入的,而類加載器和抽象類java.lang.ClassLoader則是JDK1.0時候就已經(jīng)存在,面對已經(jīng)存在 的用戶自定義類加載器的實現(xiàn)代碼,Java設(shè)計者引入雙親委派模型時不得不做出一些妥協(xié)。為了向前兼容,JDK1.2之后的java.lang.ClassLoader添加了一個新的proceted方法findClass(),在此之前,用戶去繼承java.lang.ClassLoader的唯一目的就是重寫loadClass()方法,因為虛擬在進行類加載的時候會調(diào)用加載器的私有方法loadClassInternal(),而這個方法的唯一邏輯就是去調(diào)用自己的loadClass()。JDK1.2之后已不再提倡用戶再去覆蓋loadClass()方法,應(yīng)當(dāng)把自己的類加載邏輯寫到findClass()方法中,在loadClass()方法的邏輯里,如果父類加載器加載失敗,則會調(diào)用自己的findClass()方法來完成加載,這樣就可以保證新寫出來的類加載器是符合雙親委派模型的。
  • 雙親委派模型的第二次“被破壞”是這個模型自身的缺陷所導(dǎo)致的,雙親委派模型很好地解決了各個類加載器的基礎(chǔ)類統(tǒng)一問題(越基礎(chǔ)的類由越上層的加載器進行加載),基礎(chǔ)類之所以被稱為“基礎(chǔ)”,是因為它們總是作為被調(diào)用代碼調(diào)用的API。但是,如果基礎(chǔ)類又要調(diào)用用戶的代碼,那該怎么辦呢。
    這并非是不可能的事情,一個典型的例子便是JNDI服務(wù),它的代碼由啟動類加載器去加載(在JDK1.3時放進rt.jar),但JNDI的目的就是對資源進行集中管理和查找,它需要調(diào)用獨立廠商實現(xiàn)部部署在應(yīng)用程序的classpath下的JNDI接口提供者(SPI, Service Provider Interface)的代碼,但啟動類加載器不可能“認識”之些代碼,該怎么辦?
    為了解決這個困境,Java設(shè)計團隊只好引入了一個不太優(yōu)雅的設(shè)計:線程上下文件類加載器(Thread Context ClassLoader)。這個類加載器可以通過java.lang.Thread類的setContextClassLoader()方法進行設(shè)置,如果創(chuàng)建線程時還未設(shè)置,它將會從父線程中繼承一個;如果在應(yīng)用程序的全局范圍內(nèi)都沒有設(shè)置過,那么這個類加載器默認就是應(yīng)用程序類加載器。了有線程上下文類加載器,JNDI服務(wù)使用這個線程上下文類加載器去加載所需要的SPI代碼,也就是父類加載器請求子類加載器去完成類加載動作,這種行為實際上就是打通了雙親委派模型的層次結(jié)構(gòu)來逆向使用類加載器,已經(jīng)違背了雙親委派模型,但這也是無可奈何的事情。Java中所有涉及SPI的加載動作基本上都采用這種方式,例如JNDI,JDBC,JCE,JAXB和JBI等。
  • 雙親委派模型的第三次“被破壞”是由于用戶對程序的動態(tài)性的追求導(dǎo)致的,例如OSGi的出現(xiàn)。在OSGi環(huán)境下,類加載器不再是雙親委派模型中的樹狀結(jié)構(gòu),而是進一步發(fā)展為網(wǎng)狀結(jié)構(gòu)。
本章小結(jié)

介紹了類加載過程的“加載”,“驗證”,“準(zhǔn)備”,“解析”和“初始化”五個階段中的虛擬機的動作。還介紹了類加載器的工作原因及其對虛擬機的意義。

全文手打不易(我引用中的內(nèi)用是別人的帖子,我注明出處鏈接了。剩下的都是照著書一個字一個字敲的。雖然看起來內(nèi)容有的一樣)。如果你覺得稍微幫到了你一點點,請點個喜歡點個關(guān)注。有不同意見或者問題的歡迎評論或者私信。祝大家工作生活都順順利利吧。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評論 6 532
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,978評論 2 374

推薦閱讀更多精彩內(nèi)容