1. 程式人生 > >Java類載入過程和物件例項化詳解

Java類載入過程和物件例項化詳解

Java虛擬機器類載入過程

  • 類載入時機
  • 類載入過程
    –載入
    –驗證
    –準備
    –解析
    –初始化

1、類載入時機

        類從被載入虛擬機器記憶體中開始,到卸載出記憶體為止,他的整個生命週期包括:載入、驗證、準備、解析、初始化、使用和解除安裝。其中驗證、準備、解析3個階段統稱為連線。

類的生命週期

        對於初始化階段,虛擬機器規範則嚴格規定了“**有且只有**”5種情況必須對類進行初始化(而載入、驗證、準備自然需要在此之前開始):

  1. 使用new關鍵字例項化物件的時候、讀取或設定一個類的靜態欄位(被final修飾、已在編譯期把結果放入常量池的靜態欄位除外)的時候,以及呼叫一個類的靜態方法的時候。
  2. 使用Java.lang.reflect包的方法對類進行反射呼叫的時候,如果類沒有進行過初始化,則需要先觸發其初始化
  3. 當初始化一個類的時候,如果發現其父類還沒有進行初始化,則需要先觸發其父類的初始化
  4. 當虛擬機器啟動時,使用者需要制定一個主類,虛擬機器會先初始化這個主類
  5. 當使用JDK1.7的動態語言支援時,如果一個java.lang.invoke.MethodHandle例項最後的解析結果REF_getStatic,REF_putStatic 、REF_invokeStatic的方法的控制代碼,並且這個方法控制代碼所對應的類沒有進行過初始化,則需先觸發其初始化。

這5中情況稱為對一個類的主動引用,除此之外,所有引用累的方式都不會觸發其初始化,稱為被動引用。

  1. 通過子類引用父類的靜態欄位、不會導致子類初始化
  2. 通過陣列定義來引用類,不會觸發此類的初始化
  3. 常亮在編譯階段會存入呼叫類的常量池中,本質上並沒有直接引用到定義常量的類,因此不會觸發定義常量的類的初始化
注意:介面與類在初始化時有很大區別,當一個類在初始化時,要求其父類全部都已經初始化過了,但是一個介面在初始化時,並不要求其父介面全部都完成了初始化,只有在正真使用到父介面的時候(如引用介面中定義的常量)才會初始化。

2、類的載入過程

一、載入

        “載入”是“類載入”的一個階段,在載入階段虛擬機器要完成如下三件事:

  1. 通過一個類的全限定名來獲取定義此類的二進位制位元組流
  2. 將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構
  3. 在記憶體中生成一個代表這個類的java.lang.Class物件,作為方法區這個類的各種資料的訪問入口

        非陣列類的載入可以使用系統提供的引導類載入器來完成,也可以由使用者自定義的類載入器來完成,開發人員可以通過定義自己的類載入器去控制位元組流的獲取方式(即重寫一個類載入器的loadClass方法)

        對於陣列類而言,陣列類本身不通過類載入器建立愛你,他是有Java虛擬機器直接建立的。但是陣列類與類載入器仍然有很密切的關係,因為陣列類的元素型別最終要靠類載入器去建立,一個數組類的建立愛過程遵循如下規則:

  1. 如果陣列的元件型別(指該陣列去掉一個維度的型別)是引用型別(包括類、介面、陣列、列舉、標註),那就遞迴使用類載入過程去載入這個元件型別,陣列將在載入該元件型別的類載入器的類名稱空間上被標識
  2. 如果陣列的元件型別不是引用型別(例如int[]),Java虛擬機器會把陣列標記為與引導類載入器關聯
  3. 陣列類的可見性與他的元件型別的可見性一致,如果元件型別不是引用型別,那陣列的可見性將預設為public
載入階段完成後,虛擬機器外部的二進位制位元組流就按照虛擬機器所需的格式儲存在方法區之中,方法去中的資料儲存格式由虛擬機器實現,然後在記憶體中例項化一個java.lang.Class累的物件(並沒有明確規定實在Java堆中,**對於HotSpot虛擬機器而言,Class物件比較特殊,他雖然是物件,但是儲存在方法區裡**),這個物件將作為程式訪問方法去中的這些型別資料的外部介面。

二、驗證

  • 檔案格式驗證
  • 元資料驗證
  • 位元組碼驗證
  • 符號引用驗證

三、準備

        準備階段是正事為類變數分配並設定類變數初始值的階段,這些變數所使用的記憶體都將在方法區中進行分配。這個時候進行記憶體分分配的僅包括類變數(被static修飾的變數),而不包括例項變數,例項變數將會在物件例項化時隨著物件一起分配在Java堆中。

         通常情況下是資料型別的零值,String型別的null值,Boolean的false等,如:
public static int value = 123;
那變數value在準備階段過後的初始值為0而不是123。
但是如果類的欄位被final修飾,那麼該value就會被初始化為被指定的值,如: public static final int value = 123;
編譯時javac將會為value生成ConstantValue屬性,在準備階段虛擬機器就會將其值設定為123.

四、解析

         解析階段是虛擬機器將常量池內的符號引用替換為直接引用的過程,符號引用是以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義的定位到目標即可;直接引用可以使直接指向目標的指標,相對偏移量或是一個能間接定位到目標的控制代碼。解析主要針對類或介面、欄位、類方法、介面方法、方法型別、方法控制代碼和呼叫點限定符7類符號引用進行。

  • 類或介面的解析
  • 欄位解析
  • 類方法解析
  • 介面方法解析

