類的生命周期
- 類加載過程
- 加載
- 驗證
- 准備
- 解析
- 初始化
- 卸載
類的生命周期
一個類的完整生命周期如下:
類加載過程
Class 文件需要加載到虛擬機中之后才能運行和使用,那么虛擬機是如何加載這些 Class 文件呢?
系統加載 Class 類型的文件主要三步:加載->連接->初始化。連接過程又可分為三步:驗證->准備->解析。
加載
類加載過程的第一步,主要完成下面 3 件事情:
- 通過全類名獲取定義此類的二進制字節流
- 將字節流所代表的靜態存儲結構轉換為方法區的運行時數據結構
- 在內存中生成一個代表該類的
Class
對象,作為方法區這些數據的訪問入口
虛擬機規范上面這 3 點並不具體,因此是非常靈活的。
比如:"通過全類名獲取定義此類的二進制字節流" 並沒有指明具體從哪里獲取、怎樣獲取。比如:比較常見的就是從 ZIP
包中讀取(日后出現的 JAR
、EAR
、WAR
格式的基礎)、其他文件生成(典型應用就是 JSP
)等等。
一個非數組類的加載階段(加載階段獲取類的二進制字節流的動作)是可控性最強的階段,這一步我們可以去完成還可以自定義類加載器去控制字節流的獲取方式(重寫一個類加載器的 loadClass()
方法)。
數組類型不通過類加載器創建,它由 Java 虛擬機直接創建。
類加載器、雙親委派模型也是非常重要的知識點,這部分內容會在后面的文章中單獨介紹到。
加載階段和連接階段的部分內容是交叉進行的,加載階段尚未結束,連接階段可能就已經開始了。
驗證
准備
准備階段是正式為類變量分配內存並設置類變量初始值的階段,這些內存都將在方法區中分配。對於該階段有以下幾點需要注意:
- 這時候進行內存分配的僅包括類變量( Class Variables ,即靜態變量,被
static
關鍵字修飾的變量,只與類相關,因此被稱為類變量),而不包括實例變量。實例變量會在對象實例化時隨着對象一塊分配在 Java 堆中。 - 從概念上講,類變量所使用的內存都應當在 方法區 中進行分配。不過有一點需要注意的是:JDK 7 之前,HotSpot 使用永久代來實現方法區的時候,實現是完全符合這種邏輯概念的。 而在 JDK 7 及之后,HotSpot 已經把原本放在永久代的字符串常量池、靜態變量等移動到堆中,這個時候類變量則會隨着 Class 對象一起存放在 Java 堆中。相關閱讀:《深入理解Java虛擬機(第3版)》勘誤#75
- 這里所設置的初始值"通常情況"下是數據類型默認的零值(如 0、0L、null、false 等),比如我們定義了
public static int value=111
,那么 value 變量在准備階段的初始值就是 0 而不是 111(初始化階段才會賦值)。特殊情況:比如給 value 變量加上了 final 關鍵字public static final int value=111
,那么准備階段 value 的值就被賦值為 111。
基本數據類型的零值 : (圖片來自《深入理解 Java 虛擬機》第 3 版 7.33 )
解析
解析階段是虛擬機將常量池內的符號引用替換為直接引用的過程。
解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用限定符 7 類符號引用進行。
符號引用就是一組符號來描述目標,可以是任何字面量。直接引用就是直接指向目標的指針、相對偏移量或一個間接定位到目標的句柄。
在程序實際運行時,只有符號引用是不夠的,舉個例子:在程序執行方法時,系統需要明確知道這個方法所在的位置。
Java 虛擬機為每個類都准備了一張方法表來存放類中所有的方法。當需要調用一個類的方法的時候,只要知道這個方法在方法表中的偏移量就可以直接調用該方法了。
通過解析操作符號引用就可以直接轉變為目標方法在類中方法表的位置,從而使得方法可以被調用。
綜上,解析階段是虛擬機將常量池內的符號引用替換為直接引用的過程,也就是得到類或者字段、方法在內存中的指針或者偏移量。
初始化
初始化階段是執行初始化方法 <clinit> ()
方法的過程,是類加載的最后一步,這一步 JVM 才開始真正執行類中定義的 Java 程序代碼(字節碼)。
說明:
<clinit> ()
方法是編譯之后自動生成的。
對於<clinit> ()
方法的調用,虛擬機會自己確保其在多線程環境中的安全性。因為 <clinit> ()
方法是帶鎖線程安全,所以在多線程環境下進行類初始化的話可能會引起多個進程阻塞,並且這種阻塞很難被發現。
對於初始化階段,虛擬機嚴格規范了有且只有 5 種情況下,必須對類進行初始化(只有主動去使用類才會初始化類):
- 當遇到
new
、getstatic
、putstatic
或invokestatic
這 4 條直接碼指令時,比如new
一個類,讀取一個靜態字段(未被 final 修飾)、或調用一個類的靜態方法時。- 當 jvm 執行
new
指令時會初始化類。即當程序創建一個類的實例對象。 - 當 jvm 執行
getstatic
指令時會初始化類。即程序訪問類的靜態變量(不是靜態常量,常量會被加載到運行時常量池)。 - 當 jvm 執行
putstatic
指令時會初始化類。即程序給類的靜態變量賦值。 - 當 jvm 執行
invokestatic
指令時會初始化類。即程序調用類的靜態方法。
- 當 jvm 執行
- 使用
java.lang.reflect
包的方法對類進行反射調用時如Class.forname("...")
,newInstance()
等等。如果類沒初始化,需要觸發其初始化。 - 初始化一個類,如果其父類還未初始化,則先觸發該父類的初始化。
- 當虛擬機啟動時,用戶需要定義一個要執行的主類 (包含
main
方法的那個類),虛擬機會先初始化這個類。 MethodHandle
和VarHandle
可以看作是輕量級的反射調用機制,而要想使用這 2 個調用, 就必須先使用findStaticVarHandle
來初始化要調用的類。- 「補充,來自issue745」 當一個接口中定義了 JDK8 新加入的默認方法(被 default 關鍵字修飾的接口方法)時,如果有這個接口的實現類發生了初始化,那該接口要在其之前被初始化。
卸載
卸載類即該類的 Class 對象被 GC。
卸載類需要滿足 3 個要求:
- 該類的所有的實例對象都已被 GC,也就是說堆不存在該類的實例對象。
- 該類沒有在其他任何地方被引用
- 該類的類加載器的實例已被 GC
所以,在 JVM 生命周期內,由 jvm 自帶的類加載器加載的類是不會被卸載的。但是由我們自定義的類加載器加載的類是可能被卸載的。
只要想通一點就好了,jdk 自帶的 BootstrapClassLoader
, ExtClassLoader
, AppClassLoader
負責加載 jdk 提供的類,所以它們(類加載器的實例)肯定不會被回收。
而我們自定義的類加載器的實例是可以被回收的,所以使用我們自定義加載器加載的類是可以被卸載掉的。