天天看點

JVM--詳解建立對象與類加載的差別與聯系JVM--詳解建立對象與類加載的差別與聯系

JVM--詳解建立對象與類加載的差別與聯系

轉載:https://blog.csdn.net/championhengyi/article/details/78778575

在前幾篇部落格中,我們探究了.class檔案的本質,類的加載機制,JVM運作時的棧幀結構以及位元組碼執行時對應操作數棧以及局部變量表的變化。

如果你已經掌握了這些東西,你現在應該會有一種感覺,給你一個Java代碼,你可以從JVM的層面上将這個類從javac編譯成.class檔案開始,到使用java指令運作這個Class檔案,然後這個類的運作過程是怎麼樣的,你可以解釋清楚。

但是等等,好像少了點什麼?我們好像沒有談及JVM中對象的建立?也就是說,在Java代碼中,你new一個對象,這時候都發生哪些事情,這就是今天我所要說的。

對象建立的時機

我們先不說對象建立的具體過程是啥,我們先來談一談什麼時候JVM會建立對象。

以下5種方式,會使JVM幫助你建立一個對象:

使用new關鍵字建立對象

使用Class類的newInstance方法(反射機制)

newInstance方法隻能調用無參的構造器建立對象。

Student student2 = (Student)Class.forName("Student類全限定名").newInstance(); 
// 或者
Student stu = Student.class.newInstance();
           

使用Constructor類的newInstance方法(反射機制)

java.lang.relect.Constructor類裡也有一個newInstance方法可以建立對象,該方法和Class類中的newInstance方法很像,但是相比之下,Constructor類的newInstance方法更加強大些,我們可以通過這個newInstance方法調用有參數的和私有的構造函數。

public class Student {
    private int id;

    public Student(Integer id) {
        this.id = id;
    }

    public static void main(String[] args) throws Exception {

        // 首先得到要執行個體化類的構造器(有參)
        Constructor<Student> constructor = Student.class
                .getConstructor(Integer.class);
        Student stu3 = constructor.newInstance();
    }
}
           

事實上Class的newInstance方法内部調用的也是Constructor的newInstance方法。

使用Clone方法建立對象

無論何時我們調用一個對象的clone方法,JVM都會幫我們建立一個新的、一樣的對象,特别需要說明的是,用clone方法建立對象的過程中并不會調用任何構造函數。

public class Student implements Cloneable{
    private int id;

    public Student(Integer id) {
        this.id = id;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        // TODO Auto-generated method stub
        return super.clone();
    }

    public static void main(String[] args) throws Exception {

        Constructor<Student> constructor = Student.class
                .getConstructor(Integer.class);
        Student stu3 = constructor.newInstance();
        Student stu4 = (Student) stu3.clone();
    }
}
           

使用(反)序列化機制建立對象

當我們反序列化一個對象時,JVM會給我們建立一個單獨的對象,在此過程中,JVM并不會調用任何構造函數。為了反序列化一個對象,我們需要讓我們的類實作Serializable接口。

public class Student implements Cloneable, Serializable {
    private int id;

    public Student(Integer id) {
        this.id = id;
    }

    @Override
    public String toString() {
        return "Student [id=" + id + "]";
    }

    public static void main(String[] args) throws Exception {

        Constructor<Student> constructor = Student.class
                .getConstructor(Integer.class);
        Student stu3 = constructor.newInstance();

        // 寫對象
        ObjectOutputStream output = new ObjectOutputStream(
                new FileOutputStream("student.bin"));
        output.writeObject(stu3);
        output.close();

        // 讀對象
        ObjectInputStream input = new ObjectInputStream(new FileInputStream(
                "student.bin"));
        Student stu5 = (Student) input.readObject();
        System.out.println(stu5);
    }
}
           

建立對象與類加載的差別與聯系

在明白了對象何時會被建立之後,現在我們就說一說,對象的建立與類加載的差別與聯系。

當碰到上面所述5種情況的任何一種,都會觸發對象的建立。

