[Java 執行那些事] —— 類加載機制( 上)

這裏寫圖片描述

代碼編譯的結果從本地機器碼轉換爲字節碼,是存儲格式發展的一小步,卻是編程語言發展的一大步。——周志明《深入理解Java虛擬機》

Introduction to Class Loading

類加載(Class Loading)是一種機制,他描述的是將字節碼以文件形式加載到內存再經過連接、初始化後,最終形成可以被虛擬機直接使用的Java類型地過程。

JVM採用這種在運行期纔去加載、連接、初始化的策略會稍微增加一些的性能開銷,導致例如程序啓動慢的這樣的缺點。但是它卻可以爲程序提供高度的靈活性,這是因爲JVM的字節碼執行引擎不需要提前瞭解關於文件和文件系統的任何信息,我們完全可以等到運行期才指定實際的實現方法,讓一個本地程序通過網絡加載任何地方的字節碼文件。Java的動態拓展性正是賴於JVM類加載機制實現的。

What is it

Class Loading 包含了加載(Loading)、連接(Linking)、初始化(Initialization)三大部分,其中Linking又包含了三個部分:校驗(Verification)、準備(Preparation)、解析(Resolution)。而一個類的生命週期只是在Class Loader的基礎上多了:使用(Using),卸載(Unloading)兩部分。

Class Loaders的組成:

這裏寫圖片描述

類的生命週期
這裏寫圖片描述

加載(Loading)

Loading是Class Loading的第一步,他的工作是負責將字節碼(bytecode)加載到JVM內存中,這個內存空間就是我們常說的方法區。在JVM規範中,Loading需要完成以下三點:

  1. 通過一個類的全限定名來獲取定義此類的二進制字節流
  2. 將這個字節流所代表的靜態存儲結構轉化爲方法區的運行時數據結構;
  3. 在內存中生成一個代表這個類的java.lang.Class對象,作爲方法區這個類的各種數據的訪問入口。

JVM中規定了這三條動作,並且不算具體,也就是說只要在滿足這三條,我們可以任意拓展。例如JVM只規定了“通過一個類的全限定名來獲取定義此類的二進制字節流”,但是並沒有說從哪裏加載,我們可以通過.class文件中加載,也可以通過網絡加載任何地方的字節碼。而這一階段也是開發人員能夠控制最強的地方。

這裏要特殊說明的是,JVM在加載數組的時候加載的僅僅是數組的類型類(例如String[] 加載器只會加載String這個類型類),而數組的創建則由JVM直接完成。

這裏我們多問幾個爲什麼:

1. JVM爲什麼只加載數組的類型類
我認爲JVM這樣做的目的主要是爲了節省時間,我們知道數組裏面裝的都是同一種類型的元素,JVM沒必要將一個重複的內容加載多次浪費時間。
2. N維數組怎麼加載
如果是N維數組,類加載器會從最外層開始一層一層的遞歸加載,直到加載到非數組類型爲止。
3. 引用類型與基本類型加載起來會不會有區別
其實基本類型早已經在javac階段裝箱成封裝對象了,例如int會被裝箱成Integer,long裝箱成Long等等,所以是沒有區別的。

1.2 類加載器(Class Loaders)

爲了完成加載過程中的第一條:"通過一個類的全限定名來獲取定義此類的二進制字節流"的功能,JVM團隊開發了一個模塊——類加載器。但是爲了給用戶提供更好的拓展性JVM團隊將這個過程的代碼放到了JVM的外部,以便讓開發人員可以自定義類加載器。

雖然類加器只是用於實現類加載的動作,但是他的作用遠遠不限於類加載。對於任意一個類,他的唯一決定方式是:類本身+加載此類的類加載器,這個可以類比爲C++中的命名空間,每一個類加載器都有自己獨立的命名空間,通俗的講:一個類java.lang.Object 如果被兩個類加載器加載,那麼這個兩個類就是不相同的。

