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