天天看點

Java什麼時候會觸發類初始化及原理(詳解)Java是如何加載并初始化的

先上結論:

以下情況會觸發類的初始化:

  1. 遇到new,getstatic,putstatic,invokestatic這4條指令;
  2. 使用java.lang.reflect包的方法對類進行反射調用;
  3. 初始化一個類的時候,如果發現其父類沒有進行過初始化,則先初始化其父類(注意!如果其父類是接口的話,則不要求初始化父類);
  4. 當虛拟機啟動時,使用者需要指定一個要執行的主類(包含main方法的那個類),虛拟機會先初始化這個主類;
  5. 當使用jdk1.7的動态語言支援時,如果一個java.lang.invoke.MethodHandle執行個體最後的解析結果REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄,并且這個方法句柄所對應的類沒有進行過初始化,則先觸發其類初始化;

以下情況不會觸發類的初始化:

  1. 同類子類引用父類的靜态字段,不會導緻子類初始化。至于是否會觸發子類的加載和驗證,取決于虛拟機的具體實作;
public class ClassTest1 {
    public static void main(String[] args) {
        System.out.println(SubClass.num);
//        SubClass subClass=new SubClass();
//        System.out.println(SubClass.num);
    }
}
class SubClass extends SuperClass{
    static {
        System.out.println(111111111);
    }
}
class SuperClass{
    static{
        System.out.println(2222222);
    }
    static int num=100;
}
           
Java什麼時候會觸發類初始化及原理(詳解)Java是如何加載并初始化的

2.通過數組定義來引用類,也不會觸發類的初始化;例如:People[] ps = new People[100];

public class ClassTest2 {
    public static void main(String[] args) {
        ArrayClass[] why=new ArrayClass[10];
    }
}

class ArrayClass{
    static {
        System.out.println(1111);
    }
}
           
Java什麼時候會觸發類初始化及原理(詳解)Java是如何加載并初始化的

3.引用一個類的常量也不會觸發類的初始化

public class ClassTest3 {
    public static void main(String[] args) {
        System.out.println(ConstVariable.TEL);
    }
}
class ConstVariable{
    static {
        System.out.println(1111);
    }
    public static final String TEL="10086";
}
           

Java什麼時候會觸發類初始化及原理(詳解)Java是如何加載并初始化的

Java是如何加載并初始化的

Java虛拟機如何把編譯好的.class檔案加載到虛拟機裡面?加載之後如何初始化類?靜态類變量和執行個體類變量的初始化過程是否相同,分别是如何初始化的呢?這篇文章就

是解決上面3個問題的。

若有不正之處,請多多諒解并歡迎各位能夠給予批評指正,提前謝謝各位了。

1. Java虛拟機加載.class過程

虛拟機把Class檔案加載到記憶體,然後進行校驗,解析和初始化,最終形成java類型,這就是虛拟機的類加載機制。加載,驗證,準備,初始化這5個階段的順序是确定的,

類的加載過程,必須按照這種順序開始。這些階段通常是互相交叉和混合進行的。解析階段在某些情況下,可以在初始化階段之後再開始---為了支援java語言的運作時綁定。

Java虛拟機規範中,沒有強制限制什麼時候要開始加載,但是,卻嚴格規定了幾種情況必須進行初始化(加載,驗證,準備則需要在初始化之前開始):

1)遇到 new、getstatic、putstatic、或者invokestatic 這4條位元組碼指令,如果沒有類沒有進行過初始化,則觸發初始化

2)使用java.lang.reflect包的方法,對壘進行反射調用的時候,如果沒有初始化,則先觸發初始化

3)初始化一個類時候,如果發現父類沒有初始化,則先觸發父類的初始化

2. 加載,驗證,解析

加載就是通過指定的類全限定名,擷取此類的二進制位元組流,然後将此二進制位元組流轉化為方法區的資料結構,在記憶體中生成一個代表這個類的Class對象。驗證是為了确

保Class檔案中的位元組流符合虛拟機的要求,并且不會危害虛拟機的安全。加載和驗證階段比較容易了解,這裡就不再過多的解釋。解析階段比較特殊,解析階段是虛拟機

将常量池中的符号引用轉換為直接引用的過程。如果想明白解析的過程,得先了解一點class檔案的一些資訊。class檔案采用一種類似C語言的結構體的僞結構來存儲我們編

碼的java類的各種資訊。其中,class檔案中常量池(constant_pool)是一個類似表格的倉庫,裡面存儲了我們編寫的java類的類和接口的全限定名,字段的名稱和描述符,

