JVM 是如何加載 Java 類的?


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


