Class對象在執行引擎中的初始化過程

Class對象在執行引擎中的初始化過程

一個class文件被加載到內存中需要經過三大步:裝載,鏈接,初始化。其中鏈接又可以細分爲:驗證,準備,解析三小步。用一張圖來描述class文件加載到內存的步驟如下:

在這裏插入圖片描述

1. 裝載

什麼是裝載

裝載是指Java虛擬機查找.class文件並生成字節流,然後根據字節流創建java.lang.Class對象的過程。

這一過程主要完成以下三件事:

  1. ClassLoader通過一個類的全限定名(包名+類名)來查找.class文件,並生成二進制字節流:其中class字節碼文件的來源不一定是.class文件,也可以是jar包,zip包,甚至是來源於網絡的字節流。
  2. 把.class文件的各個部分分別解析(parse)爲JVM內部特定的數據結構,並存儲在方法區。在這裏JVM會將這些.class文件的結構轉化爲JVM內部的運行時數據結構。
  3. 在內存中創建一個java.lang.Class類型的對象:接下來程序在運行過程中所有對該類的訪問都通過這個類對象,也就是這個Class類型的類對象是提供給外界訪問該類的接口。

加載時機

一個項目經過編譯之後,往往會生成大量的.class文件。當程序運行時,JVM並不會一次性的將這些.class文件全部加載進內存。對此,Java虛擬機並沒有嚴格規定何時加載.class文件,不同的虛擬機有不同的實現。不過以下兩種情況一般會對class進行裝載操作。

  • 隱式裝載:在程序運行過程中,當碰到通過new等方式生成對象時,系統會隱式調用ClassLoader去裝載對應的class到內存。
  • 顯示裝載:在編寫源代碼時,主動調用Class.forName()等方法也會進行class裝載操作,這種方式通常稱爲顯示裝載。

2.鏈接

鏈接過程分爲3步:驗證,準備,解析

驗證

驗證是鏈接的第一步,目的是爲了確保.class文件的字節流包含的信息符合當前虛擬機的要求,並且不會危及虛擬機本身的安全,主要包含以下幾個方面的檢驗:

  1. 文件格式檢驗:檢驗字節流是否符合class文件格式的規範,並且能被當前版本的虛擬機處理。
  2. 元數據檢驗:對字節碼描述的信息進行語義分析,以保證其描述的內容符合Java語言規範的要求。
  3. 字節碼檢驗:通過數據流和控制流分析,確定程序語義是合法,符合邏輯的。、
  4. 符號引用檢驗:符號引用檢驗可以看作是對類自身以外(常量池中的各種符號引用)的信息進行匹配性校驗。

實例分析

我們使用以下Foo.java來演示驗證階段的幾種情況:

在這裏插入圖片描述

使用javac編譯Foo.java生成Foo.class字節碼文件,然後使用16進制編輯器打開Foo.class文件,部分如下:

在這裏插入圖片描述

正常情況下,使用java Foo執行效果如下:

在這裏插入圖片描述

如果使用16進制編輯器修改class文件中的魔數,如下所示:

在這裏插入圖片描述

將“cafe babe”修改爲“cafe babb”,重新運行則會報如下錯誤:

在這裏插入圖片描述

class文件中魔數後的“0034”爲版本號,如果將其修改爲“0035”則會報如下錯誤:
在這裏插入圖片描述

版本號“0034”之後的“0036”是常量池計數器,表示常量池中有54個常量。如果將這個值進行修改也有可能造成運行時錯誤,比如將“0036”改爲“0032”

在這裏插入圖片描述

重新執行java Foo,則會報如下錯誤:
在這裏插入圖片描述

雖然說JVM會檢查各種對class字節碼文件的篡改行爲,但是依然無法百分之百保證class文件的安全性。比如繼續使用Foo.java舉例,在Foo.java中的print方法中,分別打印出父類和自身類的hashCode值,分別是:2018699554和111.我們可以在class字節碼的基礎上進行篡改,將父類的hashCode也返回111.

通過javap -v Foo命令可以查看Foo.class中常量池的具體信息:

在這裏插入圖片描述

圖中1處指向了父類Object的hashCode方法,圖中2處指向了Foo的hashCode方法。CONSTANT_Methodref_info的結構如下:

CONSTANT_Methodref_info{
    u1 tag = 10;
    u2 class_index;		//指向此方法的所屬類
    u2 name_type_index;	//指向此方法的名稱和類型
}

其中class_index就是指向方法的所屬類,因此只需要使用16進制編輯器將指向Object的class_index改爲執行Foo的class_index即可。具體修改如下:

在這裏插入圖片描述