方法的名稱和描述符。在java虛拟機将class檔案加載到虛拟機記憶體之後,class類檔案中的常量池資訊以及其他的資料會被儲存到java虛拟機記憶體的方法區。我們知道class檔案

的常量池存放的是java類的全名,接口的全名和字段名稱描述符,方法的名稱和描述符等資訊,這些資料加載到jvm記憶體的方法區之後,被稱做是符号引用。而把這些類的

全限定名,方法描述符等轉化為jvm可以直接擷取的jvm記憶體位址,指針等的過程,就是解析。虛拟機實作可以對第一次的解析結果進行緩存,避免解析動作的重複執行。

在解析類的全限定名的時候,假設目前所處的類為D,如果要把一個從未解析過的符号引用N解析為一個類或者接口C的直接引用,具體的執行辦法就是虛拟機會把代表N的

全限定名傳遞給D的類加載器去加載這個類C。這塊可能不太好了解,但是我們可以直接了解為調用D類的ClassLoader來加載N,然後就完成了N--->C的解析,就可以了。

3. 準備階段

之是以把在解析階段前面的準備階段,拿到解析階段之後講,是因為,準備階段已經涉及到了類資料的初始化指派。和我們本文講的初始化有關系,是以,就拿到這裡來講

述。在java虛拟機加載class檔案并且驗證完畢之後,就會正式給類變量配置設定記憶體并設定類變量的初始值。這些變量所使用的記憶體都将在方法區配置設定。注意這裡說的是類變量,

也就是static修飾符修飾的變量,在此時已經開始做記憶體配置設定,同時也設定了初始值。比如在 Public static int value = 123 這句話中,在執行準備階段的時候,會給value

配置設定記憶體并設定初始值0, 而不是我們想象中的123. 那麼什麼時候 才會将我們寫的123 指派給 value呢?就是我們下面要講的初始化階段。

4. 初始化階段

類初始化階段是類加載過程的最後階段。在這個階段,java虛拟機才真正開始執行類定義中的java程式代碼。Java虛拟機是怎麼完成初始化的呢?這要從編譯開始講起。在編

譯的時候,編譯器會自動收集類中的所有靜态變量(類變量)和靜态語句塊(static{}塊)中的語句合并産生的,編譯器收集的順序是根據語句在java代碼中的順序決定的。

收集完成之後,會編譯成java類的 static{} 方法,java虛拟機則會保證一個類的static{} 方法在多線程或者單線程環境中正确的執行,并且隻執行一次。在執行的過程中,便完

成了類變量的初始化。值得說明的是,如果我們的java類中,沒有顯式聲明static{}塊,如果類中有靜态變量,編譯器會預設給我們生成一個static{}方法。 我們可以通過

javap -c 的指令,來看一下java位元組碼中編譯器為我們生成或者合并的static{} 方法:

public class StaticValInitTest {

    public static int value = 123;

}
           
Java什麼時候會觸發類初始化及原理(詳解)Java是如何加載并初始化的

上面我們講述的是單類的情況,如果出現繼承呢?如果有繼承的話,父類中的類變量該如何初始化?這點由虛拟機來解決:虛拟機會保證在子類的static{}方法執行之

前,父類的static{}方法已經執行完畢。由于父類的static{}方法先執行,也就意味着父類的靜态變量要優先于子類的靜态變量指派操作。

上面講的都是靜态變量,執行個體變量怎麼解決呢?執行個體變量的初始化,其實是和靜态變量的過程是類似的,但是時間和地點都不同哦。我們以下面的Dog類為例來講一講。

public class Dog {

    public String type = "tai di";

    public int age = 3;
}
           

1)當用new Dog() 建立對象的時候,首先在堆上為Dog對象配置設定足夠的空間。

2)這塊存儲空間會被清零,這就是自動将Dog對象中的所有基本類型的資料都設定成了預設值,而引用類型則被設定成了null(類似靜态類的準備階段的過程)

3)Java收集我們的執行個體變量指派語句,合并後在構造函數中執行指派語句。沒有構造函數的,系統會預設給我們生成構造函數。

Java什麼時候會觸發類初始化及原理(詳解)Java是如何加載并初始化的

至此,java類初始化的理論基礎已經完成了,其中的大部分的理論和思想都出自《深入了解java虛拟機》這本書。有了以上的理論基礎,

再複雜的類初始化的情況,我們都可以應對了,下面就拿一個例子做一個具體的分析吧

public class Insect {
    private int i = 9;
    protected int j;
    
    protected static int x1 = printInit("static Insect.x1 initialized");
    
