10年程序員淺談JVM類加載的那些事

前言

Java源代碼被編譯成class字節碼,最終需要加載到虛擬機中才能運行。整個生命週期包括:加載、驗證、準備、解析、初始化、使用和卸載7個階段。

10年程序員淺談JVM類加載的那些事

加載

1、通過一個類的全限定名獲取描述此類的二進制字節流;

2、將這個字節流所代表的靜態存儲結構保存為方法區的運行時數據結構;

3、在java堆中生成一個代表這個類的java.lang.Class對象,作為訪問方法區的入口;

虛擬機設計團隊把加載動作放到JVM外部實現,以便讓應用程序決定如何獲取所需的類,實現這個動作的代碼稱為“類加載器”,JVM提供了3種類加載器:

1、啟動類加載器(Bootstrap ClassLoader):負責加載 JAVA_HOME\\lib 目錄中的,或通過-Xbootclasspath參數指定路徑中的,且被虛擬機認可(按文件名識別,如rt.jar)的類。

2、擴展類加載器(Extension ClassLoader):負責加載 JAVA_HOME\\lib\\ext 目錄中的,或通過java.ext.dirs系統變量指定路徑中的類庫。

3、應用程序類加載器(Application ClassLoader):負責加載用戶路徑(classpath)上的類庫。

JVM基於上述類加載器,通過雙親委派模型進行類的加載,當然我們也可以通過繼承java.lang.ClassLoader實現自定義的類加載器。

10年程序員淺談JVM類加載的那些事

雙親委派模型工作過程:當一個類加載器收到類加載任務,優先交給其父類加載器去完成,因此最終加載任務都會傳遞到頂層的啟動類加載器,只有當父類加載器無法完成加載任務時,才會嘗試執行加載任務。

雙親委派模型有什麼好處?

比如位於rt.jar包中的類java.lang.Object,無論哪個加載器加載這個類,最終都是委託給頂層的啟動類加載器進行加載,確保了Object類在各種加載器環境中都是同一個類。

驗證

為了確保Class文件符合當前虛擬機要求,需要對其字節流數據進行驗證,主要包括格式驗證、元數據驗證、字節碼驗證和符號引用驗證。

  1. 格式驗證
  2. 驗證字節流是否符合class文件格式的規範,並且能被當前虛擬機處理,如是否以魔數0xCAFEBABE開頭、主次版本號是否在當前虛擬機處理範圍內、常量池是否有不支持的常量類型等。只有經過格式驗證的字節流,才會存儲到方法區的數據結構,剩餘3個驗證都基於方法區的數據進行。
  • 元數據驗證
  • 對字節碼描述的數據進行語義分析,以保證符合Java語言規範,如是否繼承了final修飾的類、是否實現了父類的抽象方法、是否覆蓋了父類的final方法或final字段等。
  • 字節碼驗證
  • 對類的方法體進行分析,確保在方法運行時不會有危害虛擬機的事件發生,如保證操作數棧的數據類型和指令代碼序列的匹配、保證跳轉指令的正確性、保證類型轉換的有效性等。
  • 符號引用驗證
  • 為了確保後續的解析動作能夠正常執行,對符號引用進行驗證,如通過字符串描述的全限定名是都能找到對應的類、在指定類中是否存在符合方法的字段描述符等。

準備

在準備階段,為類變量(static修飾)在方法區中分配內存並設置初始值。

private static int var = 100;

準備階段完成後,var 值為0,而不是100。在初始化階段,才會把100賦值給val,但是有個特殊情況:

private static final int VAL= 100;

在編譯階段會為VAL生成ConstantValue屬性,在準備階段虛擬機會根據ConstantValue屬性將VAL賦值為100。

解析

解析階段是將常量池中的符號引用替換為直接引用的過程,符號引用和直接引用有什麼不同?

1、符號引用使用一組符號來描述所引用的目標,可以是任何形式的字面常量,定義在Class文件格式中。

2、直接引用可以是直接指向目標的指針、相對偏移量或則能間接定位到目標的句柄。

初始化

初始化階段是執行類構造器<clinit>方法的過程,<clinit>方法由類變量的賦值動作和靜態語句塊按照在源文件出現的順序合併而成,該合併操作由編譯器完成。/<clinit>/<clinit>

 private static int value = 100;