按照圖中修改並保存,重新運行java Foo效果如下:

在這裏插入圖片描述

可以看出,雖然在Java源文件中調用的是super.hashCode()方法,但是經過篡改之後,Foo.class文件成功通過JVM的校驗,併成功執行最終打印出我們想要的結果。

準備

準備是鏈接的第二步,這一階段主要目的是爲類中的靜態變量分配內存,併爲其設置“0值”。比如:

public static int value = 100;

在準備階段,JVM會爲value分配內存,並將其設置爲0。而真正的值100是在初始化階段設置。並且此階段進行內存分配的僅包括類變量,而不包括實例變量(實例變量將會在對象實例化時隨着對象一起分配在Java堆中)。

有一種情況比較特殊——靜態常量,比如:

public static final int value = 100;

以上代碼會在準備階段就爲value分配內存,並設置爲100.

Java中基本類型的默認“0值”如下:

  • 基本類型(int,long,short,char,byte,boolean,float,double)的默認值爲0;
  • 引用類型默認值爲null

解析

解析時鏈接的最後一步,這一階段的任務是把常量池中的符號引用轉換爲直接引用,也就是具體的內存地址。在這一階段,JVM會將常量池中的類,接口名,字段名,方法名等轉換爲具體的內存地址。

比如上面Foo.java中編譯之後的main方法的字節碼如下:

在這裏插入圖片描述

在main方法中通過invokevirtual指令調用了print方法,“Foo.print:()V"就是一個符號引用,當main方法執行到此處時,會將符號引用”Foo.print:()V“解析成直接引用,可以將直接引用理解爲方法的真正內存地址。

3.初始化

這是class加載的最後一步,這一階段是執行類構造器方法的過程,並真正初始化類變量。比如:

public static int value = 100;

在準備階段value被分配內存並設置爲0,在初始化階段value的值會被設置爲100.

初始化時機

對於裝載階段,JVM並沒有規範何時具體執行。但是對於初始化,JVM規範中嚴格規定了class初始化的時機,主要有以下幾種情況會觸發class的初始化:

  1. 虛擬機啓動時,初始化包含main方法的主類
  2. 遇到new指令創建對象實例時,如果目標對象類沒有被初始化則進行初始化操作
  3. 當遇到訪問靜態方法或者靜態字段的指令時,如果目標對象類沒有被初始化則進行初始化操作
  4. 子類的初始化過程如果發現父類還沒有進行初始化,則需要先觸發父類的初始化
  5. 使用反射API進行反射調用時,如果類沒有被初始化則進行初始化操作
  6. 第一次調用java.lang.invoke.MethodHandle實例時,需要初始化MethodHandle指向方法所在的類

初始化類變量

在初始化階段只會初始化與類相關的靜態賦值語句和靜態語句,也就是有static關鍵字修飾的信息,而沒有static修飾的語句塊在實例化對象的時候纔會執行。

比如執行以下代碼:

在這裏插入圖片描述

然後在ClassInitTest.java中訪問ClassInit的value值,如下:

在這裏插入圖片描述

執行上述代碼,日誌如下:

在這裏插入圖片描述

可以看出,非靜態代碼塊並沒有被執行。如果將ClassInitTest.java修改如下:

在這裏插入圖片描述

加了一行代碼,使用new創建ClassInit對象實例。再次執行後非靜態代碼塊也將會被執行,如下:
在這裏插入圖片描述

被動引用

上述的六種情況在JVM中被稱爲主動引用,除此六種情況之外所有引用類的方式都被稱爲被動引用。被動引用並不會觸發class的初始化。

最典型的就是在子類中調用父類的靜態變量,比如有以下兩個類:
在這裏插入圖片描述

可以看出Child繼承自Parent類,如果直接使用Child來訪問Parent中的value值,則不會初始化Child類,比如如下的代碼:

在這裏插入圖片描述

打印日誌如下:

在這裏插入圖片描述

可以看出,Child中的靜態代碼並沒有被執行。也就是說JVM並沒有對Child執行初始化操作。

對於靜態字段,只有直接定義這個字段的類纔會被初始化,因此通過子類Child來引用Parent中定義的靜態字段,只會觸發父類Parent的初始化,而不會觸發Child的初始化。至於是否要觸發子類的加載和驗證,在虛擬機中並未明確規定,可以通過XX:+TraceClassLoading參數來查看,比如使用如下命令再次執行NonInitTest

java -XX:+TraceClassLoading NonInitTest
在這裏插入圖片描述

可以看出雖然只有Parent被初始化,但是Parent和Child都經過了裝載和驗證階段,並被加載到內存中。

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