對象建立的過程

  1. 首先是對象建立的時機,在碰到new關鍵字,使用反射機制(class的new Instance、constructor的new Instance),使用clone等,會觸發對象的建立。
  2. 在配置設定記憶體之前,JVM首先會解析是否能在運作時常量池中定位到這個類的符号引用,定位之後會判斷這個類是否已經被加載、解析、初始化。如果沒有,則先進行類的加載。
  3. 在确定對象需要建立之後,給對象開始配置設定記憶體,在配置設定記憶體的過程中,需要注意使用的是哪一種垃圾收集算法,因為垃圾收集算法的不同會導緻記憶體塊是否規整,也就影響到配置設定記憶體的方式是使用指針碰撞還是使用空閑清單。
  4. 在進行記憶體配置設定的時候,如果使用的是指針碰撞方法,還需要注意并發情況下,記憶體的配置設定是否是線程安全的。一般使用加同步塊的方式和本地線程配置設定緩沖這兩種方式解決線程安全的問題。
  5. 記憶體配置設定完畢之後就是JVM對其記憶體塊進行預設初始化,這也是對象的執行個體變量不需要顯示初始化就可以直接使用的原因。
  6. 從JVM的角度來看,一個對象就此建立完畢,但是從程式的角度來看,一個對象的建立才剛剛開始,它還沒有運作<init>(執行個體初始化方法),所有的字段都還為預設值。隻有運作了<init>之後,一個真正可用的對象才算産生出來。

具體過程如下圖:

JVM--詳解建立對象與類加載的差別與聯系JVM--詳解建立對象與類加載的差別與聯系

對象的組成

符号引用解析完畢之後,JVM會為對象在堆中配置設定記憶體,HotSpot虛拟機實作的Java對象包括三個部分:對象頭、執行個體字段和對齊填充字段(非必須)。

對象頭主要包括兩部分: 

1.用于存儲對象自身的運作時資料(哈希碼、GC分代年齡、鎖狀态标志、線程持有的鎖、偏向線程ID、偏向時間戳) 

2.類型指針,即對象指向它的類中繼資料的指針,虛拟機通過這個指針來确定這個對象是哪個類的執行個體。

JVM--詳解建立對象與類加載的差別與聯系JVM--詳解建立對象與類加載的差別與聯系

執行個體字段包括自身定義的和從父類繼承下來的(即使父類的執行個體字段被子類覆寫或者被private修飾,都照樣為其配置設定記憶體)。相信很多人在剛接觸面向對象語言時,總把繼承看成簡單的“複制”,這其實是完全錯誤的。JAVA中的繼承僅僅是類之間的一種邏輯關系(具體如何儲存記錄這種邏輯關系,則設計到Class檔案格式的知識,之前也有說過),唯有建立對象時的執行個體字段,可以簡單的看成“複制”。

為對象配置設定完堆記憶體之後,JVM會将該記憶體(除了對象頭區域)進行零值初始化,這也就解釋了為什麼Java的屬性字段無需顯示初始化就可以被使用,而方法的局部變量卻必須要顯示初始化後才可以通路。最後,JVM會調用對象的構造函數,當然,調用順序會一直上溯到Object類。

JVM--詳解建立對象與類加載的差別與聯系JVM--詳解建立對象與類加載的差別與聯系

關于對象的執行個體化過程我下面詳細說明,如圖可得它是一個遞歸的過程。

<init>方法

我們在類的加載機制一文中曾經說過<clinit>(類構造器),這個方法會在類的初始化階段發揮作用,主要是收集類變量的指派動作與靜态語句塊。

<init>有類似的作用。它也會将執行個體變量的指派動作與執行個體代碼塊進行收集。說的詳細點,如果我們對執行個體變量直接指派或者使用執行個體代碼塊指派,那麼編譯器會将其中的代碼放到類的構造函數中去,并且這些代碼會被放在對超類構造函數的調用語句之後(Java要求構造函數的第一條語句必須是超類構造函數的調用語句),構造函數本身的代碼之前。

<init>()就是指收集類中的所有執行個體變量的指派動作、執行個體代碼塊和構造函數合并産生的。

我們将類構造器和執行個體構造器的初始化過程做一個總結:父類的類構造器() -> 子類的類構造器() -> 父類成員變量的指派和執行個體代碼塊 -> 父類的構造函數 -> 子類成員變量的指派和執行個體代碼塊 -> 子類的構造函數。

對象的引用

至此,一個對象就被建立完畢,此時,一般會有一個引用指向這個對象。在Java中,存在兩種資料類型,一種就是諸如int、double等基本類型,另一種就是引用類型,比如類、接口、内部類、枚舉類、數組類型的引用等。引用的實作方式一般有兩種,如下圖。

JVM--詳解建立對象與類加載的差別與聯系JVM--詳解建立對象與類加載的差別與聯系