面試被問到「類的載入過程」,怎麼回答可以脫穎而出?

2023-02-06 06:02:15

大家好,我是哪吒。

一、做一個小測試,通過註釋,標註出下面兩個類中每個方法的執行順序,並寫出studentId的最終值。

package com.nezha.javase;

public class Person1 {

    private int personId;

    public Person1() {
        setId(100);
    }

    public void setId(int id) {
        personId = id;
    }
}
package com.nezha.javase;

public class Student1 extends Person1 {

    private int studentId = 1;

    public Student1() {
    }

    @Override
    public void setId(int id) {
        super.setId(id);
        studentId = id;
    }

    public void getStudentId() {
        System.out.println("studentId = " + studentId);
    }
}
package com.nezha.javase;

public class Test1 {
    public static void main(String[] args) {
        Student1 student = new Student1();
        System.out.println("new Student() 完畢,開始呼叫getStudentId()方法");
        student.getStudentId();
    }
}

有興趣的小夥伴試一下,相信我,用System.out.println標記一下每個函數執行的先後順序,如果你全對了,下面的不用看了,大佬。

二、類的初始化步驟:

  1. 初始化父類別中的靜態成員變數和靜態程式碼塊 ;
  2. 初始化子類中的靜態成員變數和靜態程式碼塊 ;
  3. 初始化父類別的普通成員變數和程式碼塊,再執行父類別的構造方法;
  4. 初始化子類的普通成員變數和程式碼塊,再執行子類的構造方法;

三、看看你寫對了沒?

package com.nezha.javase;

public class Person {

    private int personId;

    /**
     * 第一步,走父類別無參建構函式
     */
    public Person() {
        // 1、第一步,走父類別無參建構函式
        System.out.println("第一步,走父類別無參建構函式");
        System.out.println("");
        setId(100);
    }

    /**
     * 第三步,通過super.setId(id);走父類別發方法
     * @param id
     */
    public void setId(int id) {
        System.out.println("第三步,通過super.setId(id);走父類別發方法~~~id="+id);
        personId = id;
        System.out.println("在父類別:studentId 被賦值為 " + personId);
        System.out.println("");
    }
}
package com.nezha.javase;

public class Student extends Person {

    private int studentId = 1;

    /**
     * 在走子類無參建構函式前,會先執行子類的普通成員變數初始化
     * 第五步,走子類無參建構函式
     */
    public Student() {
        System.out.println("第五步,在走子類無參建構函式前,會先執行子類的普通成員變數初始化");
        System.out.println("第六步,走子類無參建構函式");
        System.out.println("");
    }

    /**
     * 第二步,走子類方法
     *
     * 走完super.setId(id);,第四步,再回此方法
     * @param id
     */
    @Override
    public void setId(int id) {
        System.out.println("第二步,走子類方法~~id="+id);
        // 3、第三步,走子類方法
        super.setId(id);
        studentId = id;
        System.out.println("第四步,再回此方法,在子類:studentId 被賦值為 " + studentId);
        System.out.println("");
    }

    /**
     * 第六步,走getStudentId()
     */
    public void getStudentId() {
        // 4、列印出來的值是100
        System.out.println("第七步,走getStudentId()");
        System.out.println("studentId = " + studentId);
        System.out.println("");
    }
}
package com.nezha.javase;

public class Test1 {
    public static void main(String[] args) {
        Student1 student = new Student1();
        System.out.println("new Student() 完畢,開始呼叫getStudentId()方法");
        // 列印出來的值是100
        System.out.println("#推測~~列印出來的值是100");
        student.getStudentId();
    }
}

下面通過圖解JVM的方式,分析一下。

四、類的載入過程

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

(1)驗證(Verify)

  • 目的在於確保Class檔案的位元組流中包含資訊符合當前虛擬機器器要求,保證被載入類的正確性,不會危害虛擬機器器自身安全;
  • 主要包括四種驗證:檔案格式驗證、後設資料驗證、位元組碼驗證、符號參照驗證;

