根據《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{}");
}
}
增加參數:
運行:
可以發現 Parent
和 Child
類都被加載了,但是 Child
類並未被初始化。
觸發類初始化的行爲
《Java 虛擬機規範》和《深入理解 Java 虛擬機》都對此有了描述,這裏引用《深入理解 Java 虛擬機》中的內容:
遇到
new
、getstatic
、putstatic
或invokeStatic
這 4 條字節碼指令時,如果類型沒有進行過初始化,則需要先觸發其初始化階段。能夠生成這四條指令的典型 Java 代碼場景有:
使用
new
關鍵字實例化對象的時候。讀取或設置一個類型的靜態字段(被 final 修飾、已在編譯期把結果放入常量池的靜態字段除外)的時候。(補充:分別會觸發
getstatic
和putstatic
)。調用類的靜態方法的時候。(補充:會觸發
invokeStatic
)。使用
java.lang.reflect
包中的方法對類型進行反射調用的時候,如果類型沒有進行過初始化,則需要先觸發其初始化階段。初始化一個類的時候,當其父類沒有初始化,則需要先觸發其父類的初始化。
當虛擬機啓動時,用戶需指定一個要執行的主類(含有
main()
方法的類),虛擬機會先初始化這個主類。當使用 JDK 7 新加入的動態語言支持時,如果一個
java.lang.invoke.MethodHandle
實例最後的解析結果爲REF_getStatic
、REF_putStatic
、REF_invokeStatic
、REF_newInvokeSpecial
四種類型的方法句柄,並且該句柄所對應的類沒有進行過初始化,則需要先觸發其初始化。當一個接口中定義了 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 虛擬機》
歡迎關注公衆號