五、初始化

         在準備階段變數已經付過一次系統要求的初始值了,而在初始化階段,則根據程式設計師制定的主觀計劃去初始化類變數和其他資源,或者可以從另一個角度來表達:初始化階段是執行類構造器(clinit)的過程。

  • clinit方法是有編譯器自動收集類中的所有類變數的複製動作和靜態語句塊(static{})中的語句結合產生的,編譯器手機的順序是有語句在原檔案中出現的順序所決定的,靜態語句塊中只能訪問到定義在靜態語句塊之前的變數,定義在他之後的變數,在前面的靜態程式碼塊可以賦值,但是不能訪問
public class Test{
    staic{
        i = 0; //給變數賦值可以正常編譯通過
        System.out.println(i); //這局編譯器會提示“非法向前引用”
    }
    static int i = 1;
}
  • clinit方法與類的建構函式(或者說示例建構函式init)不同,他不需要顯示的呼叫父類構造器,虛擬機器會保證在子類的clinit方法執行之前,父類的clinit方法已經執行完畢。因此在虛擬機器中第一個被執行的clinit方法的類肯定是java.lang.Object
  • 由於父類的clinit方法先執行,也就意味著父類重定義的靜態程式碼塊要優於子類的變數賦值操作
  • static class Parent{
        public static int A = 1;
        static{
            A = 2;
        } 
    }
    static class Sub extends Parent{
        public static int B = A;
    }
    public staic void main(){
        System.out.println(Sub.B);
    }
    
    欄位B的值將會是2而不是1
  • clinit方法對於類或介面來說並不是必需的,如果一個類中沒有靜態程式碼塊,也就沒有對變數的複製操作,那麼編譯器可以不為這個類生成clinit方法
  • 介面中不能使用靜態程式碼塊,但仍然有變數初始化的複製操作,因此介面與類一樣都會生成clinit方法。只有介面與類不同的是,執行介面clinit方法不需要先執行父介面的clinit方法。只有當父類介面中定義的變數使用的時候,父接口才會初始化,另外,介面的實現類在初始化時也一樣不會執行介面的clinit方法
  • 虛擬機器會保證一個累的clinit方法在多執行緒環境中被正確加加鎖、同步,如果多個執行緒同時去初始化一個類,那麼只會有一個執行緒去執行這類的clinit方法其他執行緒都會阻塞等待,直到活動執行緒執行clinit完畢。
  • 具有父類的物件例項化過程

             說了這麼多來點實際的。。。。。。

    public class Fu {
        private static int age = 100;
        private String sex;
        private int fuid = 1;
        //靜態程式碼塊
        static{
            System.out.println("Fu static code block run, age:"+age);
        }
        //程式碼塊
        {
            System.out.println("Fu:id:"+fuid);
            System.out.println("Fu code block run");
        }
        public Fu(){
            System.out.println("Fu constructor run");
            func();
        }
        public Fu(String sex){
            this.sex = sex;
        }
        public void func(){
            System.out.println("Fu func run,sex:"+this.sex+"  fuid:"+fuid);
        }
    }
    
    public class Zi extends Fu {
        private String sex = "man";
        private int ziid = 2;
        //靜態程式碼塊
        static{
            System.out.println("Zi static code block run");
        }
        //程式碼塊
        {
            System.out.println("Zi:id:"+ziid);
            System.out.println("Zi code block run");
        }
        public Zi(){
            System.out.println("Zi constructor run");
            func();
        }
        public Zi(String sex) {
            super();
            this.sex = sex;
        }
        public void func(){
            System.out.println("Zi func run,sex:"+this.sex+"  Ziid:"+ziid);
        }
    }
    
    public class Test{
        public static void main(String[] args) {
            Fu obj = null;
            System.out.println("+++++++++++++++++");
            obj = new Zi();
        }
    }

    輸出結果

             這裡在new Zi()物件的時候,虛擬機器會發現方法區中並沒有該類資訊,就會先去載入該類,在載入的時候發現該類有父類,則會先載入該類的父類,然後進行父類類變數的初始化(包括準備階段的預設初始化和初始化階段的顯示初始化),這裡會先輸出父類靜態程式碼塊的資訊Fu static code block run, age:100,然後進行子類的初始化,輸出Zi static code block run,類變數初始化完畢。接著進行物件的建立,進入子類建構函式,在子類建構函式第一行會遞迴呼叫父類建構函式,進入父類建構函式之前會先進行父類例項變數初始化,即給非靜態成員變數初始化,並執行程式碼塊,輸出Fu:id:1和Fu code block run,然後進入父類建構函式,輸出Fu constructor run,當執行func方法時,發現子類對其進行了覆蓋,那麼執行子類的func方法Zi func run,sex:null Ziid:0,由於子類還沒有進行例項變數的顯示初始化,只能輸出null和0 ,父類建構函式執行結束返回到子類建構函式,此時先會進行子類例項變數的顯示初始化,運行了子類程式碼塊,輸出Zi:id:2 Zi code block run,接著執行子類建構函式,輸出Zi constructor run,接著執行func,這時子類例項變數都進行了顯示初始化,輸出Zi func run,sex:man Ziid:0