看懂Class文件的裝載流程

Class文件的加載過程java

ClassLoader的工做模式數據結構

類的熱加載函數


1 Class文件的裝載流程佈局

只有被java虛擬機裝載的Class類型才能在程序中使用(注意裝載和加載的區別優化

1.1 類裝載的條件日誌

 Class只有在必需要使用的時候纔會被裝載,Java虛擬機不會無條件的裝載Class類型。Java虛擬機規定:一個類或者接口在初次使用時,必須進行初始化。這裏的使用指的是主動使用,主動使用有如下幾種狀況:對象

  • 當建立一個類的實例時,好比使用new關鍵字,或者經過反射、克隆、反序列化。
  • 當調用類的靜態方法時,即當使用了字節碼invokestatic指令
  • 當使用類或者接口的靜態字段時(final常量除外),即便用getstatic或者putstatic指令
  • 當使用java.lang.reflect包中的方法反射類的方法時
  • 當初始化子類時,必須先初始化父類
  • 做爲啓動虛擬機、含有main方法的那個類

除了以上狀況屬於主動使用外,其餘狀況均屬於被動使用,被動使用不會引發類的初始化blog

例1:主動使用接口

public class Parent{內存

  static{

    System.out.println("Parent init");

  }

}

public class Child{

  static{

    System.out.println("Child init");

  }

}

public class InitMain{

  public static void main(String[] args){

    Child c = new Child();

  }

}

以上聲明瞭3個類:Parent Child InitMain,Child類爲Parent類的子類。若Parent被初始化,根據代碼中的static塊可知,將會打印"Parent init",若Child被初始化,則會打印"Child init"。執行InitMain,結果爲:

Parent init 

Child init

由此可知,系統首先裝載Parent類,接着裝載Child類。符合主動裝載中的兩個條件,使用new關鍵字建立類的實例會裝載相關的類,以及在初始化子類時,必須先初始化父類。

例2 :被動裝載

public class Parent{

  static{

    System.out.println("Parent init ");

  }

  public static int v = 100; //靜態字段

}

public class Child extends Parent{

  static{

    System.out.println("Child init");

  }

}

public class UserParent{

  public static void main(String[] args){

    System.out.println(Child.v);

  }

}

Parent中有靜態變量v,而且在UserParent中,使用其子類Child去調用父類中的變量。

運行代碼:

Parent init

100

雖然在UserParent中,直接訪問了子類對象,可是Child子類並未初始化,只有Parent父類進行初始化。因此,在引用一個字段時,只有直接定義該字段的類,纔會被初始化。

注意:雖然Child類沒有被初始化,可是,此時Child類已經被系統加載,只是沒有進入初始化階段。

能夠使用-XX:+ThraceClassLoading 參數運行這段代碼,查看日誌,即可以看到Child類確實被加載了,只是初始化沒有進行

 

例3 :引用final常量

public class FinalFieldClass{

  public static final String constString = "CONST";

  static{

    System.out.println("FinalFieldClass init");

  }

}

public class UseFinalField{

  public static void main(String[] args){

    System.out.println(FinalFieldClass.constString);

  }

}

運行代碼:CONST

FinalFieldClass類沒有由於其常量字段constString被引用而初始化,這是由於在Class文件生成時,final常量因爲其不變性,作了適當的優化。

分析UseFinalField類生成的Class文件,能夠看到main函數的字節碼爲:

在字節碼偏移3的位置,經過Idc將常量池第22項入棧,在此Class文件中常量池第22項爲:

#22 = String        #23     //CONST

#23 = UTF8         CONST

由此能夠看出,編譯後的UseFinalField.class中,並無引用FinalFieldClass類,而是將其final常量直接存放在常量池中,所以,FinalFiledClass類天然不會被加載。(javac在編譯時,將常量直接植入目標類,再也不使用被引用類)經過捕獲類加載日誌(部分日誌)能夠看出:

注意:並非在代碼中出現的類,就必定會被加載或者初始化,若是不符合主動使用的條件,類就不會被初始化。


 

1.2 類裝載的整個過程

1)加載類

加載類處於類裝載的第一個階段。

加載類時,JVM必須完成:

  • 經過類的全名,獲取類的二進制數據流
  • 解析類的二進制數據流爲方法區內的數據結構
  • 建立java.lang.Class類的實例,表示該類型

2)鏈接

 1 驗證類:

當類被加載到系統後,就開始鏈接操做,驗證是鏈接的第一步。

主要目的是保證加載的字節碼是符合規範的。驗證的步驟如圖:

2 準備

當一個類驗證經過後,虛擬機就會進入準備階段,在這個階段,虛擬機會爲這個類分配相應的內存空間,並設置初始值。

java虛擬機爲各類類型變量默認的初始值如表:

類型 默認初始值
int 0
long 0L
short (short)0
char \u0000
boolean false
reference null
float 0f
double 0f

 

 

 

 

 

 

 

 

 注意:java並不支持boolean類型,對於boolean類型,內部實現是Int,因爲int的默認值是0,故對應的,boolean的默認值是false

若是類屬於常量字段,那麼常量字段也會在準備階段被附上正確的值,這個賦值屬於java虛擬機的行爲,屬於變量的初始化。事實上,在準備階段,不會有任何java代碼被執行。

3 解析類

在準備階段完成後,就進入瞭解析階段。

解析階段的任務就是將類、接口、字段和方法的符號引用轉爲直接引用。

符號引用就是一些字面量的引用,和虛擬機的內部數據結構和內存佈局無關。比較容易理解的就是在Class類文件中,經過常量池進行大量的符號引用。

具體能夠使用JclassLib軟件查看Class文件的結構:::

 

3)初始化

初始化時類裝載的最後一個階段。若是前面的步驟沒有出現問題,那麼表示類能夠順利裝載到系統中。此時,類纔會開始執行java字節碼。

初始化階段的重要工做是執行類的初始化方法<clinit>。方法<clinit>是由編譯器自動生成的,它是由類靜態成員的賦值語句以及static語句塊合併產生的。

 例如:

public class SimpleStatic{

  public static int id = 1;

  public static int number;

  static{

    number = 4;

  }

}

java編譯器爲這段代碼生成以下的<clinit>:

0 iconst_1
1 putstatic #2 <Demo.id>
4 iconst_4
5 putstatic #3 <Demo.number>
8 return

能夠看出,生成的<clinit>函數中,整合了SimpleStatic類中的static賦值語句以及static語句塊,前後對id和number兩個成員變量進行賦值

因爲在加載一個類以前,虛擬機老是會試圖加載該類的父類,所以父類的<clinit>老是在子類<clinit>以前被調用。也就是說,子類的static塊優先級高於父類。

public class ChildStatic extends Demo{
  static{
    number = 2;
  }
  public static void main(String[] args){
    System.out.println(number);
  }
}

運行可知:

2

說明父類的<clinit>老是在子類<clinit>以前被調用。

注意:java編譯器並非爲全部的類都產生<clinit>初始化函數,若是一個類既沒有賦值語句,也沒有static語句塊,那麼生成的<clinit>函數就應該爲空,所以,編譯器就不會爲該類插入<clinit>函數

例如:

public class StaticFinalClass{

  public static final int i=1;

  public static final int j=2;

}

因爲StaticFinalClass只有final常量,而final常量在準備階段初始化,而不在初始化階段處理,所以對於StaticFinalClass類來講,<clinit>就無事可作,所以,在產生的class文件中沒有該函數存在。

相關文章
相關標籤/搜索