static int a = 100;
static int b = 100;
static int c;
static {

c = a + b;
System.out.println("it only run once");
}

1、<clinit>方法對於類或接口不是必須的,如果一個類中沒有靜態代碼塊,也沒有靜態變量的賦值操作,那麼編譯器不會生成<clinit>;/<clinit>/<clinit>

2、<clinit>方法與實例構造器不同,不需要顯式的調用父類的<clinit>方法,虛擬機會保證父類的<clinit>優先執行;/<clinit>/<clinit>/<clinit>

3、為了防止多次執行<clinit>,虛擬機會確保<clinit>方法在多線程環境下被正確的加鎖同步執行,如果有多個線程同時初始化一個類,那麼只有一個線程能夠執行<clinit>方法,其它線程進行阻塞等待,直到<clinit>執行完成。/<clinit>/<clinit>/<clinit>/<clinit>

4、注意:執行接口的<clinit>方法不需要先執行父接口的<clinit>,只有使用父接口中定義的變量時,才會執行。/<clinit>/<clinit>

10年程序員淺談JVM類加載的那些事

類初始化場景

虛擬機中嚴格規定了有且只有5種情況必須對類進行初始化。

  • 執行new、getstatic、putstatic和invokestatic指令;
  • 使用reflect對類進行反射調用;
  • 初始化一個類的時候,父類還沒有初始化,會事先初始化父類;
  • 啟動虛擬機時,需要初始化包含main方法的類;
  • 在JDK1.7中,如果java.lang.invoke.MethodHandler實例最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,並且這個方法句柄對應的類沒有進行初始化;

以下幾種情況,不會觸發類初始化

1、通過子類引用父類的靜態字段,只會觸發父類的初始化,而不會觸發子類的初始化。

class Parent {
static int a = 100;
static {
System.out.println("parent init!");
}
}
class Child extends Parent {

static {
System.out.println("child init!");
}
}
public class Init{
public static void main(String[] args){
System.out.println(Child.a);
}
}

輸出結果為:

parent init!

100

2、定義對象數組,不會觸發該類的初始化。

public class Init{ 
public static void main(String[] args){
Parent[] parents = new Parent[10];
}
}

無輸出,說明沒有觸發類Parent的初始化,但是這段代碼做了什麼?先看看生成的字節碼指令

10年程序員淺談JVM類加載的那些事

anewarray指令為新數組分配空間,並觸發[Lcom.ctrip.ttd.whywhy.Parent類的初始化,這個類由虛擬機自動生成。

3、常量在編譯期間會存入調用類的常量池中,本質上並沒有直接引用定義常量的類,不會觸發定義常量所在的類。

class Const {
static final int A = 100;
static {
System.out.println("Const init");
}
}
public class Init{
public static void main(String[] args){
System.out.println(Const.A);
}
}

輸出:

100

說明沒有觸發類Const的初始化,在編譯階段,Const類中常量A的值100存儲到Init類的常量池中,這兩個類在編譯成class文件之後就沒有聯繫了。

4、通過類名獲取Class對象,不會觸發類的初始化。

public class test {
public static void main(String[] args) throws ClassNotFoundException {
Class c_dog = Dog.class;
Class clazz = Class.forName("zzzzzz.Cat");
}
}
class Cat {
private String name;

private int age;
static {
System.out.println("Cat is load");
}
}
class Dog {
private String name;
private int age;
static {
System.out.println("Dog is load");
}
}

執行結果:Cat is load,所以通過Dog.class並不會觸發Dog類的初始化動作。

5、通過Class.forName加載指定類時,如果指定參數initialize為false時,也不會觸發類初始化,其實這個參數是告訴虛擬機,是否要對類進行初始化。

public class test {
public static void main(String[] args) throws ClassNotFoundException {
Class clazz = Class.forName("zzzzzz.Cat", false, Cat.class.getClassLoader());
}
}
class Cat {
private String name;
private int age;
static {
System.out.println("Cat is load");
}
}

6、通過ClassLoader默認的loadClass方法,也不會觸發初始化動作

new ClassLoader(){}.loadClass("zzzzzz.Cat");
10年程序員淺談JVM類加載的那些事


分享到:


相關文章: