看懂Class文件的裝載流程


Class文件的加載過程

ClassLoader的工作模式

類的熱加載


1 Class文件的裝載流程

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

1.1 類裝載的條件

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

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

除了以上情況屬於主動使用外,其他情況均屬於被動使用,被動使用不會引起類的初始化

例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文件中沒有該函數存在。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM