天天看點

JVM-類加載機制-《深入了解Java虛拟機》學習筆記

類加載機制

虛拟機把描述類的資料從 Class 檔案加載到記憶體,并對資料進行校驗、裝換解析和初始化,最終形成可以被虛拟機直接使用的 Java 類型。

在 Java 語言中,類型的加載、連接配接和初始化過程都是在程式運作期間完成的。

類加載時機

JVM-類加載機制-《深入了解Java虛拟機》學習筆記

其中加載、驗證、準備、初始化和解除安裝這五個階段的順序是确定的。解析階段可以在初始化之後再開始(運作時綁定或動态綁定或晚期綁定)。

以下五種情況必須對類進行初始化(而加載、驗證、準備自然需要在此之前完成):

  1. 遇到 new、getstatic、putstatic 或 invokestatic 這 4 條位元組碼指令時沒初始化觸發初始化。使用場景:使用 new 關鍵字執行個體化對象、讀取一個類的靜态字段(被 final 修飾、已在編譯期把結果放入常量池的靜态字段除外)、調用一個類的靜态方法。
  2. 使用 java.lang.reflect 包的方法對類進行反射調用的時候。
  3. 當初始化一個類的時候,如果發現其父類還沒有進行初始化,則需先觸發其父類的初始化。
  4. 當虛拟機啟動時,使用者需指定一個要加載的主類(包含 main() 方法的那個類),虛拟機會先初始化這個主類。
  5. 當使用 JDK 1.7 的動态語言支援時,如果一個 java.lang.invoke.MethodHandle 執行個體最後的解析結果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且這個方法句柄所對應的類沒有進行過初始化,則需先觸發其初始化。
  6. 當一個借口中定義了JDK8新加入的預設方法(被default關鍵字修飾的接口)時,如果這個接口的實作了發生了初始化,那該接口要在其之前被初始化

類加載過程

完整過程
1、加載
簡單的說,類加載階段就是由類加載器負責根據一個類的全限定名來讀取此類的二進制位元組流到JVM内部,并存儲在運作時記憶體區的方法區,然後将其轉換為一個與目标類型對應的java.lang.Class對象執行個體(Java虛拟機規範并沒有明确要求一定要存儲在堆區中,隻是hotspot選擇将Class對戲那個存儲在方法區中),這個Class對象在日後就會作為方法區中該類的各種資料的通路入口。
2、連結
連結階段要做的是将加載到JVM中的二進制位元組流的類資料資訊合并到JVM的運作時狀态中,經由驗證、準備和解析三個階段。
1)、驗證
驗證類資料資訊是否符合JVM規範,是否是一個有效的位元組碼檔案,驗證内容涵蓋了類資料資訊的格式驗證、語義分析、操作驗證等。
格式驗證:驗證是否符合class檔案規範
語義驗證:檢查一個被标記為final的類型是否包含子類;檢查一個類中的final方法視訊被子類進行重寫;確定父類和子類之間沒有不相容的一些方法聲明(比如方法簽名相同,但方法的傳回值不同)
操作驗證:在操作數棧中的資料必須進行正确的操作,對常量池中的各種符号引用執行驗證(通常在解析階段執行,檢查是否通過富豪引用中描述的全限定名定位到指定類型上,以及類成員資訊的通路修飾符是否允許通路等)
2)、準備
為類中的所有靜态變量配置設定記憶體空間,并為其設定一個初始值(由于還沒有産生對象,執行個體變量不在此操作範圍内)
被final修飾的靜态變量,會直接賦予原值;類字段的字段屬性表中存在ConstantValue屬性,則在準備階段,其值就是ConstantValue的值
3)、解析
将常量池中的符号引用轉為直接引用(得到類或者字段、方法在記憶體中的指針或者偏移量,以便直接調用該方法),這個可以在初始化之後再執行。
可以認為是一些靜态綁定的會被解析,動态綁定則隻會在運作是進行解析;靜态綁定包括一些final方法(不可以重寫),static方法(隻會屬于目前類),構造器(不會被重寫)
3、初始化
将一個類中所有被static關鍵字辨別的代碼統一執行一遍,如果執行的是靜态變量,那麼就會使用使用者指定的值覆寫之前在準備階段設定的初始值;如果執行的是static代碼塊,那麼在初始化階段,JVM就會執行static代碼塊中定義的所有操作。
所有類變量初始化語句和靜态代碼塊都會在編譯時被前端編譯器放在收集器裡頭,存放到一個特殊的方法中,這個方法就是<clinit>方法,即類/接口初始化方法。該方法的作用就是初始化一個中的變量,使用使用者指定的值覆寫之前在準備階段裡設定的初始值。任何invoke之類的位元組碼都無法調用<clinit>方法,因為該方法隻能在類加載的過程中由JVM調用。
如果父類還沒有被初始化,那麼優先對父類初始化,但在<clinit>方法内部不會顯示調用父類的<clinit>方法,由JVM負責保證一個類的<clinit>方法執行之前,它的父類<clinit>方法已經被執行。
JVM必須確定一個類在初始化的過程中,如果是多線程需要同時初始化它,僅僅隻能允許其中一個線程對其執行初始化操作,其餘線程必須等待,隻有在活動線程執行完對類的初始化操作之後,才會通知正在等待的其他線程。
           
加載
  1. 通過一個類的全限定名來擷取定義次類的二進制流(ZIP 包、網絡、運算生成、JSP 生成、資料庫讀取)。
  2. 将這個位元組流所代表的靜态存儲結構轉化為方法區的運作時資料結構。
  3. 在記憶體中生成一個代表這個類的 java.lang.Class 對象,作為方法去這個類的各種資料的通路入口。

數組類的特殊性:數組類本身不通過類加載器建立,它是由 Java 虛拟機直接建立的。但數組類與類加載器仍然有很密切的關系,因為數組類的元素類型最終是要靠類加載器去建立的,數組建立過程如下:

  1. 如果數組的元件類型是引用類型,那就遞歸采用類加載加載。
  2. 如果數組的元件類型不是引用類型,Java 虛拟機會把數組标記為引導類加載器關聯。
  3. 數組類的可見性與他的元件類型的可見性一緻,如果元件類型不是引用類型,那數組類的可見性将預設為 public。

記憶體中執行個體的 java.lang.Class 對象存在方法區中。作為程式通路方法區中這些類型資料的外部接口。

加載階段與連接配接階段的部分内容是交叉進行的,但是開始時間保持先後順序。

驗證
是連接配接的第一步,確定 Class 檔案的位元組流中包含的資訊符合目前虛拟機要求。

檔案格式驗證

  1. 是否以魔數 0xCAFEBABE 開頭
  2. 主、次版本号是否在目前虛拟機處理範圍之内
  3. 常量池的常量是否有不被支援常量的類型(檢查常量 tag 标志)
  4. 指向常量的各種索引值中是否有指向不存在的常量或不符合類型的常量
  5. CONSTANT_Utf8_info 型的常量中是否有不符合 UTF8 編碼的資料
  6. Class 檔案中各個部分集檔案本身是否有被删除的附加的其他資訊
  7. ……

隻有通過這個階段的驗證後,位元組流才會進入記憶體的方法區進行存儲,是以後面 3 個驗證階段全部是基于方法區的存儲結構進行的,不再直接操作位元組流。

中繼資料驗證

  1. 這個類是否有父類(除 java.lang.Object 之外)
  2. 這個類的父類是否繼承了不允許被繼承的類(final 修飾的類)
  3. 如果這個類不是抽象類,是否實作了其父類或接口之中要求實作的所有方法
  4. 類中的字段、方法是否與父類産生沖突(覆寫父類 final 字段、出現不符合規範的重載)

這一階段主要是對類的中繼資料資訊進行語義校驗,保證不存在不符合 Java 語言規範的中繼資料資訊。

