類的加載機制以及類、對象初始化的詳細過程| 8月更文挑戰

類的加載機制以及類、對象初始化的詳細過程

WangScaler: 一個用心創做的做者。html

聲明:才疏學淺,若有錯誤,懇請指正。java

java類的生命週期包括加載、鏈接(驗證、準備、解析)、初始化、使用、卸載五個階段。而解析階段會在初始化以前或以後觸發。類的加載不是隨着jvm的啓動而加載,而是隨着使用動態的加載。緩存

接下來咱們首先了解下虛擬機的類加載機制。安全

虛擬機的類加載機制

咱們知道java的優點之一就是跨平臺性,爲何java能跨平臺執行呢?就由於java是運行在java虛擬機jvm上的。那麼jvm的類加載機制是怎樣的呢?咱們知道java編譯以後的文件是class文件,而虛擬機的類加載機制就是把Class文件加載到內存,進行校驗,解析和初始化的過程。markdown

加載

加載就是經過類的全限定名來獲取到class文件,將文件的二進制字節流轉化成方法區的靜態數據結構,而後在內存中生成這個類的class對象並在堆中生成一個便於用戶調用的class類型的對象。數據結構

驗證

驗證就是對文件格式、元數據、字節碼進行驗證(即語法語義的驗證)、符號引用的驗證,確保Class文件中的字節流不會危害虛擬機的安全。可參考java虛擬機符號引用驗證_深刻了解Java虛擬機---虛擬機類加載機制app

準備

給靜態變量賦初值0。jdk8以前類的元信息、常量池、靜態變量都是存儲在永久代(方法區),而jdk8以後元空間(方法區)替代了永久代只存儲類的元信息,將常量池和靜態變量轉移至堆內存中。jvm

解析

將符號引用替換成直接引用。解析階段會在初始化以前或以後觸發。ide

  • 一、假如A引用B(具體的實現類),編譯階段編譯A的時候,是沒法知道B是否被編譯的,因此編譯階段B會被符號所代替,這個符號就是B的地址。在解析的時候若是B還沒有加載,就會加載B,此時A中的符號將替換成真正的B的地址,這種稱爲靜態解析,此時的解析是在初始化以前發生。oop

  • 二、若是A引用的是B的抽象方法或者接口。那麼只有在調用A的時候才知道具體的實現類是哪個。此時的解析是發生在初始化以後的,也被成爲動態解析。

  • 三、虛擬機能夠對第一次的解析結果進行緩存,避免解析動做的重複執行。

初始化

類、對象的初始化順序:(靜態變量、靜態代碼塊)>(變量、代碼塊)>構造器。

卸載

  • java堆中不存在該類的任何實例。
  • 加載該類的ClassLoader已經被回收。
  • 該類對應的java.lang.Class對象沒有任何地方被引用,沒法在任何地方經過反射訪問該類的方法。

下面以簡短的例子來演示初始化的過程。

初始化示例代碼

package com.wangscaler.load;
​
/** * @author WangScaler * @date 2021/7/28 19:17 */public class Father {
    private int i = method();
    private static int j = staticMethod();
​
    static {
        System.out.println("一、父類靜態代碼塊");
    }
​
    Father() {
        System.out.println("二、父類構造器");
    }
​
    {
        System.out.println("三、父類代碼塊");
    }
​
    private int method() {
        System.out.println("四、父類方法");
        return 1;
    }
​
    private static int staticMethod() {
        System.out.println("五、父類靜態方法");
        return 1;
    }
}
​
複製代碼
package com.wangscaler.load;
​
/** * @author WangScaler * @date 2021/7/29 14:15 */public class Son extends Father {
    private int i = method();
    private static int j = staticMethod();
​
    static {
        System.out.println("六、子類靜態代碼塊");
    }
​
    Son() {
        System.out.println("七、子類構造器");
    }
​
    {
        System.out.println("八、子類代碼塊");
    }
​
    public int method() {
        System.out.println("九、子類方法");
        return 2;
    }
​
    private static int staticMethod() {
        System.out.println("十、子類靜態方法");
        return 2;
    }
​
    public static void main(String[] args) {
     
    }
}
​
複製代碼

類的初始化

例子如上,咱們執行上述的代碼,main方法裏面什麼都沒有,會有打印產生嗎?

執行結果以下:

五、父類靜態方法
一、父類靜態代碼塊
十、子類靜態方法
六、子類靜態代碼塊
複製代碼

爲何main方法裏面什麼都沒有也會打印呢?

