天天看點

面試被問到“類的加載過程”,怎麼回答可以脫穎而出?

作者:哪吒程式設計

大家好,我是哪吒。

一、做一個小測試,通過注釋,标注出下面兩個類中每個方法的執行順序,并寫出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的類的加載過程、類加載器的分類、類加載器的作用。

繼續閱讀