小議 Java 類的初始化

根據《Java 虛擬機規範》:

The Java Virtual Machine dynamically loads, links and initializes classes and interfaces.

即 Java 虛擬機會動態地加載、鏈接和初始化類和接口。

類初始化的定義和 <clinit>() 方法

本文主要會討論類的初始化,那麼如何證明類已經被初始化了呢,首先要看什麼是類的初始化,根據 Java 虛擬機規範:

Initialization of a class or interface consists of executing the class or interface initialization method <clinit>().

也就是說類或者接口的初始化就是執行它的初始化方法,即 <clinit>() 方法。

關於 <clinit>() 方法,《Java 虛擬機規範》中還有一段描述:

In a class file whose version number is 51.0 or above, the method must additionally have its ACC_STATIC flag set in order to be the class or interface initialization method.

This requirement was introduced in Java SE 7. In a class file whose version number is 50.0 or below, a method named <clinit> that is void and takes no arguments is considered the class or interface initialization method regardless of the setting of its ACC_STATIC flag.

也就是說在 Java 7 之後新增了一個規定, <clinit>() 方法要作爲初始化方法必須要設置一個 ACC_STATIC 標誌,這裏可以驗證一下。

我的 JDK 版本:

➜  java git:(master) ✗ java -version
openjdk version "1.8.0_202"
OpenJDK Runtime Environment (AdoptOpenJDK)(build 1.8.0_202-b08)
OpenJDK 64-Bit Server VM (AdoptOpenJDK)(build 25.202-b08, mixed mode)

寫了一個 HelloWorld 類:

package com.dongguabai;

/**
 * @author Dongguabai
 * @Description
 * @Date 創建於 2020-06-13 10:19
 */
public class HelloWorld {

    static {
        System.out.println("init HelloWorld");
    }
}

編譯並使用 javap 查看:

➜  ~ cd /Users/dongguabai/IdeaProjects/dongguabai-jvm/src/main/java            
➜  java git:(master) ✗ javac com/dongguabai/HelloWorld.java 
➜  java git:(master) ✗ javap com.dongguabai.HelloWorld 
Compiled from "HelloWorld.java"
public class com.dongguabai.HelloWorld {
  public com.dongguabai.HelloWorld();
  static {};
}
➜  java git:(master) ✗ javap -v com.dongguabai.HelloWorld
Classfile /Users/dongguabai/IdeaProjects/dongguabai-jvm/src/main/java/com/dongguabai/HelloWorld.class
  Last modified 2020-6-13; size 423 bytes
  MD5 checksum 9bae0b1a79f9f4d4e3a93f79556ebfa5
  Compiled from "HelloWorld.java"
public class com.dongguabai.HelloWorld
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#14         // java/lang/Object."<init>":()V
   #2 = Fieldref           #15.#16        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #17            // init HelloWorld
   #4 = Methodref          #18.#19        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #20            // com/dongguabai/HelloWorld
   #6 = Class              #21            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               <clinit>
  #12 = Utf8               SourceFile
  #13 = Utf8               HelloWorld.java
  #14 = NameAndType        #7:#8          // "<init>":()V
  #15 = Class              #22            // java/lang/System
  #16 = NameAndType        #23:#24        // out:Ljava/io/PrintStream;
  #17 = Utf8               init HelloWorld
  #18 = Class              #25            // java/io/PrintStream
  #19 = NameAndType        #26:#27        // println:(Ljava/lang/String;)V
  #20 = Utf8               com/dongguabai/HelloWorld
  #21 = Utf8               java/lang/Object
  #22 = Utf8               java/lang/System
  #23 = Utf8               out
  #24 = Utf8               Ljava/io/PrintStream;
  #25 = Utf8               java/io/PrintStream
  #26 = Utf8               println
  #27 = Utf8               (Ljava/lang/String;)V
{
  public com.dongguabai.HelloWorld();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 8: 0

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String init HelloWorld
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 11: 0
        line 12: 8
}
SourceFile: "HelloWorld.java"

可以看到,增加了一個 flags: ACC_STATIC

根據《深入理解 Java 虛擬機》的描述:

<client>() 方法是由編譯器自動收集類中的所有類變量(靜態變量)的賦值動作和靜態語句塊(static{} 塊)中的語句合併產生的,編譯器收集的順序是由語句在源文件中出現的順序決定的,靜態語句塊只能訪問到定義在靜態語句塊之前的變量,定義在它之後的變量,靜態語句塊能進行賦值操作,但是不能進行訪問。

