從一道面試題來認識java類加載時機與過程

說明:本文的內容是看了《深刻理解Java虛擬機:JVM高級特性與最佳實踐》後爲加印象和理解,便記錄了重要的內容。java

 

1  開門見山

之前曾經看到過一個java的面試題,當時以爲此題很簡單,但是本身把代碼運行起來,但是結果並非本身想象的那樣。題目以下:面試

 

1數組

2安全

3數據結構

4編輯器

5佈局

6優化

7編碼

8spa

9

10

11

12

13

14

15

16

17

18

19

20

21

22

class SingleTon {

    private static SingleTon singleTon = new SingleTon();

    public static int count1;

    public static int count2 = 0;

 

    private SingleTon() {

        count1++;

        count2++;

    }

 

    public static SingleTon getInstance() {

        return singleTon;

    }

}

 

public class Test {

    public static void main(String[] args) {

        SingleTon singleTon = SingleTon.getInstance();

        System.out.println("count1=" + singleTon.count1);

        System.out.println("count2=" + singleTon.count2);

    }

}

錯誤答案

 

count1=1

count2=1

 正確答案

 

count1=1

count2=0

爲神馬?爲神馬?這要從java的類加載時機提及。

2 類的加載時機

類從被加載到虛擬機內存中開始,直到卸載出內存爲止,它的整個生命週期包括了:加載、驗證、準備、解析、初始化、使用和卸載這7個階段。其中,驗證、準備和解析這三個部分統稱爲鏈接(linking)

