1. 程式人生 > >《深入理解java虛擬機器》讀書筆記(一)---- 類載入機制

《深入理解java虛擬機器》讀書筆記(一)---- 類載入機制

類載入的時機

1、類從虛擬機器載入到記憶體開始,到卸載出記憶體為止,整個生命週期分為七個階段:載入、驗證、準備、解析、初始化、使用和解除安裝。其中驗證、準備和解析統稱為連線階段。

2、載入、驗證、準備、初始化和解除安裝這五個階段是按順序執行的,而解析階段卻不一定,解析可以在初始化之後執行。

3、初始化階段,有且只有五種情況必須對類進行初始化:
①遇到new、getstatic、putstatic和invokestatic這四個位元組碼時,如果類沒有進行過初始化,則必須要記性初始化。這四種場景分別對應著對則用new關鍵字對類進行例項化,設定或讀取一個靜態物件(用final修飾的靜態常量已經在編譯器放到了常量池,所以除外),和呼叫靜態方法。
②對類進行了反射呼叫的時候。
③初始化一個類時,如果其父類沒有被初始化,則必須初始化其父類。
④程式主類(main()方法) 必須初始化。
⑤當使用jdk7動態語言支援時,如果一個java.lang.invoke.MethodHandle例項後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法控制代碼時,並且這個方法控制代碼對應的類沒有初始化,則必須要先初始化。

4、除此之外類的所有引用都不會初始化,稱為被動引用,例子:
①通過子類引用父類的靜態欄位,只會對父類初始化,子類不初始化。
②通過陣列定義來引用類,不會初始化該類。
③常量在編譯期匯存入呼叫類的常量池中,不會觸發定義類的初始化。

類載入的過程

1、載入

(1)在載入過程中完成三件事:
①通過類的許可權定名來獲取定義此類的二進位制位元組流。
②將這個位元組流代表的靜態儲存結構轉化為方法區的執行時資料結構。
③在記憶體中生成一個代表此類的Object物件,作為方法區代表此類的訪問入口。

(2)因為虛擬機器規範中並沒有明確指定這個二進位制位元組流要從哪獲取,使用使得很多java技術都建立在這基礎之上:
①從zip包中讀取,最終演變成了jar, ear, war格式的基礎。
②從網路中獲取,代表的場景有applet。
③java的動態代理技術。
④由其他檔案生成,比如用jsp檔案去生成。
⑤從資料庫中讀取。

2、驗證

驗證階段是虛擬機器對自身的保護的一項重要工作。
(1)檔案格式驗證,驗證自己留是否符合class檔案格式規範,並且能被當前版本的虛擬機器處理。
①是否以魔數0xCAFEBABE開頭。
②主次版本號是否在當前版本處理範圍內。
③常量池的常量中是否有不被支援的常量型別。
④指向常量的各種索引值中是否有指向不存在的常量或不存在的常量型別。

(2)元資料驗證,對位元組碼描述的語義進行分析,以保證符合java的規範。
①這個類是否有父類(除了java.lang.Object類)。
②這個類的父類是否繼承了不被允許繼承的類(被final修飾的類)。
③如果這個類不是抽象類,是否實現了其父類或介面中要求實現的所有方法。
④類中的欄位、方法會與父類矛盾。

(3)位元組碼驗證,通過資料流和控制流分析,確定程式的合法性和是符合邏輯的。

(4)符號引用驗證,虛擬機器將符號引用轉化為直接引用,對類以外的星系進行匹配性校驗。
①符號引用中通過字串描述的全限定名是否能扎到類。
②在指定類中是否存在符合方法的欄位描述以及簡單名稱所描述的方法和欄位。
③符號引用中的類、欄位和方法的訪問性是否可被當前類訪問。

3、準備

準備階段是正式為類變數分配記憶體並設定類變數初始值的階段,這些變數所使用的記憶體都將在方法區中分配。這裡的只包括類變數而不包括例項變數,而且這裡的初始化也是指相應資料型別的零值,賦具體值的操作將會在初始化階段進行。

資料型別 零值
int 0
long 0L
short (short)0
char ‘\u0000’
byte (byte)0
boolean false
float 0.0f
double 0.0d
reference null

但是要注意,有final修飾的變數會直接賦具體值。

4、解析

解析階段是虛擬機器將常量池內的符號引用替換為直接引用的過程。
(1)類或介面的解析:假設當前程式碼所處的類為D,如果要把一個從未解析過的符號引用N解析為一個類或介面C的直接引用,那麼虛擬機器完成整個過程需要以下三個步驟:
①如果C不是一個數組型別,那麼虛擬機器將會把代表N的全限定名傳遞給D的類載入器去載入這個類C。
②如果C是一個數組型別,並且陣列的元素型別為物件,那麼就會按照①中的方法去載入陣列元素型別。
③如果上面的步驟沒有異常,那麼C在虛擬機器中實際上已經成為一個有效的類或介面了,但是在解析完成之前還是要進行符號引用驗證,確定D是否具備對C的訪問許可權。
(2)欄位解析
(3)類方法解析
(4)介面方法解析

5、初始化

類初始化是類載入過程的最後一步,真正執行類中定義的程式程式碼。是執行類構造器 < clinit>()方法的過程。

  • < clinit>()方法是由編譯自動收集類中所有變數的賦值動作和靜態語句塊(static{}塊)中的語句合併產生的,編譯器收集的順序是由語句在原始檔中出現的順序所決定的,靜態語句塊中只能訪問到定義在靜態語句塊之前的變數,定義在他之後的變數,在前面的靜態語句塊可以賦值,但是不能訪問。
  • < clinit>()方法與類的建構函式不同,它不需要顯式的呼叫父類構造器,虛擬機器會保證在子類的< clinit>()方法執行之前,父類的< clinit>()方法已經執行完畢。因此虛擬機器中第一個執行< clinit>()方法的類一定是java.lang.Object。
  • 由於父類的< clinit>()方法先執行,所以父類的靜態語句塊一定先於子類的靜態語句塊執行。
  • < clinit>()方法對於類或介面來說並不是必須的,如果一個類中沒有靜態語句塊,也沒有對變數的賦值操作,那麼編譯器可以不為這個類生成< clinit>()方法。
  • 解耦中不能使用靜態語句塊,但任然會有變數的初始化賦值操作,所以介面與類一樣都會生成< clinit>()方法。但介面與類不同的是,執行介面的< clinit>()方法不需要先執行父介面的< clinit>()方法。只有當父介面中定義的變數使用時,父接口才會初始化。另外,介面的實現類在初始化時也一樣不會直接介面的< clinit>()方法。
  • 虛擬機器會保證一個類的< clinit>()方法在多執行緒環境中被正確的加鎖、同步,如果多個執行緒同時去初始化一個類,那麼只會有一個執行緒去執行這個類的< clinit>()方法,其他執行緒會阻塞等待,直到活動執行緒執行< clinit>()方法完畢。