由於

  • 一、main方法所在的類優先加載並初始化,固會加載初始化Son這個類。

  • 二、Son繼承自Father,因此又會優先初始化Father這個類。

  • 三、Father從上依次往下初始化靜態變量和靜態代碼塊。

    • 先給靜態變量j賦值,調用靜態方法staticMethod。
    • 執行靜態代碼塊,打印一、父類靜態代碼塊
  • 四、Father加載完以後,加載Son,依然是從上往下依次初始化靜態變量和靜態代碼塊

    • 給靜態變量j賦值,調用靜態方法staticMethod
    • 執行靜態代碼塊,打印六、子類靜態代碼塊

實例初始化

修改main方法,以下:

public static void main(String[] args) {
    Son son = new Son();
}
複製代碼

執行結果以下:

五、父類靜態方法
一、父類靜態代碼塊
十、子類靜態方法
六、子類靜態代碼塊
四、父類方法
三、父類代碼塊
二、父類構造器
九、子類方法
八、子類代碼塊
七、子類構造器
複製代碼

前四個是毋庸置疑的,那麼main方法建立對象(new)時,此時是實例初始化。JVM爲每個類的每個構造方法都建立一個()方法,用於初始化實例變量,由虛擬機自行調用。

  • 執行方法,首行是super(),因此執行父類的方法。

  • 從上至下執行非靜態變量、非靜態代碼塊

    • 初始化非靜態變量i,調用方法method
    • 初始化非靜態代碼塊,打印三、父類代碼塊
    • 最後執行構造器
  • 執行完父類,繼續執行Son

    • 初始化非靜態變量i,調用方法method
    • 初始化非靜態代碼塊,打印八、子類代碼塊
    • 最後執行構造器

多實例初始化

修改main方法

  public static void main(String[] args) {
        Son son = new Son();
        System.out.println("---------------------------wangscaler-----------------------------------");
        Son son1 =new Son();
    }
}
複製代碼

打印以下

五、父類靜態方法
一、父類靜態代碼塊
十、子類靜態方法
六、子類靜態代碼塊
四、父類方法
三、父類代碼塊
二、父類構造器
九、子類方法
八、子類代碼塊
七、子類構造器
------------------------------wangscaler--------------------------------
四、父類方法
三、父類代碼塊
二、父類構造器
九、子類方法
八、子類代碼塊
七、子類構造器
複製代碼

由上能夠看出,多個實例就有多個方法,執行過程同實例初始化,就不過多介紹。

重寫下的初始化

咱們知道final、private修飾的方法和靜態方法不能被子類重寫。因而咱們在實例初始化的代碼狀況下修改Father。

public int method() {
    System.out.println("四、父類方法");
    return 1;
}
複製代碼

固然子類的method上須要添加註解@Override,由於此時的子類變成了重寫父類的method方法。

此次的執行結果是:

五、父類靜態方法
一、父類靜態代碼塊
十、子類靜態方法
六、子類靜態代碼塊
九、子類方法
三、父類代碼塊
二、父類構造器
九、子類方法
八、子類代碼塊
七、子類構造器
複製代碼

實例初始化狀況不一樣的是,第五個打印語句,爲何呢?

由於在執行父類的方法的時候,當初始化非靜態變量i時,調用方法this.method(),而this指得是正在建立的對象Son,因此執行的是重寫以後的method方法。

初始化總結

遇到new、getstatic、putstatic、或者invokestatic 這4條字節碼指令,進行初始化。使用java.lang.reflect包的方法,對壘進行反射調用的時候,若是沒有初始化,則先觸發初始化。當使用JDK1.7的動態語言支持時,若是一個Java.lang.invoke.MethodHandle實例最後的解析結果REF_getStatic,REF_outStatic,REF_invokeStatic的方法句柄,而且這個方法句柄所對應的類沒有進行過初始化,則須要先觸發其初始化

類的初始化:

  • main方法所在類優先加載初始化

  • 子類初始化時,優先初始化父類

  • 類的初始化就是執行方法

    • 由靜態變量和靜態代碼塊組成。
    • 從上至下執行。
    • 只執行一次。

實例的初始化:

  • 類建立實例(new)初始化,執行方法

    • 的首行是super(),對應父類的方法

    • 由非靜態變量、非靜態代碼塊、構造器組成

    • 非靜態變量、非靜態代碼塊從上至下依次執行、構造器最後執行

    • 有幾個構造器就有幾個方法

  • 重寫的方法

    • 子類重寫了父類的方法,那麼在子類中調用的必定是重寫以後的代碼
    • 父類中的非靜態的方法默認調用的調用對象是this,this在構造器或者方法中,指的就是正在建立的對象。

注意:如下條件下沒有方法:

  1. 沒有初始化語句或靜態初始化語句初始化;
  2. 僅包含static、 final修飾的類變量,而且類變量初始化語句是常量表達式;

參考文檔

相關文章
相關標籤/搜索