虛擬機類加載器

引言

今天在聽美團保障性平臺Rhino負責人的分享時瞭解到了Java探針技術,由於對這塊不是很清楚,遂上網查了些資料,瞭解到要想使用它,先要熟悉虛擬機的類加載機制,於是我又去複習了一遍類加載器的知識,這裏就把相關的知識點簡單總結一下(挖個坑,關於Java 探針技術等以後搞的稍微明白點再來寫)。

類加載機制

瞭解類加載器之前,我們需要先了解Java中的類是如何加載到虛擬機以及如何被虛擬機解析的,也就是虛擬機的類加載機制。

由於Java是運行時進行類的加載,所以就不能在程序開始運行之前將所有的類一股腦全部加載進虛擬機,類的加載需要一些特定的時機,比如我們 new 一個 object 的時候,這個object就會被虛擬機加載,或者讀取一個類的靜態字段的時候,還有對類進行反射調用的時候(記得嗎,使用反射時都要先獲取class對象),還有一些其他的連帶加載機制,典型的比如使用子類時發現父類未加載則會先加載父類(還記得做過的那些子父類中打印的筆試題嗎),還有一些其他的情況會導致類被加載進虛擬機,這裏也窮舉不全,想要詳細瞭解的同學可以去看Sun公司(現在是Oracle公司了)的虛擬機規範。瞭解了類加載的時機,接下來說下類加載的渠道,即類可以通過哪些渠道被加載進虛擬機。最典型的比如 jar包,war包,再如通過動態代理運行時寫入,或者由其他文件生成(比如 JSP,JSP最終會被編譯成servlet,這是Java Web基礎)等等方式。讀取時通過獲取類的全限命名來唯一確定一個類,這也是我們使用com.xx.xx做包名的原因--防止全限命名重複。

類被加載進虛擬機就能被直接使用嗎?我們知道我們編寫的 .java 文件最後都會被編譯成字節碼文件 .class 文件。如果我們在編譯後手動修改了 class文件的內容,那麼這個類的行爲甚至語法就會發生異常,所以虛擬機會進行class文件的驗證(主要是語法與格式的驗證),防止出現class文件異常的情況。我們平常在IDE寫代碼時的語法錯誤其實就是提前進行了驗證。

class文件已經加載進虛擬機了,接下來是類的準備階段,這個階段主要爲類的靜態變量設置初始值(注意是初始值而不是真正的值)。接着是解析階段,將字符串常量池中的符號引用替換爲直接引用(字符串常量池不僅僅存放字符串,還有符號引用【包括類和接口的全限命名,字段名稱和描述符,方法名稱和描述符】),關於字符串常量池以及虛擬機區域的劃分可以查看我之前的文章。,所謂的直接引用也就是真實的內存地址。

最後就是類的初始化階段了,通俗的說就是類的構造器執行的階段,然而這裏會涉及一些靜態字段,子父類等概念,所以它的執行過程也是很複雜(當年被父子類誰先打印的問題搞得頭疼)。其實這個誰先打印的問題只要搞明白了構造函數的執行順序就比較簡單了(這裏的構造函數代指類的構造器與類變量賦值與靜態語句塊的集合),首先子類的構造函數執行之前會執行父類的構造函數(Object的構造函數最先執行),由於父類構造函數先執這也意味着父類的類變量賦值靜態語句塊優先於子類執行,所以總結一句話就是父類優先於子類。這裏說一個小tip,我們都知道多線程會有併發問題,那麼當多個線程都對這個類進行初始化的時候會不會發生併發問題呢?可是我們也沒有對構造器進行加鎖呀,這是因爲加鎖的操作虛擬機已經替我們完成了,不得不感嘆,併發問題無處不在。

總結一下,類的加載過程主要分爲以下幾個階段,加載--驗證--準備--解析--初始化--使用

類加載器

終於步入主題了。。瞭解了類的加載過程,該說下它的加載工具了。類加載器的定義爲“通過一個類的全限命名來獲取此類的二進制碼流這個動作放到虛擬機外部來實現,以便讓應用程序自己去決定如何去獲取所需的類,實現這個動作的模塊叫做類加載器”。

虛擬機內部的加載器有3種,啓動類加載器(加載  \lib目錄下的類),擴展類加載器(加載 \lib\ext目錄下的類)以及應用程序類加載器(加載程序classpath下的類)。除了虛擬機自帶的3種,我們也可以自定義類加載器。它們之間的關係如下。

它們是一個從上到下的一個模型,這個模型稱爲雙親委派模型,它的意思是是如需加載一個類,它會一層層向上委託給“父親”來加載,即最先嚐試的是啓動類加載器,如果“父親”不能加載再依次往下嘗試,直至加載成功。這樣做的好處是保證了系統安全性,比如加載Object類總是由啓動類加載器加載,即使我們用自己定義的類加載器來加載我們自己寫的Object類,它最終也會委派給啓動類加載器,保證了系統內的Object類都是同一個(不同類加載器加載的對象不相等),這樣保證了系統的正確性(Obejct是所有類的父類 )。

雙親委派模型的破壞:上面說到使用雙親委派模型可以保證類能夠被正確的類加載器加載,然而想象一下這個場景,Java內部的代碼需要調用用戶寫的代碼,這樣使用雙親模型是完成不了的,典型的場景如JDBC服務,Java負責寫好規範接口,然後讓各個數據庫廠商自己去實現接口(這個就是Java的SPI機制,Dubbo的插件原理也是利用SPI來實現的)。這個時候就需要線程上下文類加載器了,它能夠加載用戶的代碼,如果線程本身未設置線程類加載器,會從父線程繼承一個,如果父類也未設置,那麼這個加載器就是應用程序類加載器。數據庫廠商的JDBC代碼就是使用線程上下文加載器加載的。(注:關於雙親模型被破壞的另一個例子--OSGI,我沒有研究過,感興趣的可以自己查找下資料)。

推薦觀看:https://www.javaworld.com/article/2077344/core-java/find-a-way-out-of-the-classloader-maze.html

參考:深入理解Java虛擬機

最後

最後說點題外話....最近網上一直都有裁員的信息曝出來,說是互聯網寒冬已經到來,感覺慌慌的呢,我纔剛入行,難道還沒開始就已經要結束了嗎.....啥時候可以不用擔心工作就好了啊啊啊啊啊啊啊

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