類載入、物件例項化知識點一網打盡

jimuzz發表於2021-01-04

前言

之前說了類載入的過程,但是有的讀者表示還是有些知識點沒弄清楚,相關面試題也不能思考出結果,所以今天就來總結下類載入、物件例項化方面的知識點/面試題,幫助大家加深印象。

全是乾貨,一網打盡類的基礎知識!先看看下面的問題都能回答上來嗎?

  • 描述new一個物件的過程,並結合例子說明。
  • 類初始化的觸發時機。
  • 多執行緒進行類的初始化會出問題嗎?
  • 類的例項化觸發時機。
  • <clinit>()方法和<init>()方法區別。
  • 在類都沒有初始化完畢之前,能直接進行例項化相應的物件嗎?
  • 類的初始化過程與類的例項化過程的異同?
  • 一個例項變數在物件初始化的過程中會被賦值幾次?

描述new一個物件的過程

先上圖,再描述:

類載入連結

類初始化

物件例項化

Java中物件的建立過程包括 類初始化和類例項化兩個階段。
new就是建立物件的一種方式,一種時機。

當執行到new的位元組碼指令的時候,會先判斷這個類是否已經初始化,如果沒有初始化就要進行類的初始化,也就是執行類構造器<clinit>()方法。
如果已經初始化了,就直接進行類物件的例項化。

  • 類的初始化,是類的生命週期中的一個階段,會為類中各個類成員賦初始值。
  • 類的例項化,是指建立一個類的例項的過程。

但是在類的初始化之前,JVM會保證類的裝載,連結(驗證、準備、解析)四個階段都已經完成,也就是上面的第一張圖。

  • 裝載是指 Java 虛擬機器查詢.class檔案並生成位元組流,然後根據位元組流建立java.lang.Class物件的過程。
  • 連結是指驗證建立的類,並將其解析到JVM中使之能夠被 JVM 執行。

那到底類載入的時機是什麼時候呢?JVM 並沒有規範何時具體執行,不同虛擬機器的實現會有不同,常見有以下兩種情況:

  • 隱式裝載:在程式執行過程中,當碰到通過 new 等方式生成物件時,系統會隱式呼叫 ClassLoader 去裝載對應的 class 到記憶體中;
  • 顯示裝載:在編寫原始碼時,主動呼叫 Class.forName() 等方法也會進行 class 裝載操作,這種方式通常稱為顯示裝載。

所以到這裡,大的流程框架就搞清楚了:

  • JVM碰到new位元組碼的時候,會先判斷類是否已經初始化,如果沒有初始化(有可能類還沒有載入,如果是隱式裝載,此時應該還沒有類載入,就會先進行裝載、驗證、準備、解析四個階段),然後進行類初始化

  • 如果已經初始化過了,就直接開始類物件的例項化工作,這時候會呼叫類物件的<init>方法。

結合例子說明

然後說說具體的邏輯,結合一段類程式碼:

public class Run {
    public static void main(String[] args) {
        new Student();
    }
}


public class Person{
    public static int value1 = 100;
    public static final int value2 = 200;

    public int value4 = 400;

    static{
        value1 = 101;
        System.out.println("1");
    }

    {
        value1 = 102;
        System.out.println("3");
    }

    public Person(){
        value1 = 103;
        System.out.println("4");
    }
}

public class Student extends Person{
    public static int value3 = 300;

    public int value5 = 500;

    static{
        value3 = 301;
        System.out.println("2");
    }

    {
        value3 = 302;
        System.out.println("5");
    }

    public Student(){
        value3 = 303;
        System.out.println("6");
    }
}
  • 首先是類裝載,連結(驗證、準備、解析)。

  • 當執行類準備過程中,會對類中的靜態變數分配記憶體,並設定為初始值也就是“0值”。比如上述程式碼中的value1,value3,會為他們分配記憶體,並將其設定為0。但是注意,用final修飾靜態常量value2,會在這一步就設定好初始值102。

  • 初始化階段,會執行類構造器<clinit>方法,其主要工作就是初始化類中靜態的(變數,程式碼塊)。但是在當前類的<clinit>方法執行之前,會保證其父類的<clinit>方法已經執行完畢,所以一開始會執行最上面的父類Object的<clinit>方法,這個例子中會先初始化父類Person,再初始化子類Student。

  • 初始化中,靜態變數和靜態程式碼塊順序是由語句在原始檔中出現的順序所決定的,也就是誰寫在前面就先執行誰。所以這裡先執行父類中的value1=100,value1 = 101,然後執行子類中的value3 = 300,value3 = 301

  • 接著就是建立物件的過程,也就是類的例項化,當物件被類建立時,虛擬機器會分配記憶體來存放物件自己的例項變數和父類繼承過來的例項變數,同時會為這些事例變數賦予預設值(0值)。

  • 分配完記憶體後,會初始化父類的普通成員變數(value4 = 400),和執行父類的普通程式碼塊(value1=102),順序由程式碼順序決定。

  • 執行父類的建構函式(value1 = 103)

  • 父類例項化完了,就例項化子類,初始化子類的普通成員變數(value5 = 500),執行子類的普通程式碼塊(value3 = 302),順序由程式碼順序決定。

  • 執行子類的建構函式(value3 = 303)

