JVM類載入機制

愛喝啤酒的雷神發表於2020-11-02

一、類載入器

在這裡插入圖片描述

1、分類

站在Java虛擬機器的角度來講,只存在兩種不同的類載入器:
1、啟動類載入器:它使用C++實現(這裡僅限於Hotspot,也就是JDK1.5之後預設的虛擬機器,有很多其他的虛擬機器是用Java語言實現的),是虛擬機器自身的一部分。
2、所有其他的類載入器:這些類載入器都由Java語言實現,獨立於虛擬機器之外,並且 全部繼承自抽象類java.lang.ClassLoader,這些類載入器需要由啟動類載入器載入到記憶體中之後才能去載入其他的類
站在Java開發人員的角度來看,類載入器可以大致劃分為以下三類:
1、啟動類載入器:Bootstrap ClassLoader,
跟上面相同,它負責載入存放在JDK\jre\lib(JDK代表JDK的安裝目錄,下同)下,或被-Xbootclasspath引數指定的路徑中的,並且能被虛擬機器識別的類庫(如rt.jar,所有的java.*開頭的類均被Bootstrap ClassLoader載入)。啟動類載入器是無法被Java程式直接引用的。
主要載入JVM自身工作所需的類,完全由JVM自己控制。沒有父載入器,也沒有子載入器,只是一個類載入工具。根類載入器從System.getProperty(“sun.boot.class.path”)所指定的目錄中載入類庫。
2、擴充套件類載入器:Extension ClassLoader,
該載入器由sun.misc.Launcher$ ExtClassLoader實現,它負責載入JDK\jre\lib\ext目錄中,或者由System.getProperty(“java.ext.dirs”)指定的路徑中的所有類庫(如javax.*開頭的類),開發者可以直接使用擴充套件類載入器。
3、應用程式類載入器:Application ClassLoader,
該類載入器由sun.misc.Launcher$AppClassLoader來實現,它負責載入使用者類路徑(System.getProperty(“java.class.path”),就是我們常用的classpath)所指定的類,開發者可以直接使用該類載入器,如果應用程式中沒有自定義過自己的類載入器,一般情況下這個就是程式中預設的類載入器。
應用程式都是由這三種類載入器互相配合進行載入的,如果有必要,我們還可以加入自定義的類載入器。因為JVM自帶的ClassLoader只是懂得 從本地檔案系統載入標準的java class檔案

特別注意父載入器不等同於父類(載入器之間的父子關係實際上指的是載入器物件之間的包裝關係,而不是類之間的繼承關係。)。我們實現自己的類載入器(不論是繼承ClassLoader、還是URLClassLoader或者其他子類),其父載入器都是AppClassLoader,因為不管呼叫哪個父類構造器,建立的物件都必須最終呼叫getSystemClassLoader()作為其父載入器,而getSystemClassLoader()方法獲取到的正是AppClassLoader。

2、類載入器類繼承關係圖

在這裡插入圖片描述

①ClassLoader:抽象類,類中未定義如何去找到指定類並把它的位元組碼載入到記憶體,需要ClassLoader的子類去實現。(findClass方法)


二、類載入時機


JVM 規範沒有強制約束類載入過程的第一階段(載入)什麼時候開始,但對於“初始化”階段,有著嚴格的規定。

1、主動引用

有且僅有 5 種情況必須立即對類進行“初始化”:
1.在遇到 new、putstatic、getstatic、invokestatic 位元組碼指令時,如果類尚未初始化,則需要先觸發初始化。
2.對類進行反射呼叫時,如果類還沒有初始化,則需要先觸發初始化。
3.初始化一個類時,如果其父類還沒有初始化,則需要先初始化父類。
4.虛擬機器啟動時,用於需要指定一個包含 main() 方法的主類,虛擬機器會先初始化這個主類。
5.當使用 JDK 1.7 的動態語言支援時,如果一個 java.lang.invoke.MethodHandle 例項最後的解析結果為 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法控制程式碼,並且這個方法控制程式碼所對應的類還沒初始化,則需要先觸發初始化。
這 5 種場景中的行為稱為對一個類進行主動引用,除此之外,其它所有引用類的方式都不會觸發初始化,稱為被動引用。

