本文源自參考《深入理解jvm虛擬機器》,多篇博文的總結
前言
我們編寫的程式碼最終會編譯為Class
檔案,Class
檔案中描述的各種資訊,最
終都需要載入到虛擬機器中之後才能執行和使用。而虛擬機器如何載入這些Class
檔案?Class
文
件中的資訊進入到虛擬機器後會發生什麼變化?這些都關係著程式碼的最終執行情況。
一 類載入器
類載入器:通過一個類的全限定名來獲取描述此類二進位制位元組流的程式碼模組
這是類載入器比較官方的說法,簡單來說就是負責讀取Java位元組碼資訊。
1.1類的唯一性
類在Java虛擬機器中的唯一性:對於任意一個類,都需要由載入它的類載入器和這個類本身一同確立其在Java虛擬機器中的唯一性。
通俗一點來講,要判斷兩個類是否“相同”,前提是這兩個類必須被同一個類載入器載入,且類位元組碼相同,否則這個兩個類不“相同”。
這裡所指的“相等”,包括代表類的Class
物件的equals()
方法、isAssignableFrom()
方
法、isInstance(
)方法的返回結果,也包括使用instanceof
關鍵字做物件所屬關係判定等情況。
1.2 類載入器種類
- 啟動類載入器
Bootstrap ClassLoader
:載入JACA_HOME\lib
,或者被-Xbootclasspath
引數限定的類。使用C++語言實現,是Java虛擬機器自身的一部分,啟動類載入器無法被Java程式直接引用。 - 擴充套件類載入器
Extension ClassLoader
:載入\lib\ext
,或者被java.ext.dirs
系統變數指定的類。開發人員可以直接使用。 - 應用程式類載入器
Application ClassLoader
:載入使用者類路徑(ClassPath)上所指定的類庫,是ClassLoader
中的getSystemClassLoader()
方法的返回值。開發人員可以直接使用。如果應用程式中沒有自定義過自己的類載入器,一般情況下這個就是程式中預設的類載入器。 - 自定義類載入器通過繼承ClassLoader實現,一般是載入我們的自定義類。
類載入器 Java 類如同其它的Java類一樣,也是要由類載入器來載入的。
二 雙親委派模型
既然有這麼多種類載入器,那麼在JVM中類在載入時,各個類載入器是怎麼協調統一運作的呢,這就引出了雙親委派模型。
(它並不是一個強制性的約束模型,而是Java設計者推薦給開發者的一種類載入器實現
方式。不過我們常用的JVM都實現了該模型,如:官方下載的JavaSE中JVM——Oracle Hotspot)
雙親委派模型要求除了頂層的啟動類載入器外,其餘的類載入器都應當有自己的父類載入器。這裡類載入器之間的父子關係一般不會以繼承(Inheritance)的關係來實現,而是都使用組合(Composition)關係來複用父載入器的程式碼。
雙親委派模型的工作過程:如果一個類載入器收到了類載入的請求,它首先不會自己
去嘗試載入這個類,而是把這個請求委派給父類載入器去完成,每一個層次的類載入器都是如此,因此所有的載入請求最終都應該傳送到頂層的啟動類載入器中,只有當父載入器反饋自己無法完成這個載入請求(它的搜尋範圍中沒有找到所需的類)時,子載入器才會嘗試自己去載入。
雙親委派好處:1.避免同一個類被多次載入 2.每個載入器只能載入自己範圍內的類
三 類載入時機
理解了類載入器,即可開始說明類的載入詳情了。首先我們要知道,JVM在什麼時候會進行類載入。
3.1 類在JVM中的生命週期
類從被載入到虛擬機器記憶體中開始,到解除安裝出記憶體為止,它的整個生命週期包括:載入
(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和解除安裝(Unloading)7個階段。其中驗證、準備、解析3個部分統稱為連線(Linking)。
3.2 類載入的各階段順序
在圖中,載入、驗證、準備、初始化和解除安裝這5個階段的順序是確定的,類的載入過程
必須按照這種順序按部就班地開始(按順序開始,而不是完成。因為這些階段通常都是互相交叉地混合式進行的)。
並且解析階段並不完全遵守圖中描述的載入順序:它在某些情況下可以在初始化階
段之後再開始,這是為了支援Java語言的執行時繫結(即動態繫結)。
3.3 類載入時機
Java虛擬機器規範中並沒有進行強制約束類載入時機,這點可以交給虛擬機器的具體實現來自由把握。但是對於初始化階段,虛擬機器規範則是嚴格規定了有且只有5種情況必須立即對類進行“初始化”(而載入、驗證、準備自然需要在此之前開始):
1.遇到new
、getstatic
、putstatic
、invokestatic
這4條位元組碼指令時。生成這4條指令最常見的Java程式碼場景是:使用new關鍵字例項化物件的時候、讀取或設定一個類的靜態欄位(被final
修飾、已在編譯期把結果放入常量池的靜態欄位除外)的時候,以及呼叫一個類的靜態方法的時候。
2.使用java.lang.reflect
包方法對類進行反射呼叫的時候。
3.當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。
4.當虛擬機器啟動時,使用者需要指定一個要執行的主類(包含main()
的那個類),虛擬機器會先初始化這個主類。
5.當使用JDK 1.7的動態語言支援時,如果一個java.lang.invoke.MethodHandle
例項最後的解析結果REF_getStatic
、REF_putStatic
、REF_invokeStatic
的方法控制程式碼,則需要先觸發這個方法控制程式碼所對應的類的初始化。
四 類載入過程
接下來我們詳細講解一下Java虛擬機器中類載入的全過程,也就是載入、驗證、準備、解析和初始化這5個階段所執行的具體動作。
4.1 載入
載入階段是類載入過程的第一個階段。在這個階段,JVM的主要目的是將位元組碼從各個位置(網路、磁碟等)轉化為二進位制位元組流載入到記憶體中,接著會為這個類在JVM的方法區建立一個對應的 Class 物件,這個 Class物件就是這個類各種資料的訪問入口。
分開來說載入階段主要做了三件事。
1.通過一個類的全限定名來獲取定義此類的二進位制位元組流。
2.將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構。
3.在記憶體中生成一個代表這個類的java.lang.Class
物件,作為方法區這個類的各種資料
的訪問入口。
4.2 驗證
驗證是連線階段的第一步,這一階段的目的是為了確保Class檔案的位元組流中包含的資訊符合當前虛擬機器的要求,並且不會危害虛擬機器——class檔案是否符合Java規範,程式碼是否邏輯正確。
主要有4個驗證動作:
1.檔案格式驗證:驗證位元組流是否符合Class檔案格式的規範,並且能被當前版本的虛擬機器處理。(這階段的驗證是基於二進位制位元組流進行的,只有通過了這個階段的驗證後,位元組流才會進入記憶體的方法區中進行儲存,所以後面的3個驗證階段全部是基於方法區的儲存結構進行的,不會再直接操作位元組流。)
2.後設資料驗證:是對位元組碼描述的資訊進行語義分析,以保證其描述的資訊符合Java語言規範的要求。簡單來說就是驗證類的後設資料資訊是否符合Java規範。
3.位元組碼驗證:會對程式碼組成的資料流和控制流進行校驗,驗證程式語義是否合法、符合邏輯。
4.符號引用驗證:發生在虛擬機器將符號引用轉化為直接引用的時候,這個轉化動作將
在連線的第三階段——解析階段中發生。
4.3 準備
當完成位元組碼檔案的校驗之後,JVM便會開始為類變數分配記憶體並初始化。這裡需要注意兩個關鍵點,即記憶體分配的物件以及初始化的型別。(也是面試題中經常考的內容)
- ****記憶體分配的物件**。Java中的變數有「類變數」和「類成員變數」兩種型別,「類變數」指的是被 static 修飾的變數,而其他所有型別的變數都屬於「類成員變數」。在準備階段,JVM 只會為「類變數」分配記憶體,而不會為「類成員變數」分配記憶體。「類成員變數」的記憶體分配需要等到初始化階段才開始。
- 初始化的型別。在準備階段,JVM會為類變數分配記憶體,併為其初始化。但是這裡的初始化指的是為變數賦予 Java 語言中該資料型別的零值,而不是使用者程式碼裡初始化的值。
例如下面的程式碼在準備階段之後,sector 的值將是 0,而不是 3。
但有一點例外。如果一個變數是常量(被 static final
修飾)的話,那麼在準備階段,屬性便會被賦予使用者希望的值。
例如下面的程式碼在準備階段之後,number 的值將是 3,而不是 0。
public static final int number = 3;
4.4 解析
當通過準備階段之後,JVM針對類或介面、欄位、類方法、介面方法、方法型別、方法控制程式碼和呼叫點限定符 7 類引用進行解析。這個階段的主要任務是將其在常量池中的符號引用替換成直接其在記憶體中的直接引用。
其實這個階段對於我們來說也是幾乎透明的,瞭解一下就好。
4.5 初始化
到這一步JVM才開始執行我們編寫的Java程式碼,按照一定的順序,依次執行靜態程式碼。
具體順序如下:
- 靜態屬性:
static
開頭定義的屬性 - 靜態方法塊:
static {}
圈起來的方法塊
執行完成後,類的初始化便算完成了,一個Java物件的建立往往包含了類的初始化
和類的例項化
兩步驟。如果進行了類的例項化,還會按順序執行以下步驟:
1.普通屬性: 未帶static定義的屬性
2.普通方法塊: {} 圈起來的方法塊
3.建構函式: 類名相同的方法
4.方法: 普通方法