老馬的JVM筆記(四)----虛擬機類加載機制

Class文件現在是有了,JVM怎麼根據Class文件把內容加載出來?接口或類都要在這裏被加載。

4.1 類加載時機

(!)類的生命週期:

+-----------------------------------------------------------------------------------------------------------------------------+

|  加載(loading) -->  驗證(verification) --> 準備(preparation) --> 解析(resolution) -->   |

|  初始化(initialization) --> 使用(using) --> 卸載(unloading)                                               | 

+-----------------------------------------------------------------------------------------------------------------------------+

4.1.1 加載

在遇到new,getstatic,putstatic,invokestatic指令時,如果該類沒有被初始化過,那就先初始化。final修飾的變量會被放在常量池,這個不重要。

當觸發反射機制,調用reflect包時,也要將該類初始化。

在初始化一個類時,如果其父類沒有被初始化過,初始化。這回扯出來一個家譜,那就都初始化。且最先初始化輩分大的。但接口不然,用到哪個祖宗加載哪個祖宗,沒用的不加載。

在虛擬機啓動時最先初始化包含main方法的類,主類。

Methodhandler實例最後的解析結果爲REF_getStatic...等方法句柄,並且這個方法句柄對應的類沒有被初始化,需要先觸發它的初始化。(*)

除以上之外,就算“用”了該類,也不會加載他。比如其實是訪問其父類屬性,沒有顯示new一個該類的對象,那隻會加載父類,不會加載該類。

加載階段,虛擬機通過類的權限定名獲取類的字節流,將字節流的靜態存儲結構轉化爲方法區的運行時數據結構,最後在內存中生成一個Class對象用於訪問該類。這是說加載類,不是加載對象。是在可能用到什麼類時,獲取內容好可以使用其中的屬性和方法。加載數組類就是一層層扒下來,直到把到可用的引用類型或基本變量類型。

4.1.2 驗證

主要驗證Class文件中的信息是否符合虛擬機的要求。正常Java語言啥問題沒有,但是Class文件是編譯出來的,會有問題。都驗證啥?

1.文件格式:0xCAFEBABE,版本號,常量池中常量類型是不是都被虛擬機支持...很多。反正是虛擬機查不是你查。

2.元數據:查父類,家譜上是否有不允許繼承的類,是否實現了接口所需要的所有方法,是否與長輩們有字段矛盾衝突...

3.字節碼:檢查字節碼的數據流,控制流,用於確認語義的合法合理性。包含的方面很多,控制流是否跳轉到方法外,類型是否合邏輯。這要求的東西很多,而且涉及到path coverage問題,不可能全走完。所以虛擬機提供了StackMapTable屬性,用於記錄一些“會安全”的代碼塊來節省時間。

4.符號引用:驗證引用該類以外的各種信息的引用。是否能訪問到對應的類,在該類中是否存在想要訪問的方法和變量,該類中想訪問的變量與方法是否可以被訪問...

4.1.3 準備

準備階段需要給類中的變量開闢內存,並且給變量初步賦初始值。初步賦值的意思是先讓值佔個位,都用0或null或其他占上,而不是賦給他字面量。

4.1.4 解析

將符號引用替換成直接引用。

符號引用(Symbolic References):標記要引用的目標,用什麼都可以,只要能標識到目標就可以,暫時不需要把引用目標加載到內存中。

直接引用(Direct References):直接指向引用的目標,已經在內存中存在。

解析目標:類或接口,字段,類的方法,接口的方法,方法類型,方法句柄,調用點限定符。

1.類或接口:非數組,即引用類的加載器加載被引用類(A類中B引用C類,A加載器加載C)。與前文類似,加載的時候要加載一個家譜,每個都要成功纔可以。如果是數組,就一層層扒,扒到基本類型或可以直接用的。

2.字段:先解析該字段所在的類或接口C。如果C中直接有目標字段,直接返回直接引用;如果沒有就在實現的接口裏一個一個找,接口再一輩一輩找,直到找到目標字段;如果找不到就開始查家譜,從父類開始一輩輩地向上查找;如果再沒有,那就是真沒有,失敗返回。找到了,如果不能訪問,也失敗返回。如果同一字段同時出現在接口和父類中出現,或者同時在多個接口中出現,拒絕編譯。

3.類方法:思路不同。先解析該類或接口C。如果在類方法表中發現C是接口,直接拋異常(畢竟是類方法解析);如果“類"C中存在目標方法,返回直接引用;如果沒有,就先找家譜,查到有目標方法,返回直接引用;如果家譜裏沒有,去接口裏找,接口裏也是一輩一輩找;再沒有那就是真沒有,返回失敗。

4.接口方法:又一個思路。先解析該類或接口C。爲什麼總說類或接口,不能直接分開呢?那肯定是不能了。class_index裏類和接口放一起了。如果解析出是類,報錯;如果在C接口中找到目標方法,返回直接引用;如果沒有,如父接口中找,一輩一輩找;如果再沒有就失敗返回。接口裏的方法一定是public的,權限一定沒問題。

4.1.5 初始化

這是正經變量初始化。在準備階段,我們已經給變量開闢了空間,所以在這裏其實已經有了給變量的內存和初始賦值,所以即使是在聲明變量前發生了變量賦值,也是合理的,前提是要發生在靜態塊中,因爲類會先編譯靜態區(static{})。但這種不合常識的行爲,只有賦值,不能訪問。當然這種靜態塊用得也不多。

4.2 類加載器

自寫類加載器,就是用java語言自己代替jvm的“根據全限定名找到對應類的字節流”。兩個類相同的前提就是要由同一個加載器加載出來。Java虛擬機支持兩種類加載器,一種是啓動類加載器,Bootstrap ClassLoader,是虛擬機自己的加載器,用C++實現。另一種就是自己用Java實現的類加載器,需要繼承java.lang.ClassLoader。

細緻一點,Java程序使用到的類加載器有三種:啓動類加載器,擴展類加載器(Extension ClassLoader),應用程序類加載器(Application ClassLoader)(程序中的默認類加載器)。如果自己實現類加載器,自定義的類加載器需要依賴應用程序類加載器->擴展類加載器->啓動類加載器。這裏的依賴不算繼承,算組合(composition),複用父類加載器的代碼。這個鏈條關係叫雙親委派模型(Parents Delegation Model)。意思就是我不行,我得找我爹。在加載一個類的時候,加載器會層層找上級,直到找到啓動類加載器。祖宗說不行,那就向下查找,直到找到一個能加載該類的長輩。這樣的好處是如果一個類很多加載器都可以加載,我們可以保證他被最祖宗的一個加載出來,不會產生歧義。這是一個規定,不是什麼死要求,不這樣也不是不行。所以這樣的問題也就是,如果我想寫一個基礎類的加載器,因爲太基礎了,所以我的加載器根本沒法被使用。一些已有框架就想辦法突破了這種規定。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章