2、幾種被動引用:

1.透過子類引用父類的靜態欄位,不會導致子類初始化。對於靜態欄位,只有直接定義這個欄位的類才會被初始化。
2.透過陣列定義來引用類,不會觸發此類的初始化。
3.常量在編譯階段會存入呼叫類的常量池中,本質上並沒有直接引用到定義常量的類,因此不會觸發定義常量的類的初始化。
編譯透過之後,常量儲存到 NotInitialization 類的常量池中,NotInitialization 的 Class 檔案中並沒有 ConstClass 類的符號引用入口,這兩個類在編譯成 Class 之後就沒有任何聯絡了。

3、關於介面載入

當一個類在初始化時,要求其父類全部都已經初始化過了,但是一個介面在初始化時,並不要求其父介面全部都完成了初始化,當真正用到父介面的時候才會初始化。


三、類載入機制

雙親委派模型的工作流程:
如果一個類載入器收到了類載入的請求,它首先不會自己去嘗試載入這個類,而是把請求委託給父載入器去完成,依次向上,因此,所有的類載入請求最終都應該被傳遞到頂層的啟動類載入器中,只有當父載入器在它的搜尋範圍中沒有找到所需的類時,即無法完成該載入,子載入器才會嘗試自己去載入該類。
好處:
使用雙親委派模型來組織類載入器之間的關係,有一個很明顯的好處,就是Java類隨著它的類載入器(說白了,就是它所在的目錄)一起具備了一種帶有優先順序的層次關係,這對於保證Java程式的穩定運作很重要。例如,類java.lang.Object類存放在JDK\jre\lib下的rt.jar之中,因此無論是哪個類載入器要載入此類,最終都會委派給啟動類載入器進行載入,這邊保證了Object類在程式中的各種類載入器中都是同一個類。
loader1首先從自己的名稱空間中查詢Sample類是否已經被載入,如果已經載入,就直接返回代表Sample類的Class物件的引用。若所有的父載入器及loader1本身都不能載入,則丟擲ClassNotFoundException異常。

定義類載入器、初始類載入器:
定義類載入器:若有一個類載入器能成功載入Sample類,那麼這個類載入器被稱為定義類載入器。
初始類載入器:所有能成功返回Class物件的引用的類載入器(包括定義類載入器,即包括定義類載入器和它下面的所有子載入器)都被稱為初始類載入器。
名稱空間
每個類載入器都有自己的名稱空間,名稱空間由該載入器及所有父載入器所載入的類組成。
在同一個名稱空間中,不會出現類的全限定名相同的兩個類。
在不同的名稱空間中,有可能會出現類的全限定名相同的兩個類。
執行時包
由同一類載入器載入的屬於相同包的類組成了執行時包。
只有屬於同一執行時包的類才能互相訪問包可見(即預設訪問級別)的類和類成員。
這樣的限制能避免使用者自定義的類冒充核心類庫的類,去訪問核心類庫的包可見成員。

四、類載入


將類的class檔案中的二進位制資料從硬碟讀取到記憶體中,將其放置在執行時資料區的方法區(虛擬機器規範並未要求,但hotspot虛擬機器將其放置在虛擬機器中)內,然後在記憶體中建立唯一一個類的class物件,用以封裝類在方法區中的資料結構,無論new出多少個物件,最終對應的class物件只有一個。

1、JVM載入class檔案到記憶體的方法
①隱式載入:透過JVM自動載入需要的類到記憶體中
②顯式載入:透過寫程式碼去呼叫ClassLoader類來載入   1)forName()  2)loadClass()  3)findClass()

2、載入過程

在這裡插入圖片描述 ①載入

載入時類載入過程的第一個階段,在載入階段,虛擬機器需要完成以下三件事情:
1、透過一個類的全限定名來獲取其定義的二進位制位元組流。
2、將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構。
3、在Java堆中生成一個代表這個類的java.lang.Class物件,作為對方法區中這些資料的訪問入口。
②驗證

