1. 程式人生 > >JVM6:虛擬機器類載入機制

JVM6:虛擬機器類載入機制

文章目錄

類載入時機

類從被載入到虛擬機器記憶體中開始,到卸載出記憶體為止,整個生命週期包括:載入(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)及解除安裝(Unloading)。其中,驗證、準備和解析3個部分統稱為連線(Linking)。如圖所示: 類載入機制

載入、驗證、準備、初始化和解除安裝這5個生命週期部分,是需要按順序開始。解析由於Java支援動態繫結的緣故,所以不一定,有可能在初始化之後再開始。

在虛擬機器規範中,並沒有規定載入階段何時開始;而是規範了以下5種情況時必須得完成初始化:

  1. 遇到newgetstaticputstaticinvokedynamic4個指令時;
  2. 使用java.lang.reflect包的方法對類進行反射呼叫時;
  3. 當初始化一個類時,若父類沒有初始化,則需初始化父類;
  4. 虛擬機器啟動時,使用者指定要執行的主類(包含main()方法的那個類),需要初始化該主類;
  5. 使用JDK1.7動態語言支援時,如果java.lang.invoke.MethodHandle例項最後的解析結果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法控制代碼且該控制代碼對應的類沒有初始化,則需要初始化

以上5種情形,都會觸發類的初始化,稱為主動引用;除此之外,還會有以下3種方式並不會觸發類的初始化,稱為被動引用:

  1. 通過子類引用父類的靜態欄位,不會導致子類的初始化;
public class SuperClass {

  static {
    System.out.println("SuperClass init!");
  }

  public static int value = 123;

  public static final String HELLOWORLD = "Hello World!!";
}

public class SubClass extends SuperClass {

  static {
    System.out.println("SubClass init!");
  }

}

//輸出SuperClass init!
public class NotInitializationDemo1 {

  public static void main(String[] args) {
    System.out.println(SubClass.value);
  }

}
  1. 通過陣列定義來引用類,不會觸發此類的初始化;
public class SuperClass {

  static {
    System.out.println("SuperClass init!");
  }

  public static int value = 123;
}

public class NotInitializationDemo2 {

  public static void main(String[] args) {
    SuperClass[] superClasses = new SuperClass[10];
  }

}
  1. 常量在編譯階段會存入呼叫類的常量池中,本質上並沒有直接引用到定義常量的類,也不會觸發定義常量的類的初始化
public class NotInitializationDemo3 {

  public static void main(String[] args) {
    System.out.println(SuperClass.HELLOWORLD);
  }

}

類載入過程

載入

在載入階段,需要完成3件事情:

  1. 通過類的全限定名來獲取定義此類的二進位制位元組流;注意:此處並沒有說從哪裡獲取二進位制流,可以從ZIP包、網路或者執行時自動計算生成等;
  2. 將二進位制位元組流所代表的靜態儲存結構轉為方法區的執行時資料;
  3. 在記憶體中生成一個代表這個類的java.lang.Class物件,作為方法區這個類的各種資料的訪問入口

驗證

確保Class檔案的位元組流中包含的資訊符合當前虛擬機器的要求,且不會危害虛擬機器自身的安全。 驗證階段大致完成4類驗證:

  1. 檔案格式驗證-是否符合Class檔案格式的規範

    1. 檔案是否已0xCAFEBABE開頭;
    2. 主次版本號是否在虛擬機器支援範圍內;
    3. 常量池的常量中是否有不被支援的常量型別(檢查常量tag標誌);
    4. 指向常量的各種索引值中是否有指向不存在的常量或不符合型別的常量;
    5. CONSTANT_Utf8_info型的常量中是否有不符合UTF8編碼的資料; …
  2. 元資料驗證-是否符合Java語言規範的要求;

    1. 這個類是否有父類,除了java.lang.Object之外;
    2. 這個類的父類是否繼承了不允許被繼承的類
    3. 如果此類不是抽象類,是否實現了父類或介面中要實現的所有方法; …
  3. 位元組碼驗證-通過資料流和控制流,確認程式語義是合法的

    1. 保證跳轉指令不會跳轉到方法體以外的位元組碼指令上; …
  4. 符號引用驗證-對類自身以外(常量池中的各種符號引用)的資訊進行匹配性校驗

    1. 符號引用中通過字串描述的全限定名是否能找到對應的類;
    2. 在指定類中是否存在符合方法的欄位描述符及簡單名稱所描述的方法和欄位;
    3. 符號引用中的類、欄位、方法的訪問性是否可被當前類訪問; …

準備

類變數分配記憶體,並設定類變數的初始值。此處的初始值,是指資料型別的零值,而非使用者程式定義的值。各型別的初始值如下:

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

注意:如果類欄位屬性表中存在ConstantValue屬性,那在準備階段變數會被初始化為ConstantValue屬性所指定的值。如public static final int value = 123;

解析

