JVM 是如何加載 Java 類(lèi)的?


看到這個(gè)題目的時(shí)候,你可能就會(huì)覺(jué)得,阿粉,這不是挺簡(jiǎn)單的一個(gè)問(wèn)題么
如何加載?不就是 加載,鏈接,初始化 這三步嘛,說(shuō)白了不就是類(lèi)加載過(guò)程么
那么,你知道這三步具體又做了什么嘛?這就是本篇文章想要寫(xiě)的
加載加載的過(guò)程,就是查找字節(jié)流,并根據(jù)查找到的字節(jié)流來(lái)創(chuàng)建類(lèi)的一個(gè)過(guò)程
Java 語(yǔ)言的類(lèi)型可以分成兩大類(lèi):基本類(lèi)型和引用類(lèi)型。基本類(lèi)型就是由 JVM 預(yù)先定義好的,所以也就沒(méi)有查找字節(jié)流這一說(shuō)了
對(duì)于引用類(lèi)型來(lái)說(shuō)的話,又可以細(xì)分為四種:類(lèi),接口,數(shù)組類(lèi)和泛型參數(shù)。因?yàn)榉盒蛥?shù)在編譯過(guò)程中會(huì)被擦除,所以在 JVM 中就只有前三種。而數(shù)組類(lèi)又是由 JVM 直接生成的,所以查找字節(jié)流的話,就只有類(lèi)和接口了。
那么 JVM 是怎么查找字節(jié)流的呢?如果你對(duì)這塊內(nèi)容比較熟的話,應(yīng)該就能想起來(lái)類(lèi)加載器,它主要有四類(lèi):?jiǎn)?dòng)類(lèi)加載器,擴(kuò)展類(lèi)加載器,應(yīng)用程序類(lèi)加載器和用戶(hù)自定義類(lèi)加載器
這塊又有個(gè)知識(shí)點(diǎn)就是雙親委派機(jī)制:大概就是如果一個(gè)類(lèi)加載器收到了類(lèi)加載的請(qǐng)求,首先不會(huì)自己去加載這個(gè)類(lèi),而是把這個(gè)請(qǐng)求委派給父類(lèi)加載器去完成。通過(guò)雙親委派機(jī)制就能保證同樣一個(gè)類(lèi)只被加載一次
經(jīng)過(guò)類(lèi)加載器之后,這個(gè)類(lèi)就算是加載進(jìn)來(lái)了
鏈接對(duì)鏈接過(guò)程而言, jvm 實(shí)現(xiàn)具有靈活性,但必須保留下列屬性:
1、在鏈接之前,類(lèi)或者接口必須已經(jīng)被完全加載;
2、在初始化之前,類(lèi)或者接口必須已經(jīng)被完全驗(yàn)證和準(zhǔn)備;
3、鏈接過(guò)程中檢測(cè)到的程序錯(cuò)誤會(huì)拋出到程序中某個(gè)位置,在該位置上,程序?qū)⒉扇∧承┎僮鳎@些操作可能會(huì)直接或間接地鏈接到類(lèi)或者接口所涉及到的類(lèi)或者接口。
鏈接這塊又分為三部分:驗(yàn)證,準(zhǔn)備,解析
驗(yàn)證階段就是想要看看 class 文件的前 8 位是不是 java 標(biāo)識(shí)符,想看看符不符合規(guī)范什么的
準(zhǔn)備階段就是給靜態(tài)字段分配內(nèi)存。除了分配內(nèi)存之外,部分 JVM 還會(huì)在此階段構(gòu)造其他跟類(lèi)層次相關(guān)的數(shù)據(jù)結(jié)構(gòu),比如說(shuō)用來(lái)實(shí)現(xiàn)虛方法的動(dòng)態(tài)綁定的方法表,這個(gè)方法表是用來(lái)解決動(dòng)態(tài)綁定的問(wèn)題的,解析時(shí)通過(guò)這個(gè)方法表,根據(jù)實(shí)際類(lèi)型來(lái)解析獲取對(duì)應(yīng)的方法。
在 class 文件被加載到 JVM 之前,這個(gè)類(lèi)沒(méi)辦法知道其他類(lèi)和方法,字段所對(duì)應(yīng)的具體地址,甚至都不知道自己的方法,字段的地址,所以如果需要引用這些成員時(shí), Java 編譯器就會(huì)生成一個(gè)符號(hào)引用,在運(yùn)行階段,這個(gè)符號(hào)引用一般都可以準(zhǔn)確的定位到具體目標(biāo)上
解析階段主要就是將符號(hào)引用解析成實(shí)際引用。如果符號(hào)引用指向一個(gè)未被加載的類(lèi),或者沒(méi)有被加載類(lèi)的字段或方法,此時(shí)解析階段就會(huì)觸發(fā)這個(gè)類(lèi)的加載(但不一定會(huì)觸發(fā)這個(gè)類(lèi)的鏈接以及初始化)
在解析階段,不同的 JVM 有不同的解析策略,例如:
public?class?A?{
??public?void?main(String?args[])?{
????B?b?=?null;
??}
}
策略 1 :鏈接 A 的時(shí)候發(fā)現(xiàn)引用了 B,因此加載 B
策略 2 :鏈接 A 的時(shí)候發(fā)現(xiàn)引用了 B,但是 B 沒(méi)有被使用,所以暫時(shí)不加載 B。在真正使用 B 的時(shí)候才進(jìn)行加載,比如?b = new B();
所以在一些 JVM 實(shí)現(xiàn)中,可能采取在使用時(shí)才會(huì)解析類(lèi)或接口中的符號(hào)引用,或采取在該類(lèi)或者接口被驗(yàn)證時(shí)一次性解析全部符號(hào)引用。這取決于采用的是哪種策略,也意味著解析過(guò)程可能在類(lèi)或者接口被初始化后還會(huì)進(jìn)行
初始化在 Java 代碼中,如果想要初始化一個(gè)靜態(tài)字段,可以在聲明的時(shí)候直接賦值,也可以選擇在靜態(tài)代碼塊中對(duì)它賦值
如果直接賦值的靜態(tài)字段被 final 修飾了,而且這個(gè)靜態(tài)字段是基本類(lèi)型或者字符串時(shí),就會(huì)被 Java 編譯器標(biāo)記成常量值,初始化就直接被 JVM 完成了。除此之外的直接賦值操作,還有所有靜態(tài)代碼塊中的代碼,就會(huì)被 Java 編譯器放到同一個(gè)方法中,并且把它命名為?
類(lèi)加載的最后一步就是初始化,就是給標(biāo)記為常量值的字段賦值,執(zhí)行??方法的過(guò)程。這個(gè)時(shí)候 JVM 會(huì)通過(guò)加鎖來(lái)確保類(lèi)的??方法只被執(zhí)行一次
?方法可厲害了,因?yàn)椋?/p>
?方法與類(lèi)的構(gòu)造函數(shù)不同,它不需要顯示的調(diào)用父類(lèi)的??方法,虛擬機(jī)會(huì)保證在子類(lèi)的??方法執(zhí)行之前,父類(lèi)的??方法已經(jīng)執(zhí)行完畢。因此在虛擬機(jī)中第一個(gè)被執(zhí)行的??方法的類(lèi)肯定是 java.lang.Object?方法對(duì)于類(lèi)或接口來(lái)說(shuō)不是必需的,如果一個(gè)類(lèi)中沒(méi)有靜態(tài)語(yǔ)句塊,也沒(méi)有對(duì)變量的賦值操作,那么編譯器可以不為這個(gè)類(lèi)生成??方法。接口中不能使用靜態(tài)初始化塊,但是仍有 static 變量的賦值操作,所以也會(huì)有?
?方法,但是接口執(zhí)行??方法不需要先執(zhí)行父接口的??方法。只有當(dāng)父接口中定義的變量被使用到時(shí),才會(huì)執(zhí)行??方法。虛擬機(jī)會(huì)保證一個(gè)類(lèi)的?
?方法在多線程環(huán)境中被正確的加鎖、同步,如果多個(gè)線程同時(shí)去初始化一個(gè)類(lèi),那么只會(huì)有一個(gè)線程去執(zhí)行這個(gè)類(lèi)的??方法,其它線程都需要阻塞等待
?方法執(zhí)行之后, JVM 才算成功的加載了 Java 類(lèi)
那么,類(lèi)的初始化什么時(shí)候會(huì)被觸發(fā)呢?
JVM 規(guī)范列舉了以下幾種觸發(fā)情況:
1 , 當(dāng)虛擬機(jī)啟動(dòng)時(shí),初始化用戶(hù)指定的主類(lèi);
2 ,當(dāng)遇到用以新建目標(biāo)類(lèi)實(shí)例的 new 指令時(shí),初始化 new 指令的目標(biāo)類(lèi);
3 ,當(dāng)遇到調(diào)用靜態(tài)方法的指令時(shí),初始化該靜態(tài)方法所在的類(lèi);
4 ,當(dāng)遇到訪問(wèn)靜態(tài)字段的指令時(shí),初始化該靜態(tài)字段所在的類(lèi);
5 ,子類(lèi)的初始化會(huì)觸發(fā)父類(lèi)的初始化;
6 ,如果一個(gè)接口定義了 default 方法,那么直接實(shí)現(xiàn)或者間接實(shí)現(xiàn)該接口的類(lèi)的初始化,會(huì)觸發(fā)該接口的初始化;
7 ,使用反射 API 對(duì)某個(gè)類(lèi)進(jìn)行反射調(diào)用時(shí),初始化這個(gè)類(lèi);
8 ,當(dāng)初次調(diào)用 MethodHandle 實(shí)例時(shí),初始化該 MethodHandle 指向的方法所在的類(lèi)
再談 雙親委派機(jī)制在上面類(lèi)加載機(jī)制那塊,提了一下雙親委派機(jī)制
我覺(jué)得之所以有這樣的機(jī)制,就是為了避免資源的浪費(fèi)。上面的雙親委派機(jī)制我們?cè)诂F(xiàn)實(shí)中也可以找到例子,比如說(shuō):公司部門(mén)有位程序員 A 發(fā)現(xiàn)如果做一個(gè)數(shù)據(jù)系統(tǒng)的話,來(lái)把公司各部門(mén)的數(shù)據(jù)打通,這樣就可以減少很多交流成本,那么他可能就會(huì)和老大去說(shuō),申請(qǐng)去做這個(gè)系統(tǒng),老大一看,這個(gè)方案完全可以抽成公共的呀,就自己去寫(xiě)了(父類(lèi)加載公共方法),也可能老大一看,你就自己去寫(xiě)吧(父類(lèi)不加載時(shí),子類(lèi)再進(jìn)行加載),更巧的是,程序員 B 也發(fā)現(xiàn)了,他也去找老大說(shuō),這個(gè)時(shí)候老大會(huì)說(shuō)什么呢?這個(gè)事情 A 去做了,就不用太擔(dān)心了
那如果程序員 A 和 B 發(fā)現(xiàn)了之后沒(méi)有和老大交流,都自己悶頭去做了,這樣的話,同樣的系統(tǒng)做了兩遍,還浪費(fèi)了兩個(gè)人的時(shí)間精力,由此造成的資源浪費(fèi)太大了
我覺(jué)得雙親委派的機(jī)制類(lèi)似于這樣,因?yàn)檫@個(gè)機(jī)制的存在,讓資源浪費(fèi)的現(xiàn)象大大減少了
但是 tomcat 打破了這種機(jī)制,這怎么說(shuō)?
我們都知道 tomcat 是個(gè) web 容器,那么它應(yīng)該:
支持部署兩個(gè)應(yīng)用程序,不同的應(yīng)用程序可能會(huì)依賴(lài)同一個(gè)第三方類(lèi)庫(kù)的不同版本,就比如兩個(gè)應(yīng)用程序,其中一個(gè)依賴(lài)的是一個(gè)類(lèi)庫(kù)的 v1.0 ,另外一個(gè)依賴(lài)的是同樣一個(gè)類(lèi)庫(kù)的 v2.0 ,那么 tomcat 是不是應(yīng)該允許這個(gè)類(lèi)庫(kù)的 1.0 和 2.0 版本都存在?
部署在同一個(gè) web 容器中相同的類(lèi)庫(kù)相同的版本是應(yīng)該可以共享的。就比如,服務(wù)器上有 100 個(gè)應(yīng)用程序,這些程序依賴(lài)的都是相同的類(lèi)庫(kù),那 tomcat 總不能把這 100 份相同的類(lèi)庫(kù)都加載到虛擬機(jī)里面去吧,要是非要加載進(jìn)去,那服務(wù)器不得分分鐘炸了
web 容器需要支持 jsp 文件的修改,也就是說(shuō),當(dāng)程序運(yùn)行之后,我對(duì) jsp 文件進(jìn)行了修改,那么 tomcat 是不是也應(yīng)該支持?如果不支持的話,那我修改一次就不能用了,不合適吧?
基于上面三點(diǎn),就能看到 tomcat 其實(shí)是打破了雙親委派機(jī)制的
比如第一個(gè)問(wèn)題,第三方類(lèi)庫(kù)就是同樣一個(gè)資源,在雙親委派機(jī)制中,同樣一個(gè)資源是不應(yīng)該加載兩次的,但是在 tomcat 里面卻被允許了;但是第二個(gè)問(wèn)題好像又在說(shuō)雙親委派的機(jī)制,正是因?yàn)殡p親委派機(jī)制的存在,所以第二個(gè)問(wèn)題就不是問(wèn)題了嘛;第三個(gè)問(wèn)題又打破了雙親委派機(jī)制,因?yàn)槿绻淮蚱频脑挘瓉?lái)的 jsp 文件已經(jīng)加載進(jìn)來(lái)了,現(xiàn)在對(duì)它進(jìn)行了修改,那么應(yīng)該還會(huì)加載原來(lái)的 jsp 文件,這樣的話修改豈不是無(wú)效了?
所以, tomcat 打破了雙親委派機(jī)制,但并不是完全打破
至于 tomcat 打破雙親委派的機(jī)制,阿粉還沒(méi)搞懂,等阿粉搞懂了再來(lái)寫(xiě)吧
或者你搞懂了嘛?給阿粉講講好不好~
參考:極客時(shí)間 – 深入拆解 Java 虛擬機(jī)
End
如果大家喜歡我們的文章,歡迎大家轉(zhuǎn)發(fā),點(diǎn)擊在看讓更多的人看到。


