本文主要講述虛擬機如何把 Class文件加載到內存的過程。校驗、轉換解析和初始化,最終形成可被虛擬機使用的Java類型,這就是虛擬機的類加載機制。類型的加載、連接和初始化都是在程序運行期間完成,這樣做的優劣勢,如下:
類的生命周期是指把Class字節碼從文件中加載到內存,直到卸載內存整個過程,分為7個步驟。
圖中用紅色圈起來的3個過程分別為驗證、准備、解析,它們合稱為鏈接(Linking)過程。另外圖中紫色的5項是嚴格按照執行。而藍色的解析階段不一定要在初始化之前, 也可以在初始化之後再解析,這種情況稱為動態綁定或晚期綁定。
虛擬機在加載階段,主要工作如下:
類的全限定名
獲取該類的二進制字節流;對於上述字節流,可能來源:
注:對於數組類,不通過類加載器創建,而是由虛擬機直接創建的。另外加載階段尚未完成,連接階段可能已經開始。
驗證是連接階段(Linking)的第一步,目的是為了確保Class文件的字節流符合虛擬機規范,不會危害虛擬機自身安全。比如:訪問數組越界問題,將對象轉型為未實現的類型,跳轉到不存在的代碼區等情緒編譯器都會拒絕編譯,也就是無法生成Class文件,既然如此,為什麼還要驗證呢?原因是Class文件不一定都是由java源碼編譯而成,可以是任何途徑,所以驗證還是很有必要的,盡可能保證系統能承受住惡意代碼攻擊。
驗證主要工作分4階段:
驗證點有比如是否魔數0xCAFEBABE開頭;主、次版本號是否范圍之內;常量池中常量tag標示是否正確等等,只有通過全部的驗證,才能把字節流存儲到內存的方法區。
經過文件格式驗證,字節流已加載到方法區,這個階段工作是對方法區的字節碼進行語義分析,保證符合Java語言規范。 驗證點比如:
比如操作數棧的數據類型和指令代碼序列配合,跳轉指令不會跳到方法體之外等。HotSpot虛擬機提供 -XX:-UseSplitVerifier選項來關閉這項優化。
校驗點:
對於虛擬機的類加載機制來說,驗證階段非常重要的,但不是一定必要的。如果所運行的全部代碼(包含自己編寫以及第三方包的代碼)都已經被反復使用和驗證過,那麼可以考慮使用 -Xverify:none參數來關閉大部分的類驗證措施,以縮短虛擬機類加載的時間。
主要工作:static變量
分配內存,並設置類變量的初始值的階段。
(1). 類變量:賦予零值
數據類型的零值表,如下:
類型 int long float double short byte char boolean reference 零值 0 0L 0.0f 0.0d (short)0 (byte)0 ‘\u0000’ false null例如:
public static int value = 10;
在准備階段,會為變量value
在方法區分配內存並初始化零值,即value=0
,而非10。 因為對於value的賦值10,是由putstatic
指令完成。該指令是在java程序被編譯後,存放在類構造器<clinit>
方法之中。所以 value
=10的操作是在類初始化的時候才發生,故類變量在准備期value=0
。
(2). 常量:賦予真實值
例如:
public static final int value = 10;
對於常量,准備階段會把類字段的字段屬性表中的ConstantValue屬性所指定的值(此處是10),賦給常量(value
),故常量在准備期間value =10
;
(3). 實例變量:不賦任何值
該階段僅對類變量進行內存分配,而對於實例變量(或者稱呼為成員變量)並不會分配內存,也就更不用提賦值的事。實例變量的初始化,是隨著對象實例化時在Java堆上分配內存而進行的。
主要工作:虛擬機將常量池內的符號引用替換為直接引用的階段。
先解析下符號引用和直接引用的概念
同一個符號引用 在不同的虛擬機中解析出來的直接引用地址一般都是不相同的;同一個符號引用,在同一個虛擬機下,多次解析時,會對第一次解析結果進行緩存(常量池記錄直接引用,並標記已解析狀態),從而避免多次解析。
特殊情形,對於invokedynamic指令,不會進行緩存過程,每次使用前都會進行解析。
主工作:主要是執行類構造器方法clinit。(class init的簡稱)
類初始化階段是類加載的最後一個階段。在初始化之前的過程中,用戶可控的地方只有通過自定義類加載器參與,其余都是虛擬機主導和控制。
到了初始化,才開始真正的執行類中定義的Java程序代碼。
(1). 類的構造方法
類構造方法是由編譯器自動收集源文件中的類變量賦值操作
和靜態語句塊
合並而成的。收集順序是由語句在源文件的順序所決定。故靜態語句塊只能訪問定義之前的靜態變量;對於定義之後的變量可以賦值,但不能訪問。
clinit方法不需要顯式調用父類構造器
,虛擬機會保證子類的clinit方法執行之前,父類的clinit方法已經執行完畢。故第一個被執行的clinit方法的類肯定是java.lang.Object;clinit方法不是必需的
,對於沒有靜態塊和類變量賦值操作,編譯器不會生成clinit方法。父類靜態語句和靜態變量賦值優先於子類
.interface中不能有靜態語句塊
,但仍可以有變量初始化的賦值操作,也可以生成clinit方法。但接口和類的不同是,執行接口的clinit方法不需要先執行父接口的clinit方法。只有當父接口中定義的變量使用時,父接口才會初始化。阻塞等待
,當類構造方法有耗時操作,會造成多進程的阻塞,往往比較隱蔽。(2). 類初始化時機
虛擬機規范中嚴格規定有且只有5種情況下,當類沒有初始化時必須立即對類進行初始化:
new
、getstatic
、putstatic
或invokeStatic
這4條字節碼指令時。常見場景:
new
;getstatic
;(final常量除外)putstatic
;invokeStatic
;main()
的類),虛擬機會先初始化該類;java.lang.reflect
包中的方法對類進行反射調用時;java.lang.invoke.MethodHandle
實例最後的解析結果為REF_getStatic
、REF_putStatic
、REF_invokeStatic
的方法句柄,並且該句柄所對應的類沒有進行過初始化;上面講到final常量不能觸發類初始化,是由於在編譯時已把數據放入常量池的靜態字段,當讀取類的static final
字段時,並不需要初始化類,而是從常量池中去獲取相應的數據。
上述的5種場景的行為都是對類的一個主動引用過程。除此之外,還有被動引用並不會除非類的初始化過程。 另外