    Insect() {
        System.out.println("基類構造函數階段: i = " + i + ", j = " + j);
        j = 39;
    }
    
    static int printInit(String s) {
        System.out.println(s);
        return 47;
    }
}

public class Beetle extends Insect {
    
    protected int k = printInit("Beetle.k initialized");
    
    protected static int x2 = printInit("static Beetle.x2 initialized");    
    
    public static void main(String[] args) {        
        Beetle b = new Beetle();
    }
}
           

上面例子來自《java程式設計思想》,以上代碼的執行結果是什麼呢?如果對上面我們講的理論了解的話,很容易就知道結果是:

static Insect.x1 initialized

static Beetle.x2 initialized

基類構造函數階段: i = 9, j = 0

Beetle.k initialized

具體的執行結果過程是:

在執行Beetle 類的 main方法的時候,因為該main方法是static方法,我們在上面已經知道,在執行類的static方法的時候,如果該類沒有初始化,則要進行初始化,

是以,我們在執行main方法的時候,會執行加載--驗證--準備--解析---初始化這個過程。在進行最後的初始化的時候,又有一個限制:虛拟機會保證在子類的static{}

方法執行之前,父類的static{}方法已經執行完畢。是以,在執行完解析之後,會先執行父類的初始化,在執行父類初始化的時候,

輸出: static Insect.x1 initialized

然後接着初始化子類,輸出:static Beetle.x2 initialized

以上兩行輸出,是靜态變量的初始化,是在第一次調用靜态方法,即,在執行new、getstatic、putstatic、或者invokestatic 這4條位元組碼指令時候觸發的。是以,

你如果把上例中的static main 方法中的 Beetle b = new Beetle(); 

Java什麼時候會觸發類初始化及原理(詳解)Java是如何加載并初始化的

注釋掉,上面兩行仍然會輸出出來。然後就是執行Beetle b = new Beetle();這句代碼了。我們知道,在執行個體化子類對象的時候,會自動調用父類的構造函數。

是以,接着就輸出:基類構造函數階段: i = 9, j = 0

緊接着是執行自己的構造函數,在堆上建立類執行個體對象,執行個體對象空間清零,然後執行指派語句k = printInit("Beetle.k initialized");

輸出: Beetle.k initialized

至此,整個類加載并初始化完畢,是不是了解起來就很簡單了,趁勝追擊,我們還是再來看一個例子吧:

public class Base {

    Base() {
        preProcess();
    }

    void preProcess() {
    }
}

public class Derived extends Base {

    public String whenAmISet = "set when declared"; 

    @Override 
    void preProcess() {

        whenAmISet = "set in preProcess";

    }    

    public static void main(String[] args) {
        Derived d = new Derived();
        System.out.println(d.whenAmISet);
    }
}
           

一個地方比較繞:父類在執行構造函數的時候,調用了子類(導出類)重載過的方法,在子類的重載方法中,給執行個體變量做了一次指派,正是這次指派,幹擾了我們對類初始化的了解。

我們不管類裡面是怎麼做的,還按照我們上個例子中那樣進行分析:

1. 執行Derived 類 static main 方法的時候,執行類變量初始化,但是此例中父類和子類都沒有類變量,是以此步驟什麼都不做,進行執行個體變量初始化

2. 執行new Derived()的時候,先調用了父類的構造函數,因為子類的重載,調用了子類的preProcess方法,為執行個體變量whenAmISet 指派為"set in preProcess"

3. 然後執行子類Derived 的構造函數,在構造函數中,有編譯器為我們收集生成的執行個體變量指派語句,最終,又将執行個體變量whenAmISet 指派為"set when declared"

4. 是以最終的輸出是: set when declared

如果對這個還不太了解的話,可以再Derived 類裡面添加注釋,改成下面的樣子,輸出看看,是不是對這個執行過程更清晰了呢?

public class Derived extends Base {
    
    // 準備階段指派 whenAmISet=null
    public String whenAmISet = "set when declared";
    
    public Derived() {
        System.out.println("do son constructor");
    }
     
    @Override 
    void preProcess() {
        System.out.println("do son process");
        System.out.println("whenAmISet:" + whenAmISet);
        whenAmISet = "set in preProcess";
        System.out.println("whenAmISet:" + whenAmISet);
        System.out.println("set in preProcess end");
    }

    public static void main(String[] args) {
        Derived d = new Derived();
        System.out.println(d.whenAmISet);
    }
}    
           

參考文獻:

http://www.cnblogs.com/jimxz/p/3974939.html

https://www.cnblogs.com/hujinshui/p/10422521.html