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>()方法。同一個類加載器下,一個類只會被加載一次。

描述類的實例化順序,比如父類靜態數據、構造函數、字段、子類靜態數據、構造函數、字段,當new的時候,他們的執行順序

class Parent {

  private static int pa = 1;
  private static int pb;
  private int pc = initPc();

  static {
    System.out.println("1. Parent靜態代碼塊");
    pb = 1;
    System.out.println("1. Parent靜態代碼塊爲pb賦值");
  }

  int initPc() {
    System.out.println("3. Parent實例化屬性pc");
    return 3;
  }

  public Parent() {
    System.out.println("4. Parent構造函數");
  }
}

class Sub extends Parent {

  private static int sa = 11;
  private static int sb;

  private int sc = initSc();

  private int initSc() {
    System.out.println("5. Sub 實例化屬性sc");
    return 0;
  }

  static {
    System.out.println("2. Sub 靜態代碼塊");
    sb = 12;
    System.out.println("2. Sub 靜態代碼塊爲sb賦值");
  }

  public Sub() {
    System.out.println("6. Sub 構造函數");
  }
}

public class App {

  /** 數據結果:
   * 1. Parent靜態代碼塊
   * 1. Parent靜態代碼塊爲pb賦值
   * 2. Sub 靜態代碼塊
   * 2. Sub 靜態代碼塊爲sb賦值
   * 3. Parent實例化屬性pc
   * 4. Parent構造函數
   * 5. Sub 實例化屬性sc
   * 6. Sub 構造函數
   */
  public static void main(String[] args) {
    Sub sub = new Sub();
  }
}

結果:

  • Parent靜態屬性
  • Parent靜態代碼塊
  • Sub靜態屬性
  • Sub靜態代碼塊
  • Parent實例屬性
  • Parent構造函數
  • Sub實例屬性
  • Sub構造函數

類加載器

雙親委派模型

絕大部分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;
  }
}

參考個案例分析:http://blog.csdn.net/u013256816/article/details/50837863

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章