解析是將常量池內的符號引用替換為直接引用的過程。JVM規範未規定解析階段發生的時間,只要求在執行anewarraycheckcastgetfieldgetstaticinstanceofinvokedynamicinvokeinterfaceinvokespecialinvokestaticinvokevirtualldcldc_wmultianewarraynewputfieldputstatic這16個用於操作符號引用的位元組碼指令前,先對它們所使用的符號引用進行解析。

初始化

初始化是執行類構造器<clinit>()方法的過程。

  1. <clinit>()方法是由編譯器自動收集類中的所有類變數的賦值動作和靜態語句塊(static{}塊)中的語句合併產生的,編譯器收集的順序是由語句在原始檔中出現的順序決定的,靜態語句塊中只能訪問到定義在靜態語句塊之前的變數,定義在其之後的變數,在前面的靜態語句塊可以賦值,但不能被訪問。如:
public class StaticBlockVarTest {

  static {
    i = 0;
    //System.out.println(i);  //非法向前訪問
  }

  static int i = 1;

}
  1. <clinit>()方法與類的建構函式(即init())方法不同,它不需要顯式地呼叫父類構造器,虛擬機器會保證在子類的<clinit>()方法執行之前,父類的<clinit>()方法已經執行完畢。
  2. 由於父類的<clinit>()方法先執行,意味著父類中定義的靜態語句塊要優於子類的變數賦值操作。如:
public class CInitTest {

  public static int A = 1;

  static {
    A = 2;
  }

  static  class CInitSub extends CInitTest {
    public static int B = A;
  }

  public static void main(String[] args) {
    System.out.println(CInitSub.B);
  }

}
  1. <clinit>()方法對於類或介面來說並不是必需的。如果一個類中沒有靜態語句塊,也沒有對變數的賦值操作,那麼編譯器可以不為這個類生成<clinit>()方法。
  2. 介面中不能使用靜態語句塊,但仍然有變數初始化的賦值操作,因此介面與類一樣都會生成<clinit>()方法。但介面與類不同,執行介面的<clinit>()方法不需要先執行父介面的<clinit>()方法。只有當父介面中定義的變數使用時,父接口才會初始化。另外,介面的實現類在初始化時也一樣不會執行介面的<clinit>()方法。
  3. 虛擬機器會保證一個類的<clinit>()方法在多執行緒環境中被正確地加鎖、同步。如果多個執行緒去初始化一個類,那麼只會有一個執行緒去執行這個類的<clinit>()方法,其他執行緒都需要阻塞,直到活動執行緒執行完畢之後喚醒,但是其他執行緒不會再進入<clinit>()方法。同一個類載入器下,一個類只會被載入一次。

類載入器

雙親委派模型

絕大部分Java程式使用以下3種系統提供的類載入器:

  1. 啟動類載入器(Bootstrap ClassLoader)

    負責載入<JAVA_HOME>\lib目錄中,或者被-Xbootclasspath引數所指定的路徑中的,並且是虛擬機器識別的類庫載入到虛擬機器記憶體中

  2. 擴充套件類載入器(Extension ClassLoader)

    此類載入器由sun.misc.Launcher$ExtClassLoader實現,負責載入<JAVA_HOME>\lib\ext目錄中的,或者被java.ext.dirs系統變數所指定的路徑中的所有類庫

  3. 應用程式類載入器(Application ClassLoader)

    此類載入器由sun.misc.Launcher$AppClassLoader實現,負責載入使用者類路徑(ClassPath)上所指定的類庫

如若還有必要,可以加入自定義的類載入器。這些載入器之間的關係一般如圖所示: 雙親委派模型

上圖展示的類載入器之間的層次關係成為類載入器的雙親委派模型。其工作過程:如果一個類載入器收到了類載入的請求,首先不會自己去載入這個類,而是把這個請求委派給父類載入器去完成,每個層次的類載入器都是如此,因此所有的載入請求最終都傳到啟動類載入器中,只有當父載入器無法載入對應的請求時,子載入器才會嘗試去載入。

自定義類載入器

自定義類載入器只需要繼承java.lang.ClassLoader類,重寫findClass()方法:

/**
 * 自定義類載入器.
 */
public class CustomClassLoaderDemo extends ClassLoader {

  private String rootPath;

  public CustomClassLoaderDemo(String rootPath) {
    this.rootPath = rootPath;
  }

  @Override
  protected Class<?> findClass(String s) throws ClassNotFoundException {
    try {
      byte[] classData = loadClassData(s);
      return this.defineClass(s, classData, 0, classData.length);
    } catch (IOException e) {
      e.printStackTrace();
    }

    return null;
  }

  private byte[] loadClassData(String s) throws IOException {
    String s1 = s.replace('.', '\\');
    String classFile = rootPath + File.separatorChar + s1 + ".class";
    FileInputStream fis = new FileInputStream(classFile);

    byte[] classData = new byte[fis.available()];
    fis.read(classData);

    return classData;
  }
}