第17次文章:初探JVM

  • 2019 年 10 月 8 日
  • 筆記

類載入全過程

一、為什麼研究類載入全過程?

(1)有助於了解JVM運行過程

(2)更深入了解java動態性(解熱部署,動態載入),提高程式的靈活性。

二、類載入機制

JVM把class文件載入到記憶體,並對數據進行校驗、解析和初始化,最終形成JVM可以直接使用的java類型的過程。

(1)載入:

將class文件位元組碼內容載入到記憶體中,並將這些靜態數據轉換成方法區中的運行數據結構,在堆中生成一個代表這個類的java.lang.Class對象,作為方法區類數據的訪問入口,這個過程需要類載入器的參與。這個java.lang.Class對象其實就是我們在利用反射機制時候的那個鏡像對象。

(2)鏈接:

將java類的二進位程式碼合併到JVM的運行狀態之中的過程

(3)初始化:

-初始化階段是執行類構造器<client>()方法的過程。類構造器<client>()方法是由編譯器自動收集類中的所有類變數的賦值動作和靜息態語句塊(static塊)中的語句合併產生的。

-當初始化一個類的時候,如果發現其父類還沒有進行初始化,則需要先初始化其父類

-虛擬機會保證一個類的<client>()方法在多執行緒環境中正確加鎖和同步。

三、下面我們主要對類的初始化時機進行討論

1、類載入器的初始化時機主要分為主動引用和被動引用

(1)類的主動引用(一定會發生類的初始化)

-new一個類的對象

-調用類的靜態成員(除了final常量)和靜態方法

-使用java.lang.reflect包的方法對類進行反射調用

-當初始化一個類,如果其父類沒有被初始化,則會先初始化其父類

(2)類的被動引用(不會發生類的初始化)

-當訪問一個靜態域時,只有真正聲明這個域的類才會被初始化。通過子類引用父類的靜態變數,不會導致子類初始化

-通過數組定義類引用,不會觸發此類的初始化

-引用常量不會觸發此類的初始化(常量在編譯階段就存入調用類的常量池中了)

2、為了測試類的初始化時機,我們首先創建一個父類A_Father和子類A

class A extends A_Father{    public static int width = 100;    public static final int MAX = 100;    static {      System.out.println("靜態初始化類A");      width = 300;    }    public A() {      System.out.println("構造對象A");    }  }    class A_Father{    public static int hight = 100;    static {      System.out.println("靜態初始化類A_Father");    }  }

3、然後我們在main函數中進行更改不同的調用方法,查看所得結果,測試類的初始化時機。

package com.peng.test;    /**   * 測試類載入的全過程   */  public class Demo01 {    static {      System.out.println("靜態初始化類Demo01");    }    public static void main(String[] args) throws ClassNotFoundException {      System.out.println("執行Demo01的main方法");      A a = new A();      A a2 = new A();    }    }

我們查看一下結果:

tips:

(1)我們對Demo01類的初始化順序進行分析。Demo01類的靜態static塊兒首先被執行,然後進入main方法,在new對象A的時候(主動引用)。由於A為父類A_Father的子類,並且父類沒有被初始化,所以類載入器首先初始化的是父類A_Father(主動引用),而不是直接初始化子類A。

(2)與此同時,類只會被載入一次,當載入完以後,後面的調用不會再去重新載入這個類,所以在我們重新new一個對象a2的時候,僅僅調用了子類A的構造器,而並沒有重新載入子類A和父類A_Father。

(3)在對類進行初始化的時候,編譯器其實是將靜態賦值語句,比如「public static int width = 100;」和靜態初始化塊「static{}」合併之後一起運行的。

4、下面我們再更改一種調用方法,測試其初始化時機

package com.peng.test;    public class Demo01 {    static {      System.out.println("靜態初始化類Demo01");    }    public static void main(String[] args) throws ClassNotFoundException {      System.out.println("執行Demo01的main方法");      System.out.println(A.width);    }    }

我們查看一下結果:

tips:

通過結果我們可以看出,當我們調用類的靜態成員(除了final常量)和靜態方法時,類A和A_Father被直接初始化了。這也屬於主動引用中的一種。還有一種就屬於反射調用類「Class.forName("com.peng.test.A");」此時也會出現上述結果。

5、我們現在將類的引用改為被動引用,重新查看一下結果

package com.peng.test;    public class Demo01 {    static {      System.out.println("靜態初始化類Demo01");    }    public static void main(String[] args) throws ClassNotFoundException {      System.out.println(A.MAX);      A[] as = new A[10];      System.out.println(A.hight);    }    }

結果圖:

tips:

(1)首先我們調用了A中的靜態域MAX,由於MAX被final關鍵字修飾,屬於常量,所以在編譯的時候,常量就已經被存入了調用類的常量池中。在我們進行調用常量池中數據的時候,不需要初始化類就可以進行使用。(被動引用)

(2)在我們定義一個數組的時候,雖然使用了new關鍵字,但是也屬於被動引用,所以沒有進行初始化操作。(被動引用)

(3)在此方法調用中,我們使用了A.hight靜態屬性值,但是由於這個靜態域是父類A_Father聲明的,所以在調用A.hight靜態域的時候,JVM並不會去初始化A類,而是僅僅初始化A_Father類。(被動引用)