學習參考資料:周志明老師的著作《深入理解Java虛擬機(第3版)》
我們知道Java代碼經編譯后會生成Class檔案,然后都需要加載到虛擬機中才能被運行和使用,
而虛擬機如何加載這些Class檔案,Class檔案中的資訊進入到虛擬機會發生什么變化,接下來就會圍繞這兩個問題展開,
1.類的生命周期
一個型別從被加載到虛擬機中開始,到卸出記憶體,它的生命周期將會經歷加載、驗證、準備、決議、初始化、使用和卸載七個階段,如下圖,

圖中,加載、驗證、準備、初始化和卸載這五個階段的順序是確定的,型別的加載程序必須按照這種順序按部就班的開始,而決議則不一定:它在某些情況下可以在初始化階段之后再開始,這是為了支持Java語言的運行時系結特性(也成為動態系結),
注意:這里的加載只是類加載程序的一個階段
在Java語言里面,型別的加載、連接和初始化程序都是在程式運行期間完成的,也因此為Java應用提供了極高的可擴展性和靈活性,Java天生可以動態擴展的語言特征就是依賴運行期動態加載和動態連接這個特點實作的
2.類加載的程序
類加載的全程序即加載、驗證、準備、決議和初始化這五個階段所執行的具體動作,
加載
在加載階段,Java虛擬機主要完成下面三件事情:
1)通過類的全限定名來獲取定義此類的二進制位元組流,
2)將這個位元組流的靜態存盤結構轉化為方法區的運行時資料結構,
3)在記憶體中生成一個代表此類的java.lang.Class物件,作為方法區這個類的各種資料的訪問入口,
對于陣列類而言,陣列類本身不需要類加載器創建,它是由Java虛擬機直接在記憶體中創建,但是陣列類中的元素還是需要類加載器來完成加載,
驗證
驗證是連接階段的第一步,這一階段的主要目的是確保Class檔案的位元組流符合《Java虛擬機規范》的全部約束要求,保證這些代碼運行后不會危害虛擬機的自身安全,大致會完成下面四個階段的驗證動作:
-
檔案格式驗證
第一階段要驗證位元組流是否符合Class檔案格式的規范, 并且能被當前版本的虛擬機處理,
主要目的是保證輸入的位元組流能正確地決議并存盤于方法區之內, 格式上符合描述一個Java型別資訊的
要求, 這階段的驗證是基于二進制位元組流進行的, 只有通過了這個階段的驗證后, 位元組流才會進入記憶體的方法區中
進行存盤, 所以后面的3個驗證階段全部是基于方法區的存盤結構進行的, 不會再直接操作位元組流, -
元資料驗證
第二階段是對位元組碼描述的資訊進行語意分析, 以保證其描述的資訊符合Java語言規范的要求,
主要目的是對類的元資料資訊進行語意校驗, 保證不存在不符合Java語言規范的元資料資訊,
-
位元組碼驗證
第三個階段將對類的方法體進行校驗分析, 保證被校驗類的方法在運行時不會做出危害虛擬機安全的事件,
主要目的是通過資料流和控制流分析, 確定程式語意是合法的、 符合邏輯的,
-
符號參考驗證
第四個階段可以看做是對類自身以外( 常量池中的各種符號參考) 的資訊進行匹配性校驗,
這個階段的校驗發生在虛擬機將符號參考轉化為直接參考的時候, 這個轉化動作將在連接的第三階段——決議階段中發生,
對于虛擬機的類加載機制來說, 驗證階段是一個非常重要的、 但不是一定必要( 因為對程式運行期沒有影響)的階段, 如果所運行的全部代碼( 包括自己撰寫的及第三方包中的代碼) 都已經被反復使用和驗證過, 那么在實施階段就可以考慮使用引數來關閉大部分的類驗證措施, 以縮短虛擬機類加載的時間,
準備
準備階段是為類中定義的變數(即靜態變數,被static修飾的變數)分配記憶體并設初始值的階段(靜態變數分配在方法區),
需要注意的是:這里僅僅指的是靜態變數,而實體變數分配記憶體要在實體化的時候進行了,這里的初始值并不是將值直接賦值,而是資料型別的”零值“,如下:
public static int value=123;
value的初始值不會被賦成123,而是0;賦值成123要在初始化階段才能完成,
但假如將代碼改成,如下:
public static final int value=123;
這樣子就會在準備階段,為靜態常量value賦值成123了,
決議
決議階段是將常量池中的符號參考改為直接參考的程序,決議動作主要針對類或介面、 欄位、 類方法、 介面方法、 方法型別、 方法句柄和呼叫點限定符7類符號參考進行,
- 符號參考( Symbolic References) : 符號參考以一組符號來描述所參考的目標, 符號可以是任何形式的字面
量, 只要使用時能無歧義地定位到目標即可, - 直接參考( Direct References) : 直接參考可以是直接指向目標的指標、 相對偏移量或是一個能間接定位到
目標的句柄,
初始化
前面幾個階段中,都是有虛擬機主導和控制(除了在加載階段用戶程式可以通過自定義類加載器),到了初始化這個階段,就真正開始執行類中定義的Java程式,
在準備階段, 類變數已經賦過一次系統要求的初始值, 而在初始化階段, 則根據程式員通程序式制定的主觀計劃去初始化類變數和其他資源, 或者可以從另外一個角度來表達: 初始化階段是執行類構造器<clinit>() 方法的程序,根據準備階段的例子:
public static int value=123;
在這個階段才會被賦值為123;
3.類加載器
3.1類與類加載器
一個類本身和加載這個類的加載器共同確立才能在Java虛擬機中具有唯一性,也就是說,即使來自同一份Class檔案,被不同的加載器加載,兩個類也是不”相等“的(這里的相等指的是instanceof和Class類的equals的回傳值),
絕大多數Java程式都會使用到以下3個系統提供的類加載器來加載:
1)啟動類加載器(Bootstrap ClassLoader)
負責加載$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++實作,不是ClassLoader子類,由于引導類加載器涉及到虛擬機本地實作細節,開發者無法直接獲取到啟動類加載器的參考,所以不允許直接通過參考進行操作,
2)擴展類加載器(Extension ClassLoader)
負責加載java平臺中擴展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目錄下的jar包
3)應用程式類加載器(App ClassLoader)
負責加載classpath中指定的jar包及目錄中class
另外,JVM的類加載機制主要有如下3種:
- 全盤負責:所謂全盤負責,就是當一個類加載器負責加載某個Class時,該Class所依賴和參考其他Class也將由該類加載器負責載入,除非顯示使用另外一個類加載器來載入,
- 雙親委派:所謂的雙親委派,則是先讓父類加載器試圖加載該Class,只有在父類加載器無法加載該類時才嘗試從自己的類路徑中加載該類,通俗的講,就是某個特定的類加載器在接到加載類的請求時,首先將加載任務委托給父加載器,依次遞回,如果父加載器可以完成類加載任務,就成功回傳;只有父加載器無法完成此加載任務時,才自己去加載,
- 快取機制:快取機制將會保證所有加載過的Class都會被快取,當程式中需要使用某個Class時,類加載器先從快取區中搜尋該Class,只有當快取區中不存在該Class物件時,系統才會讀取該類對應的二進制資料,并將其轉換成Class物件,存入緩沖區中,這就是為什么修改了Class后,必須重新啟動JVM,程式所做的修改才會生效的原因,
3.2雙親委派模型

如圖,各類加載器之間的層次關系就被稱為加載器的雙親委派模型,
雙親委派模型的作業程序:如果一個類加載器收到了加載類的請求,它首先會委托其上層類加載器加載,上層接到請求再交給它的上層,依次遞回下去,直到啟動類加載器,如果本層類加載器不能加載才會讓其子層進行加載,
使用雙親委派模型的好處:無論哪一個類加載器加載這個類,最終都會委派給最頂端的啟動類加載器,因此Object類在各種加載環境下都能保證是同一個類,假如沒有采用這種雙親委派模型進行加載,而是由各自的類加載器進行加載,那么如果自定義了一個java.lang.Object,那么系統中就可能會有多個Object,這就會導致Java型別體系中最基礎的行為都無法保證,

后面還會陸陸續續更新這系列的讀書筆記,期待您的關注~~
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/376060.html
標籤:其他
上一篇:shell那點事兒——運維工程師必會正則運算式及文本處理三劍客
下一篇:YOLOV3代碼主干部分代碼決議