JVM爲什麼要如此設計呢?

答案是爲了拓展性。這種情況出現在類本身的限制名(包名+類名)無法唯一區分類的時候。例如在不更改包名的情況,如何讓不同版本的kafka在同一個JVM下運行呢?爲了解決類似的問題類加載器引入的命名空間的概念,提高了拓展性。

加載器的類型

爲此JVM提供了多種類加載器,當然用戶也可以自行拓展。

  • 從Java虛擬機的角度看,只有兩種不同的類加載器:
  1. 啓動類加載器(Bootstrap ClassLoader):用C++實現,是虛擬機自身的一部分;
  2. 所有其他的類加載器:用Java語言實現,獨立於虛擬機外部,都繼承自抽象類java.lang.ClassLoader;
  • 從Java開發人員看,類加載器可分爲3種
  1. 啓動類加載器(Bootstrap ClassLoader):負責加載<\JAVA——HOME>\lib目錄中的並且可以被虛擬機識別的;
  2. 擴展類加載器(Extension ClassLoader):負責加載<\JAVA_HOME>\lib\ext目錄中的所有類庫,開發者可以直接使用擴展類加載器;
  3. 應用程序類加載器(Application ClassLoader):它是ClassLoader中的getSystemClassLoader()方法的返回值,所以也稱它爲系統類加載器。他負責加載用戶類路徑(ClassPath)上所指定的類庫

這裏寫圖片描述

Object類重複多次怎麼辦?

但是這又引入了一個問題:如果每個類加載擁有自己的命名空間,而且是隨機的加載類,那麼如果用戶自己編寫了一個java.lang.Object類,並把它放到了ClassPath中,豈不是會出現很多個Object類!這樣Java類型體系中最最基礎的行爲都無法保證,應用程序也將一片混亂。爲此JVM團隊提出了雙親委派模型(Parent Delegation Model)

雙親委派模型

雙親委派模型的英文名字叫:Parent Delegation Model,當我第一次聽到“雙親”這個詞的時候很困惑,有歧義感,所以還是建議大家去多看看英文原版的內容會有一種豁然開朗的感覺。

如圖所示,所謂的雙親委派模型指除了啓動類加載器以外,其餘的加載器都有自己的父類加載器,而在工作的時候,如果一個類加載器收到加載請求,他不會馬上加載類,而是將這個請求向上傳遞給他的父加載器,看父加載器能不能加載這個類,加載的原則就是優先父加載器加載,若果父加載器加載不了,自己才能加載。

綜上就是雙親委派模型的原理,是不是很簡單!

因爲有了雙親委派模型的存在,類似Object類重複多次的問題就不會存在了,因爲經過層層傳遞,加載請求最終都會被Bootstrap ClassLoader所響應。加載的Object對象也會只有一個。

並且面對同一JVM進程多版本共存的問題,只要自定義一個不向上傳遞加載請求的加載器就好啦。

這裏寫圖片描述

Summary

今天我主要講述Java類加載機制的第一步:加載(Loading),通過這一章節的學習我們知道了類加載其實是一個包含:加載、連接(校驗、準備、解釋)、初始化的機制。正是因爲Java在運行時才進行類加載,從而爲Java提供了更高的動態拓展性。

而在Loading過程中JVM規範並沒有明確表示要從什麼地方加載字節碼,所以用戶可以通過自定義類加器的方式加載任何地方的字節碼。

爲了支持類多版本共存,JVM提供了加載器帶有命名空間的功能,可以在不修包名的情況下實現多版本共存。

而加載器帶有命名空間後又帶來了Object類可能重複的問題,爲此引入雙親委派模型:子加載器收到加載請求後需要向上傳遞,優先父加載器加載。

參考:《深入瞭解Java虛擬機 周志明》

文章的最後向您推薦兩個關於Java的專欄。專欄的內容有音頻有文稿, 無論是在路上還是業餘時間的學習都很有裨益。

這裏寫圖片描述

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