代碼編譯的結(jié)果從本地機(jī)器碼轉(zhuǎn)變?yōu)樽止?jié)碼售碳,是存儲格式發(fā)展的一小步句喷,卻是編程語言發(fā)展的一大步亲怠。
虛擬機(jī)把描述”類“的數(shù)據(jù)從Class文件加載到內(nèi)存若皱,并對數(shù)據(jù)進(jìn)行校驗(yàn)镊叁,轉(zhuǎn)換解析和初始化有梆,最終形成可以被虛擬機(jī)直接使用的java類型,這就是虛擬機(jī)的類加載機(jī)制意系。
在java語言里泥耀,類型的加載,連接和初始化都是在程序運(yùn)行期間完成的蛔添,這種策略雖然會(huì)令類加載時(shí)稍加一些性能開銷痰催,但是會(huì)為java應(yīng)用程序提供高度的靈活性。java的動(dòng)態(tài)擴(kuò)展就是依據(jù)運(yùn)行期動(dòng)態(tài)加載和動(dòng)態(tài)連接這個(gè)特點(diǎn)實(shí)現(xiàn)的迎瞧。
其實(shí)拆開了講就是編寫一個(gè)面向接口的應(yīng)用程序夸溶,運(yùn)行的時(shí)候再指定實(shí)現(xiàn)類。用戶通過預(yù)定義和自定義類加載器凶硅,讓應(yīng)用程序再運(yùn)行時(shí)再加載那個(gè)實(shí)現(xiàn)類的二進(jìn)制流作為程序代碼的一部分缝裁。(這個(gè)是我的理解。原話比較復(fù)雜而且不太好理解足绅。我反正個(gè)人覺得我這么說簡單多了捷绑。如果理解的有不對的地方歡迎指出。)
類加載的時(shí)機(jī)
類從被加載到虛擬機(jī)內(nèi)存中開始氢妈,到卸載出內(nèi)存為止粹污。它的整個(gè)生命周期包括:
- 加載
- 驗(yàn)證
- 準(zhǔn)備
- 解析
- 初始化
- 使用
- 卸載
七個(gè)階段。其中驗(yàn)證首量,準(zhǔn)備壮吩,解析三個(gè)部分統(tǒng)稱為連接。
加載加缘,驗(yàn)證鸭叙,準(zhǔn)備漓柑,初始化和卸載這五個(gè)順序的確定的兔院,類的加載過程必須按照這種順序按部就班的開始。而解析階段卻不一定悲雳。在某些情況下可以在初始化之后進(jìn)行蚀浆。這是為了支持java語言的動(dòng)態(tài)綁定缀程。
什么情況下開始類的加載呢?java虛擬機(jī)規(guī)范沒有強(qiáng)制約束市俊,一般由虛擬機(jī)的具體實(shí)現(xiàn)來自由把握杨凑。但是對于初始化階段,虛擬機(jī)規(guī)范是嚴(yán)格規(guī)定了有且只有五種情況必須立即對類進(jìn)行初始化:
new關(guān)鍵字實(shí)例化對象摆昧,讀取或設(shè)置一個(gè)類的靜態(tài)字段以及調(diào)用一個(gè)類的靜態(tài)方法時(shí)撩满。如果這個(gè)類沒有進(jìn)行初始化,則要先初始化。
使用java.lang.reflect進(jìn)行反射調(diào)用的時(shí)候伺帘,如果這個(gè)類沒有進(jìn)行初始化昭躺,則要先初始化。
當(dāng)初始化一個(gè)類伪嫁,發(fā)現(xiàn)其父類沒有進(jìn)行初始化领炫,則要先初始化其父類。
虛擬機(jī)啟動(dòng)時(shí)张咳,用戶需要指定一個(gè)執(zhí)行的主類(包含main方法的那個(gè)類)帝洪,虛擬機(jī)會(huì)先初始化這個(gè)主類。
(這個(gè)我沒太看懂脚猾,查了一些資料差不多就是類似反射的Method調(diào)用類的靜態(tài)塊要先初始化)JDK1.7的動(dòng)態(tài)語言支持時(shí)葱峡,一個(gè)java.lang.invoke.MethodHandle實(shí)例最后的解析結(jié)果是REF_getStatic,REF_putStatic龙助,REF_invokeStatic的方法句柄砰奕,并且這個(gè)方法對應(yīng)的類沒有進(jìn)行初始化,則要先初始化提鸟。
java7在JSR 292中增加了對動(dòng)態(tài)類型語言的支持军援,使java也可以像C語言那樣將方法作為參數(shù)傳遞,其實(shí)現(xiàn)在lava.lang.invoke包中沽一。MethodHandle作用類似于反射中的Method類盖溺,但它比Method類要更加靈活和輕量級。通過MethodHandle進(jìn)行方法調(diào)用一般需要以下幾步:
(1)創(chuàng)建MethodType對象铣缠,指定方法的簽名;
(2)在MethodHandles.Lookup中查找類型為MethodType的MethodHandle昆禽;
(3)傳入方法參數(shù)并調(diào)用MethodHandle.invoke或者M(jìn)ethodHandle.invokeExact方法蝗蛙。
MethodType
可以通過MethodHandle類的type方法查看其類型,返回值是MethodType類的對象醉鳖。也可以在得到MethodType對象之后,調(diào)用MethodHandle.asType(mt)方法適配得到MethodHandle對象盗棵。可以通過調(diào)用MethodType的靜態(tài)方法創(chuàng)建MethodType實(shí)例纹因,有三種創(chuàng)建方式:
(1)methodType及其重載方法:需要指定返回值類型以及0到多個(gè)參數(shù);
(2)genericMethodType:需要指定參數(shù)的個(gè)數(shù)屯曹,類型都為Object;
(3)fromMethodDescriptorString:通過方法描述來創(chuàng)建。
創(chuàng)建好MethodType對象后恶耽,還可以對其進(jìn)行修改密任,MethodType類中提供了一系列的修改方法,比如:changeParameterType偷俭、changeReturnType等浪讳。
Lookup
MethodHandle.Lookup相當(dāng)于MethodHandle工廠類,通過findxxx方法可以得到相應(yīng)的MethodHandle涌萤,還可以配合反射API創(chuàng)建MethodHandle驻债,對應(yīng)的方法有unreflect、unreflectSpecial等形葬。
invoke
在得到MethodHandle后就可以進(jìn)行方法調(diào)用了合呐,有三種調(diào)用形式:
(1)invokeExact:調(diào)用此方法與直接調(diào)用底層方法一樣,需要做到參數(shù)類型精確匹配笙以;
(2)invoke:參數(shù)類型松散匹配淌实,通過asType自動(dòng)適配;
(3)invokeWithArguments:直接通過方法參數(shù)來調(diào)用猖腕。其實(shí)現(xiàn)是先通過genericMethodType方法得到MethodType拆祈,再通過MethodHandle的asType轉(zhuǎn)換后得到一個(gè)新的MethodHandle,最后通過新MethodHandle的invokeExact方法來完成調(diào)用倘感。
原文:https://blog.csdn.net/aesop_wubo/article/details/48858931
然后對于這物種出發(fā)類進(jìn)行初始化的場景放坏,是有且只有!除了這五種之外所有引用類的方式都不會(huì)出發(fā)初始化老玛,稱之為被動(dòng)引用淤年。
接口的加載過程與類的加載過程有一些不同。針對接口需要做一些特殊說明:
接口也有初始化過程蜡豹,這點(diǎn)與類是一致的麸粮。接口與類初始化的最大的不同是上面第三點(diǎn):當(dāng)初始化一個(gè)類,發(fā)現(xiàn)其父類沒有進(jìn)行初始化镜廉,則要先初始化其父類弄诲。接口不要求初始化其父類接口。只有在真正使用到父接口的時(shí)候才會(huì)初始化娇唯。
類加載過程
加載:
加載是”類加載“過程的一個(gè)階段齐遵。加載階段虛擬機(jī)做三件事:
- 通過一個(gè)類的全限定名來獲取定義此類的二進(jìn)制字節(jié)流。
- 將這個(gè)字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化成為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)塔插。
- 在內(nèi)存中生成一個(gè)代表這個(gè)類的java.lang.Class對象梗摇。作為方法去這個(gè)類的各種數(shù)據(jù)的訪問入口留美。
(我個(gè)人簡單的理解:1,通過名字找到類逢倍,2较雕,把這個(gè)類存起來亮蒋。3慎玖,有人訪問這個(gè)類根據(jù)名字去找存起來的趁怔。)
這個(gè)從哪里獲取沒有明確的規(guī)定润努。從zip包讀取铺浇,就是我們jar鳍侣,war拱她,ear的基礎(chǔ)。從網(wǎng)絡(luò)中獲取典型的應(yīng)用Applet桶雀,運(yùn)行時(shí)計(jì)算生成就是動(dòng)態(tài)代理技術(shù)的原理矗积。棘捣。。還有好多例子测砂,我就不一一說了砌些。
數(shù)組類而言存璃,本身不通過類加載器創(chuàng)建纵东。它是由于java虛擬機(jī)直接創(chuàng)建的偎球。但數(shù)組與類加載器仍然有很密切的關(guān)系示姿。因?yàn)閿?shù)組類的元素類型要通過類加載器去創(chuàng)建啊栈戳。
驗(yàn)證:
驗(yàn)證是連接階段的第一步子檀。這一階段的目的是為了確保Class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求。并且不會(huì)危害虛擬機(jī)自身的安全亩进。
java語言本身是相對安全的 語言归薛。但是Class文件不一定是java源碼編譯來的主籍,所以虛擬機(jī)會(huì)檢查輸入流千元。他要做的檢查:
- 文件格式驗(yàn)證
- 元數(shù)據(jù)驗(yàn)證
- 字節(jié)碼驗(yàn)證
- 符號引用驗(yàn)證
準(zhǔn)備:
準(zhǔn)備階段是正式為類變量分配內(nèi)存并設(shè)置類變量初始值的階段幸海。這些變量所使用的內(nèi)存都會(huì)在方法去中進(jìn)行分配。(注意,這里說的變量都是類變量袜硫。也就是static修飾的)
這里有個(gè)很有意思的事:比如一個(gè)類中private static int value = 123父款;在準(zhǔn)備階段這個(gè)value的值是0而不是123.因?yàn)樵诔跏蓟臅r(shí)候才會(huì)把value賦值為123.但是如果這個(gè)value是final的憨攒,則準(zhǔn)備階段直接賦值123 了阀参。
解析:
解析階段是虛擬機(jī)將常量池內(nèi)的符號引用替換為直接引用的過程蛛壳。
在java中衙荐,一個(gè)java類將會(huì)編譯成一個(gè)class文件忧吟。在編譯時(shí)溜族,java類并不知道引用類的實(shí)際內(nèi)存地址煌抒,因此只能使用符號引用來代替。比如org.simple.People類引用org.simple.Tool類贩疙,在編譯時(shí)People類并不知道Tool類的實(shí)際內(nèi)存地址屋群,因此只能使用符號org.simple.Tool(假設(shè))來表示Tool類的地址。而在類裝載器裝載People類時(shí)降狠,此時(shí)可以通過虛擬機(jī)獲取Tool類 的實(shí)際內(nèi)存地址,因此便可以既將符號org.simple.Tool替換為Tool類的實(shí)際內(nèi)存地址否纬,及直接引用地址临燃。
總結(jié):JVM對于直接引用和符號引用的處理是有區(qū)別的膜廊,可以看到符號引用時(shí)爪瓜,JVM將使用StringBuilder來完成字符串的 添加匙瘪,而直接引用時(shí)則直接使用String來完成;直接引用永遠(yuǎn)比符號引用效率更快丹喻,但實(shí)際應(yīng)用開發(fā)中不可能全用直接引用,要提高效能可以考慮按虛擬機(jī)的思維來編寫你的程序谅猾。
參考地址
初始化:
類初始化階段是類加載過程的最后一步赊瞬。前面類加載過程中巧涧,除了在加載階段用戶應(yīng)用程序可以通過自定義類加載器參與外谤绳,其余動(dòng)作都是虛擬機(jī)控制和主導(dǎo)缩筛。到了初始化階段瞎抛,才真正開始執(zhí)行類中定義的java程序代碼桐臊。
在準(zhǔn)備階段我們的變量已經(jīng)被賦一次值了断凶。但是這個(gè)是系統(tǒng)要求的初始值认烁。而在初始化階段舶沛,變量的值變成我們程序中指定的值冠王。
類加載器
虛擬機(jī)中的“通過一個(gè)類的全限定名來獲取定義此類的二進(jìn)制字節(jié)流柱彻。這個(gè)動(dòng)作放到j(luò)ava虛擬機(jī)外部去實(shí)現(xiàn)餐胀。以便應(yīng)用程序自己決定如何去獲取所需要的類卖擅。實(shí)現(xiàn)這個(gè)動(dòng)作的代碼模塊稱為”類加載器“墨技。
類與類加載器
類加載器只用于實(shí)現(xiàn)類的加載動(dòng)作扣汪。但是他在java程序中的作用卻遠(yuǎn)遠(yuǎn)不限于類加載階段冬筒。對于任意一個(gè)類舞痰,都需要由加載它的類加載器和這個(gè)類本身一同確立其在java虛擬機(jī)中的唯一性响牛。每一個(gè)類加載器都有獨(dú)立的類名稱空間娃善。這句話換個(gè)說法:比較兩個(gè)類是否相等只有在這兩個(gè)類是由同一個(gè)類加載器加載的前提下才有意義聚磺。否則都沒必要比較瘫寝,必定不等焕阿。
雙親委派模型
從java虛擬機(jī)的角度來講只存在兩種不同的類加載器:一種是啟動(dòng)類加載器暮屡,另一種就是所有其他的類加載器。
其他的類加載器都是java語言實(shí)現(xiàn)毅桃,獨(dú)立于虛擬機(jī)外部褒纲,并且全都繼承自抽象類:java.long.ClassLoader。
絕大部分Java程序都會(huì)使用到以下3種系統(tǒng)提供的類加載器:
- 啟動(dòng)類加載器(Bootstrap ClassLoader)
- 應(yīng)用程序類加載器(Application ClassLoader)
- 擴(kuò)展類加載器(Extension ClassLoader)
雙親委派模型要求除了頂層的啟動(dòng)類加載器外钥飞,其余的類加載器都應(yīng)當(dāng)有自己的父類加載器莺掠。
雙親委派模型的工作過程是:
如果一個(gè)類加載器收到了類加載的請求,它首先不會(huì)自己去嘗試加載這個(gè)類读宙,而是把這個(gè)請求委派給父類加載器去完成彻秆。
每一個(gè)層次的類加載器都是如此。因此结闸,所有的加載請求最終都應(yīng)該傳送到頂層的啟動(dòng)類加載器中唇兑。
只有當(dāng)父加載器反饋?zhàn)约簾o法完成這個(gè)加載請求時(shí)(搜索范圍中沒有找到所需的類)帕棉,子加載器才會(huì)嘗試自己去加載具则。
使用雙親委派模型來組織類加載器之間的關(guān)系,有一個(gè)顯而易見的好處:類隨著它的類加載器一起具備了一種帶有優(yōu)先級的層次關(guān)系掘猿。
例如類java.lang.Object改橘,它由啟動(dòng)類加載器加載碌识。雙親委派模型保證任何類加載器收到的對java.lang.Object的加載請求胖烛,最終都是委派給處于模型最頂端的啟動(dòng)類加載器進(jìn)行加載趟畏,因此Object類在程序的各種類加載器環(huán)境中都是同一個(gè)類猎莲。
相反身笤,如果沒有使用雙親委派模型,由各個(gè)類加載器自行去加載的話,如果用戶自己編寫了一個(gè)稱為java.lang.Object的類细疚,并用自定義的類加載器加載,那系統(tǒng)中將會(huì)出現(xiàn)多個(gè)不同的Object類姨裸,Java類型體系中最基礎(chǔ)的行為也就無法保證,應(yīng)用程序也將會(huì)變得一片混亂料身。
雙薪委派模型對于保證java小恒徐的穩(wěn)定運(yùn)做很重要祟牲,但是它的實(shí)現(xiàn)很簡單:
- 首先言询,檢查目標(biāo)類是否已在當(dāng)前類加載器的命名空間中加載。
- 如果沒有找到,則嘗試將請求委托給父類加載器(如果指定父類加載器為null,則將啟動(dòng)類加載器作為父類加載器云挟;如果沒有指定父類加載器日矫,則將應(yīng)用程序類加載器作為父類加載器)珊膜,最終所有類都會(huì)委托到啟動(dòng)類加載器。
- 如果父類加載器加載失敗,則自己加載瓦胎。
- 默認(rèn)resolve取false旧蛾,不需要解析锨天,直接返回搂赋。
破壞雙親委派模型
上文說到了雙親委派模型不是一個(gè)強(qiáng)制性的約束模型捺信。而是java設(shè)計(jì)者推薦給開發(fā)者的類加載器實(shí)現(xiàn)方式掌挚。在java的世界大部分類加載器都遵循這個(gè)模型。但是也有例外的。 雙親委派模型主要出現(xiàn)過三次較大規(guī)模的“被破壞”情況阱佛。
- 雙親委派模型的第一次“被破壞”其實(shí)發(fā)生在雙親委派模型出現(xiàn)之前--即JDK1.2發(fā)布之前。由于雙親委派模型是在JDK1.2之后才被引入的戴而,而類加載器和抽象類java.lang.ClassLoader則是JDK1.0時(shí)候就已經(jīng)存在凑术,面對已經(jīng)存在 的用戶自定義類加載器的實(shí)現(xiàn)代碼,Java設(shè)計(jì)者引入雙親委派模型時(shí)不得不做出一些妥協(xié)所意。為了向前兼容淮逊,JDK1.2之后的java.lang.ClassLoader添加了一個(gè)新的proceted方法findClass()催首,在此之前,用戶去繼承java.lang.ClassLoader的唯一目的就是重寫loadClass()方法泄鹏,因?yàn)樘摂M在進(jìn)行類加載的時(shí)候會(huì)調(diào)用加載器的私有方法loadClassInternal()郎任,而這個(gè)方法的唯一邏輯就是去調(diào)用自己的loadClass()。JDK1.2之后已不再提倡用戶再去覆蓋loadClass()方法备籽,應(yīng)當(dāng)把自己的類加載邏輯寫到findClass()方法中舶治,在loadClass()方法的邏輯里,如果父類加載器加載失敗车猬,則會(huì)調(diào)用自己的findClass()方法來完成加載霉猛,這樣就可以保證新寫出來的類加載器是符合雙親委派模型的。
- 雙親委派模型的第二次“被破壞”是這個(gè)模型自身的缺陷所導(dǎo)致的珠闰,雙親委派模型很好地解決了各個(gè)類加載器的基礎(chǔ)類統(tǒng)一問題(越基礎(chǔ)的類由越上層的加載器進(jìn)行加載)惜浅,基礎(chǔ)類之所以被稱為“基礎(chǔ)”,是因?yàn)樗鼈兛偸亲鳛楸徽{(diào)用代碼調(diào)用的API伏嗜。但是坛悉,如果基礎(chǔ)類又要調(diào)用用戶的代碼,那該怎么辦呢承绸。
這并非是不可能的事情裸影,一個(gè)典型的例子便是JNDI服務(wù),它的代碼由啟動(dòng)類加載器去加載(在JDK1.3時(shí)放進(jìn)rt.jar)军熏,但JNDI的目的就是對資源進(jìn)行集中管理和查找空民,它需要調(diào)用獨(dú)立廠商實(shí)現(xiàn)部部署在應(yīng)用程序的classpath下的JNDI接口提供者(SPI, Service Provider Interface)的代碼,但啟動(dòng)類加載器不可能“認(rèn)識”之些代碼羞迷,該怎么辦?
為了解決這個(gè)困境画饥,Java設(shè)計(jì)團(tuán)隊(duì)只好引入了一個(gè)不太優(yōu)雅的設(shè)計(jì):線程上下文件類加載器(Thread Context ClassLoader)衔瓮。這個(gè)類加載器可以通過java.lang.Thread類的setContextClassLoader()方法進(jìn)行設(shè)置,如果創(chuàng)建線程時(shí)還未設(shè)置抖甘,它將會(huì)從父線程中繼承一個(gè)热鞍;如果在應(yīng)用程序的全局范圍內(nèi)都沒有設(shè)置過,那么這個(gè)類加載器默認(rèn)就是應(yīng)用程序類加載器衔彻。了有線程上下文類加載器薇宠,JNDI服務(wù)使用這個(gè)線程上下文類加載器去加載所需要的SPI代碼,也就是父類加載器請求子類加載器去完成類加載動(dòng)作艰额,這種行為實(shí)際上就是打通了雙親委派模型的層次結(jié)構(gòu)來逆向使用類加載器澄港,已經(jīng)違背了雙親委派模型,但這也是無可奈何的事情柄沮。Java中所有涉及SPI的加載動(dòng)作基本上都采用這種方式回梧,例如JNDI,JDBC,JCE,JAXB和JBI等废岂。 - 雙親委派模型的第三次“被破壞”是由于用戶對程序的動(dòng)態(tài)性的追求導(dǎo)致的,例如OSGi的出現(xiàn)狱意。在OSGi環(huán)境下湖苞,類加載器不再是雙親委派模型中的樹狀結(jié)構(gòu),而是進(jìn)一步發(fā)展為網(wǎng)狀結(jié)構(gòu)详囤。
本章小結(jié)
介紹了類加載過程的“加載”财骨,“驗(yàn)證”,“準(zhǔn)備”藏姐,“解析”和“初始化”五個(gè)階段中的虛擬機(jī)的動(dòng)作隆箩。還介紹了類加載器的工作原因及其對虛擬機(jī)的意義。
全文手打不易(我引用中的內(nèi)用是別人的帖子包各,我注明出處鏈接了摘仅。剩下的都是照著書一個(gè)字一個(gè)字敲的。雖然看起來內(nèi)容有的一樣)问畅。如果你覺得稍微幫到了你一點(diǎn)點(diǎn)娃属,請點(diǎn)個(gè)喜歡點(diǎn)個(gè)關(guān)注。有不同意見或者問題的歡迎評論或者私信护姆。祝大家工作生活都順順利利吧矾端。