所以上述例子列印的結果是:

123456

總結一下執行流程就是:

  1. 父類靜態變數和靜態程式碼塊;

  2. 子類靜態變數和靜態程式碼塊;

  3. 父類普通成員變數和普通程式碼塊;

  4. 父類的建構函式;

  5. 子類普通成員變數和普通程式碼塊;

  6. 子類的建構函式。

最後,大家再結合流程圖好好梳理一下:

類載入連結

類初始化

物件例項化

類初始化的觸發時機

在同一個類載入器下,一個型別只會被初始化一次,剛才說到new物件是類初始化的一個判斷時機,其實一共有六種能夠觸發類初始化的時機:

  • 虛擬機器啟動時,初始化包含 main 方法的主類;

  • 遇到 new 等指令建立物件例項時,如果目標物件類沒有被初始化則進行初始化操作;

  • 當遇到訪問靜態方法或者靜態欄位的指令時,如果目標物件類沒有被初始化則進行初始化操作;

  • 子類的初始化過程如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化;

  • 使用反射 API 進行反射呼叫時,如果類沒有進行過初始化則需要先觸發其初始化;

  • 第一次呼叫 java.lang.invoke.MethodHandle 例項時,需要初始化 MethodHandle 指向方法所在的類。

多執行緒進行類的初始化會出問題嗎

不會,<clinit>()方法是阻塞的,在多執行緒環境下,如果多個執行緒同時去初始化一個類,那麼只會有一個執行緒去執行這個類的<clinit>(),其他執行緒都會被阻塞。

類的例項化觸發時機

  • 使用new關鍵字建立物件
  • 使用Class類的newInstance方法,Constructor類的newInstance方法(反射機制)
  • 使用Clone方法建立物件
  • 使用(反)序列化機制建立物件

<clinit>()方法和<init>()方法區別。

  • <clinit>()方法發生在類初始化階段,會執行類中的靜態類變數的初始化和靜態程式碼塊中的邏輯,執行順序就是語句在原始檔中出現的順序。

  • <init>()方法發生在類例項化階段,是預設的建構函式,會執行普通成員變數的初始化和普通程式碼塊的邏輯,執行順序就是語句在原始檔中出現的順序。

在類都沒有初始化完畢之前,能直接進行例項化相應的物件嗎?

剛才都說了先初始化,再例項化,如果這個問題可以的話那不是打臉了嗎?

沒錯,要打臉了哈哈。

確實是先進行類的初始化,再進行類的例項化,但是如果我們在類的初始化階段就直接例項化物件呢?比如:

public class Run {
    public static void main(String[] args) {
        new Person2();
    }
}

public class Person2 {
    public static int value1 = 100;
    public static final int value2 = 200;

    public static Person2 p = new Person2();
    public int value4 = 400;

    static{
        value1 = 101;
        System.out.println("1");
    }

    {
        value1 = 102;
        System.out.println("2");
    }

    public Person2(){
        value1 = 103;
        System.out.println("3");
    }
}

嘿嘿,這時候該怎麼列印結果呢?

按照上面說過的邏輯,應該是先靜態變數和靜態程式碼塊,然後普通成員變數和普通程式碼塊,最後是建構函式。

但是因為靜態變數又執行了一次new Person2(),所以例項化過程被強行提前了,在初始化過程中就進行了例項化。這段程式碼的結果就變成了:

23123

所以,例項化不一定要在類初始化結束之後才開始初始化,有可能在初始化過程中就進行了例項化。

類的初始化過程與類的例項化過程的異同?

學了上面的內容,這個問題就很簡單了:

  • 類的初始化,是指在類裝載,連結之後的一個階段,會執行<clinit>()方法,初始化靜態變數,執行靜態程式碼塊等。只會執行一次。

  • 類的例項化,是指在類完全載入到記憶體中後建立物件的過程,會執行<init>()方法,初始化普通變數,呼叫普通程式碼塊。可以被呼叫多次。

一個例項變數在物件初始化的過程中最多可以被賦值幾次?

那我們就試試舉例出最多的情況,其實也就是每個要經過的地方都對例項變數進行一次賦值:

  • 1、物件被建立時候,分配記憶體會把例項變數賦予預設值,這是肯定會發生的。
  • 2、例項變數本身初始化的時候,就給他賦值一次,也就是int value1=100。
  • 3、初始化程式碼塊的時候,也賦值一次。
  • 4、建構函式中,在進行賦值一次。

一共四次,看程式碼:

public class Person3 {
    public int value1 = 100;

    {
        value1 = 102;
        System.out.println("2");
    }

    public Person3(){
        value1 = 103;
        System.out.println("3");
    }
}

參考

https://blog.csdn.net/justloveyou_/article/details/72466416
https://kaiwu.lagou.com/course/courseInfo.htm?courseId=67#/detail/pc?id=1860
https://www.jianshu.com/p/8a14ed0ed1e9

拜拜

有一起學習的小夥伴可以關注下❤️ 我的公眾號——碼上積木,每天剖析一個知識點,我們一起積累知識。公眾號回覆111可獲得面試題《思考與解答》以往期刊。

相關文章