(2)準備(Prepare)

  • 為類變數分配記憶體並且設定該類變數的預設初始值;
  • 這裡不包含final修飾的static,因為final在編譯的時候就會分配了,準備階段會顯示初始化;
  • 這裡不會為範例變數分配初始化,類變數會分配在方法區中,而範例變數是會隨著物件一起分配到堆中;

(3)解析

  • 將常數池內的符號參照轉換為直接參照的過程
  • 例如靜態程式碼塊、靜態變數的顯示賦值
  • 事實上,解析操作往往會伴隨著JVM在執行完初始化之後在執行
  • 符號參照就是一組符號來描述所參照的目標。符號參照的字面量形式明確定義在《Java虛擬機器器規範》的Class檔案格式中。直接參照就是指- 向目標的指標、相對偏移量或一個間接定位到目標的控制程式碼
  • 解析動作主要針對類或介面、欄位、類方法、介面方法、方法型別等。對常數池中的CONSTANT_Filedref_info、CONSTANT_Class_info、CONSTANT_Methodref_info等。
3、初始化
  • 初始化階段就是執行類構造器方法的過程;
  • 此方法不需要定義,是javac編譯器自動收集類中的所有類變數的賦值動作和靜態程式碼塊中的語句合併而來。;
  • 構造器方法中指令按語句在原始檔中出現的順序執行;
  • 類構造器方法不同於類的構造器。構造器是虛擬機器器視角下的類構造器方法;
  • 若該類具有父類別,JVM會保證子類的構造器方法執行前,父類別的類構造器方法已經執行完畢;
  • 虛擬機器器必須保證一個類的類構造器方法在多執行緒下被同步加鎖;

五、類載入器的分類

JVM類載入器包括兩種,分別為引導類載入器(Bootstrap ClassLoader)和自定義類載入器(User-Defined ClassLoader)。

所有派生於抽象類ClassLoader的類載入器劃分為自定義類載入器。

1、啟動類載入器(引導類載入器)
  1. 啟動類載入器是使用C/C++語言實現的,巢狀在JVM內部;
  2. Java的核心類庫都是使用引導類載入器載入的,比如String;
  3. 沒有父載入器;
  4. 是擴充套件類載入器和應用程式類載入器的父類別載入器 ;
  5. 出於安全考慮,Bootstrap啟動類載入器只載入包名為java、javax、sun等開頭的類 ;

2、擴充套件類載入器
  1. java語言編寫
  2. 派生於ClassLoader類
  3. 父類別載入器為啟動類載入器
  4. 從java.ext.dirs系統屬性所指定的目錄中載入類庫,或從JDK的安裝目錄jre/lib/ext子目錄(擴充套件目錄)下載入類庫。如果使用者建立的jar放在此目錄下,也會自動由擴充套件類載入器載入

3、應用程式類載入器(系統類載入器)
  1. java語言編寫
  2. 派生於ClassLoader類
  3. 父類別載入器為擴充套件類載入器
  4. 它負責載入環境變數classpath或系統屬性java.class.path指定路徑下的類庫
  5. 該類載入器是程式中預設的類載入器,一般來說,Java應用的類都是由它來完成載入的
  6. 通過ClassLoader.getSystemClassLoader()方法可以獲得該類載入器

六、類載入器子系統的作用

類載入器子系統負責從檔案系統或網路中載入class檔案,class檔案在檔案開頭有特定的檔案標識。

ClassLoader只負責class檔案的載入,至於它是否可以執行,則有執行引擎決定。

載入的類資訊存放於一塊稱為方法區的記憶體空間。除了類的資訊外,方法區中還會存放執行時常數池的資訊,可能還包括字串字面量和數位常數(這部分常數資訊是class檔案中常數池部分的記憶體對映)。

七、總結

類的初始化步驟,這看似非常基礎的話題,卻實打實的難住了很多人,還總結了更為深入JVM的類的載入過程、類載入器的分類、類載入器的作用。