其中,加載、驗證、準備、初始化和卸載這五個階段的順序是肯定的,類的加載過程必須按照這種順序循序漸進的「開始」(僅僅指的是開始,而非執行或者結束,由於這些階段一般都是互相交叉的混合進行,一般會在一個階段執行的過程當中調用或者激活另外一個階段),而解析階段則不必定(它在某些狀況下能夠在初始化階段以後再開始,這是爲了支持Java語言的運行時綁定。

3 什麼時候開始類的初始化

什麼狀況下須要開始類加載過程的第一個階段:"加載"。虛擬機規範中並沒強行約束,這點能夠交給虛擬機的的具體實現自由把握,可是對於初始化階段虛擬機規範是嚴格規定了以下幾種狀況,若是類未初始化會對類進行初始化。

  1. 建立類的實例
  2. 訪問類的靜態變量(除常量【被final修辭的靜態變量】緣由:常量一種特殊的變量,由於編譯器把他們看成值(value)而不是域(field)來對待。若是你的代碼中用到了常變量(constant variable),編譯器並不會生成字節碼來從對象中載入域的值,而是直接把這個值插入到字節碼中。這是一種頗有用的優化,可是若是你須要改變final域的值那麼每一塊用到那個域的代碼都須要從新編譯。
  3. 訪問類的靜態方法
  4. 反射如(Class.forName("my.xyz.Test"))
  5. 當初始化一個類時,發現其父類還未初始化,則先出發父類的初始化
  6. 虛擬機啓動時,定義了main()方法的那個類先初始化

以上狀況稱爲稱對一個類進行「主動引用」,除此種狀況以外,均不會觸發類的初始化,稱爲「被動引用」

接口的加載過程與類的加載過程稍有不一樣。接口中不能使用static{}塊。當一個接口在初始化時,並不要求其父接口所有都完成了初始化,只有真正在使用到父接口時(例如引用接口中定義的常量)纔會初始化。

4 被動引用例子

  1. 子類調用父類的靜態變量,子類不會被初始化。只有父類被初始化。。對於靜態字段,只有直接定義這個字段的類纔會被初始化.
  2. 經過數組定義來引用類,不會觸發類的初始化
  3. 訪問類的常量,不會初始化類

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

class SuperClass {

    static {

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

    }

    public static int value = 123;

}

 

class SubClass extends SuperClass {

    static {

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

    }

}

 

public class Test {

    public static void main(String[] args) {

        System.out.println(SubClass.value);// 被動應用1

        SubClass[] sca = new SubClass[10];// 被動引用2

    }

}

程序運行輸出    superclass init 

                            123

從上面的輸入結果證實了被動引用1與被動引用2

1

2

3

4

5

6

7

8

9

10

11

12

class ConstClass {

    static {

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

    }

    public static final String HELLOWORLD = "hello world";

}

 

public class Test {

    public static void main(String[] args) {

        System.out.println(ConstClass.HELLOWORLD);// 調用類常量

    }

}

程序輸出結果

hello world

從上面的輸出結果證實了被動引用3

5 類的加載過程

5.1 加載

 「加載」(Loading)階段是「類加載」(Class Loading)過程的第一個階段,在此階段,虛擬機須要完成如下三件事情:

       一、 經過一個類的全限定名來獲取定義此類的二進制字節流。

       二、 將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構。

       三、 在Java堆中生成一個表明這個類的java.lang.Class對象,做爲方法區這些數據的訪問入口。

      加載階段便可以使用系統提供的類加載器在完成,也能夠由用戶自定義的類加載器來完成。加載階段與鏈接階段的部份內容(如一部分字節碼文件格式驗證動做)是交叉進行的,加載階段還沒有完成,鏈接階段可能已經開始。

 

5.2 驗證

 

       驗證是鏈接階段的第一步,這一階段的目的是爲了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,而且不會危害虛擬機自身的安全。

       Java語言自己是相對安全的語言,使用Java編碼是沒法作到如訪問數組邊界之外的數據、將一個對象轉型爲它並未實現的類型等,若是這樣作了,編譯器將拒絕編譯。可是,Class文件並不必定是由Java源碼編譯而來,可使用任何途徑,包括用十六進制編輯器(如UltraEdit)直接編寫。若是直接編寫了有害的「代碼」(字節流),而虛擬機在加載該Class時不進行檢查的話,就有可能危害到虛擬機或程序的安全。

      不一樣的虛擬機,對類驗證的實現可能有所不一樣,但大體都會完成下面四個階段的驗證:文件格式驗證、元數據驗證、字節碼驗證和符號引用驗證。

       一、文件格式驗證,是要驗證字節流是否符合Class文件格式的規範,而且能被當前版本的虛擬機處理。如驗證魔數是否0xCAFEBABE;主、次版本號是否正在當前虛擬機處理範圍以內;常量池的常量中是否有不被支持的常量類型……該驗證階段的主要目的是保證輸入的字節流能正確地解析並存儲於方法區中,通過這個階段的驗證後,字節流纔會進入內存的方法區中存儲,因此後面的三個驗證階段都是基於方法區的存儲結構進行的。

       二、元數據驗證,是對字節碼描述的信息進行語義分析,以保證其描述的信息符合Java語言規範的要求。可能包括的驗證如:這個類是否有父類;這個類的父類是否繼承了不容許被繼承的類;若是這個類不是抽象類,是否實現了其父類或接口中要求實現的全部方法……

       三、字節碼驗證,主要工做是進行數據流和控制流分析,保證被校驗類的方法在運行時不會作出危害虛擬機安全的行爲。若是一個類方法體的字節碼沒有經過字節碼驗證,那確定是有問題的;但若是一個方法體經過了字節碼驗證,也不能說明其必定就是安全的。

       四、符號引用驗證,發生在虛擬機將符號引用轉化爲直接引用的時候,這個轉化動做將在「解析階段」中發生。驗證符號引用中經過字符串描述的權限定名是否能找到對應的類;在指定類中是否存在符合方法字段的描述符及簡單名稱所描述的方法和字段;符號引用中的類、字段和方法的訪問性(private、protected、public、default)是否可被當前類訪問

驗證階段對於虛擬機的類加載機制來講,不必定是必要的階段。若是所運行的所有代碼確認是安全的,可使用-Xverify:none參數來關閉大部分的類驗證措施,以縮短虛擬機類加載時間。

5.3 準備

       準備階段是爲類的靜態變量分配內存並將其初始化爲默認值,這些內存都將在方法區中進行分配。準備階段不分配類中的實例變量的內存,實例變量將會在對象實例化時隨着對象一塊兒分配在Java堆中。

        public static int value=123;//在準備階段value初始值爲0 。在初始化階段纔會變爲123 。

5.4 解析

       解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程。

       符號引用(Symbolic Reference):符號引用以一組符號來描述所引用的目標,符號能夠是任何形式的字面量,只要使用時能無歧義地定位到目標便可。符號引用與虛擬機實現的內存佈局無關,引用的目標並不必定已經加載到內存中。

       直接引用(Direct Reference):直接引用能夠是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄。直接引用是與虛擬機實現的內存佈局相關的,若是有了直接引用,那麼引用的目標一定已經在內存中存在。

5.5 初始化

       類初始化是類加載過程的最後一步,前面的類加載過程,除了在加載階段用戶應用程序能夠經過自定義類加載器參與以外,其他動做徹底由虛擬機主導和控制。到了初始化階段,才真正開始執行類中定義的Java程序代碼。

        初始化階段是執行類構造器<clinit>()方法的過程。<clinit>()方法是由編譯器自動收集類中的全部類變量的賦值動做和靜態語句塊(static{}塊)中的語句合併產生的。

6 題目分析

上面很詳細的介紹了類的加載時機和類的加載過程,經過上面的理論來分析本文開門見上的題目

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

class SingleTon {

    private static SingleTon singleTon = new SingleTon();

    public static int count1;

    public static int count2 = 0;

 

    private SingleTon() {

        count1++;

        count2++;

    }

 

    public static SingleTon getInstance() {

        return singleTon;

    }

}

 

public class Test {

    public static void main(String[] args) {

        SingleTon singleTon = SingleTon.getInstance();

        System.out.println("count1=" + singleTon.count1);

        System.out.println("count2=" + singleTon.count2);

    }

}

分析:

1:SingleTon singleTon = SingleTon.getInstance();調用了類的SingleTon調用了類的靜態方法,觸發類的初始化 2:類加載的時候在準備過程當中爲類的靜態變量分配內存並初始化默認值 singleton=null count1=0,count2=0 3:類初始化化,爲類的靜態變量賦值和執行靜態代碼快。singleton賦值爲new SingleTon()調用類的構造方法 4:調用類的構造方法後count=1;count2=1 5:繼續爲count1與count2賦值,此時count1沒有賦值操做,全部count1爲1,可是count2執行賦值操做就變爲0

相關文章
相關標籤/搜索