位元組碼驗證

  1. 保證任意時刻操作數棧的資料類型與指令代碼序列都鞥配合工作(不會出現按照 long 類型讀一個 int 型資料)
  2. 保證跳轉指令不會跳轉到方法體以外的位元組碼指令上
  3. 保證方法體中的類型轉換是有效的(子類對象指派給父類資料類型是安全的,反過來不合法的)
  4. ……

這是整個驗證過程中最複雜的一個階段,主要目的是通過資料流和控制流分析,确定程式語義是合法的、符合邏輯的。這個階段對類的方法體進行校驗分析,保證校驗類的方法在運作時不會做出危害虛拟機安全的事件。

符号引用驗證

  1. 符号引用中通過字元串描述的全限定名是否能找到對應的類
  2. 在指定類中是否存在符方法的字段描述符以及簡單名稱所描述的方法和字段
  3. 符号引用中的類、字段、方法的通路性(private、protected、public、default)是否可被目前類通路
  4. ……

最後一個階段的校驗發生在迅疾将符号引用轉化為直接引用的時候,這個轉化動作将在連接配接的第三階段——解析階段中發生。符号引用驗證可以看做是對類自身以外(常量池中的各種符号引用)的資訊進行比對性校驗,還有以上提及的内容。

符号引用的目的是確定解析動作能正常執行,如果無法通過符号引用驗證将抛出一個 java.lang.IncompatibleClass.ChangeError 異常的子類。如 java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError 等。

準備
這個階段正式為類配置設定記憶體并設定類變量初始值,記憶體在方法去中配置設定(含 static 修飾的變量不含執行個體變量)。

public static int value = 1127;

這句代碼在初始值設定之後為 0,因為這時候尚未開始執行任何 Java 方法。而把 value 指派為 1127 的 putstatic 指令是程式被編譯後,存放于 clinit() 方法中,是以初始化階段才會對 value 進行指派。

基本資料類型的零值

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

特殊情況:如果類字段的字段屬性表中存在 ConstantValue 屬性(被final修飾),在準備階段虛拟機就會根據 ConstantValue 的設定将 value 指派為 1127。

解析
這個階段是虛拟機将常量池内的符号引用替換為直接引用的過程。
  1. 符号引用

    符号引用以一組符号來描述所引用的目标,符号可以使任何形式的字面量。

  2. 直接引用

    直接引用可以使直接指向目标的指針、相對偏移量或是一個能間接定位到目标的句柄。直接引用和迅疾的記憶體布局實作有關

解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符 7 類符号引用進行,分别對應于常量池的 7 中常量類型。

初始化
前面過程都是以虛拟機主導,而初始化階段開始執行類中的 Java 代碼。
  • 會把static的指派為對應的值
  • 普通的開始先指派為零值

以下情況必須初始化

  1. 遇到 new、getstatic、putstatic 或 invokestatic 這 4 條位元組碼指令時沒初始化觸發初始化。使用場景:使用 new 關鍵字執行個體化對象、讀取一個類的靜态字段(被 final 修飾、已在編譯期把結果放入常量池的靜态字段除外)、調用一個類的靜态方法。
  2. 使用 java.lang.reflect 包的方法對類進行反射調用的時候。
  3. 當初始化一個類的時候,如果發現其父類還沒有進行初始化,則需先觸發其父類的初始化。
  4. 當虛拟機啟動時,使用者需指定一個要加載的主類(包含 main() 方法的那個類),虛拟機會先初始化這個主類。
  5. 當使用 JDK 1.7 的動态語言支援時,如果一個 java.lang.invoke.MethodHandle 執行個體最後的解析結果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且這個方法句柄所對應的類沒有進行過初始化,則需先觸發其初始化。
  6. 當一個借口中定義了JDK8新加入的預設方法(被default關鍵字修飾的接口)時,如果這個接口的實作了發生了初始化,那該接口要在其之前被初始化