所以後文驗證一個類是否被初始化就是通過查看靜態代碼塊是否執行。

類沒有被初始化但是已經被加載

對於 HotSpot 虛擬機來說,可以通過 -XX:+TraceClassLoading 參數查看有哪些類被加載。

這裏定義了一個啓動類 HelloWorld 以及 Parent 和它的子類 Child

package com.dongguabai;

/**
 * @author Dongguabai
 * @Description
 * @Date 創建於 2020-06-13 10:52
 */
public class HelloWorld {
    static {
        System.out.println("init HelloWorld");
    }

    public static void main(String[] args) {
        System.out.println(Child.parentStr);
    }
}

class Parent{

    public static String parentStr = "parentStr";

    static {
        System.out.println("parent static{}");
    }
}


class Child extends Parent{
    static {
        System.out.println("Child static{}");
    }
}

增加參數:
在這裏插入圖片描述
運行:
在這裏插入圖片描述

可以發現 ParentChild 類都被加載了,但是 Child 類並未被初始化。

觸發類初始化的行爲

《Java 虛擬機規範》和《深入理解 Java 虛擬機》都對此有了描述,這裏引用《深入理解 Java 虛擬機》中的內容:

  1. 遇到 newgetstaticputstaticinvokeStatic 這 4 條字節碼指令時,如果類型沒有進行過初始化,則需要先觸發其初始化階段。能夠生成這四條指令的典型 Java 代碼場景有:

    • 使用 new 關鍵字實例化對象的時候。

    • 讀取或設置一個類型的靜態字段(被 final 修飾、已在編譯期把結果放入常量池的靜態字段除外)的時候。(補充:分別會觸發 getstaticputstatic )。

    • 調用類的靜態方法的時候。(補充:會觸發 invokeStatic)。

  2. 使用 java.lang.reflect 包中的方法對類型進行反射調用的時候,如果類型沒有進行過初始化,則需要先觸發其初始化階段。

  3. 初始化一個類的時候,當其父類沒有初始化,則需要先觸發其父類的初始化。

  4. 當虛擬機啓動時,用戶需指定一個要執行的主類(含有main() 方法的類),虛擬機會先初始化這個主類。

  5. 當使用 JDK 7 新加入的動態語言支持時,如果一個 java.lang.invoke.MethodHandle 實例最後的解析結果爲 REF_getStaticREF_putStaticREF_invokeStaticREF_newInvokeSpecial 四種類型的方法句柄,並且該句柄所對應的類沒有進行過初始化,則需要先觸發其初始化。

  6. 當一個接口中定義了 JDK 8 新加入的默認方法(被 default 關鍵字修飾的接口方法)時,如果有這個接口的實現類發生了初始化,那該接口要在其之前被初始化。

《深入理解 Java 虛擬機》對以上六種場景稱爲類型的主動引用,除此之外,其他的引用類型都不會觸發類的初始化,稱爲被動引用。

兩個類型被動引用的例子

類型的主動引用在上文中已經介紹了,這裏舉兩個被動引用的例子。

通過數組定義來引用類

代碼如下:

package com.dongguabai;

/**
 * @author Dongguabai
 * @Description
 * @Date 創建於 2020-06-13 14:45
 */
public class ArratTest {

    public static void main(String[] args) {
        User[] users = new User[0];
    }
}

class User{
    static {
        System.out.println("User static{}");
    }
}

可以發現此時 User 類並未被初始化。

通過子類引用父類的靜態字段

引用上面 HelloWorld 類的例子:

package com.dongguabai;

/**
 * @author Dongguabai
 * @Description
 * @Date 創建於 2020-06-13 10:52
 */
public class HelloWorld {
    static {
        System.out.println("init HelloWorld");
    }

    public static void main(String[] args) {
        System.out.println(Child.parentStr);
    }
}

class Parent{

    public static String parentStr = "parentStr";

    static {
        System.out.println("parent static{}");
    }
}


class Child extends Parent{
    static {
        System.out.println("Child static{}");
    }
}

運行:

init HelloWorld
parent static{}
parentStr

可以發現 Child 類並未初始化。

要注意的是,對於靜態字段,只有直接定義這個字段的類纔會被初始化,因此通過其子類來引用父類中定義的靜態字段,只會觸發父類的初始化而不會觸發子類的初始化。

References

  • 《The Java Virtual Machine Specification Java SE 8 Edition》
  • 《深入理解 Java 虛擬機》

歡迎關注公衆號
​​​​​​在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章