驗證的目的是為了確保Class檔案中的位元組流包含的資訊符合當前虛擬機器的要求,而且不會危害虛擬機器自身的安全。不同的虛擬機器對類驗證的實現可能會有所不同,但大致都會完成以下四個階段的驗證:檔案格式的驗證、後設資料的驗證、位元組碼驗證和符號引用驗證。
1、檔案格式的驗證:驗證位元組流是否符合Class檔案格式的規範,並且能被當前版本的虛擬機器處理,該驗證的主要目的是保證輸入的位元組流能正確地解析並儲存於方法區之內。 經過該階段的驗證後,位元組流才會進入記憶體的方法區中進行儲存,後面的三個驗證都是基於方法區的儲存結構進行的。
2、後設資料驗證:對類的後設資料資訊進行語義校驗(其實就是對類中的各資料型別進行語法校驗),保證不存在不符合Java語法規範的後設資料資訊。
3、位元組碼驗證:該階段驗證的主要工作是進行資料流和控制流分析,對類的方法體進行校驗分析,以保證被校驗的類的方法在執行時不會做出危害虛擬機器安全的行為。
4、符號引用驗證:這是最後一個階段的驗證,它發生在虛擬機器將符號引用轉化為直接引用的時候(解析階段中發生該轉化,後面會有講解),主要是對類自身以外的資訊(常量池中的各種符號引用)進行匹配性的校驗。
③準備
準備階段是正式為類變數分配記憶體並設定類變數初始值的階段,這些記憶體都將在方法區中分配。對於該階段有以下幾點需要注意:
1、這時候進行記憶體分配的僅包括類變數(static),而不包括例項變數,例項變數會在物件例項化時隨著物件一塊分配在Java堆中。
2、這裡所設定的初始值通常情況下是資料型別預設的零值(如0、0L、null、false等),而不是被在Java程式碼中被顯式地賦予的值。
3、如果類欄位的欄位屬性表中存在ConstantValue屬性,即同時被final和static修飾,那麼在準備階段變數value就會被初始化為ConstValue屬性所指定的值。我們可以理解為static final常量在編譯期就將其結果放入了呼叫它的類的常量池中。 在這裡插入圖片描述
④解析
解析階段是虛擬機器將常量池中的符號引用轉化為直接引用的過程。

⑤初始化
初始化是類載入過程的最後一步,到了此階段,才真正開始執行類中定義的Java程式程式碼。在準備階段,類變數已經被賦過一次系統要求的初始值,而在初始化階段,則是根據程式設計師透過程式指定的主觀計劃去初始化類變數和其他資源,或者可以從另一個角度來表達:初始化階段是執行類構造器< clinit>()方法的過程。

總結:
整個類載入過程中,除了在載入階段使用者應用程式可以自定義類載入器參與之外,其餘所有的動作完全由虛擬機器主導和控制。到了初始化才開始執行類中定義的Java程式程式碼(亦及位元組碼),但這裡的執行程式碼只是個開端,它僅限於()方法。類載入過程中主要是將Class檔案(準確地講,應該是類的二進位制位元組流)載入到虛擬機器記憶體中,真正執行位元組碼的操作,在載入完成後才真正開始。

3、檢視載入類
jvm虛擬機器引數:都以-XX:開始
-XX:+ 表示開啟option選項 ,
-XX:- 表示關閉option選項,
-XX:=,表示將option值設定為value
例如:
-XX:+TraceClassLoading
有時候我們需要監控系統中哪些類被載入進來,什麼樣的類載入的比較頻繁,什麼樣的類載入的比較少,可以使用這個引數來配置列印出程式執行過程中類的載入資訊。
4、類的解除安裝

雙向關聯關係:
載入器和Class物件:
在類載入器的內部實現中,用一個Java集合來存放所載入類的引用。
另一方面,一個Class物件總是會引用它的類載入器。呼叫Class物件的getClassLoader()方法,就能獲得它的類載入器。
由此可見,Class例項和載入它的載入器之間為雙向關聯關係。
在這裡插入圖片描述

當類的Class物件不再被引用,即不可觸及時,Class物件就會結束生命週期。
由Java虛擬機器自帶的類載入器所載入的類,在虛擬機器的生命週期中,始終不會被解除安裝。由使用者自定義的類載入器載入的類是可以被解除安裝的。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69984138/viewspace-2731586/,如需轉載,請註明出處,否則將追究法律責任。

相關文章