一.類加載時機
類從被加載到虛擬機內存中開始,到卸載出內存為止,它的整個生命周期包括:加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸載(Unloading)7個階段。其中驗證、準備、解析3個部分統稱為連接(Linking),這7個階段的發生順序如下圖所示:
加載、驗證、準備、初始化和卸載這5個階段的順序是確定的,類的加載過程必須按照這種順序按部就班地開始,而解析階段則不一定:它在某些情況下可以在初始化階段之后再開始,這是為了支持Java語言的運行時綁定(動態綁定)。
Java虛擬機規范沒有對什么時候開始執行加載做強制約定,但是對初始化階段嚴格規定了有且只有以下5種情況下必須對類進行初始化:
- 1.遇到:new、getstatic、putstatic、invokestatic指令時,如果類尚未初始化,那就要進行初始化。這四個指令對應的Java代碼場景是:
通過new創建對象;
讀取、設置一個類的靜態成員變量(不包括final修飾的靜態變量);
調用一個類的靜態成員函數。
- 2.使用
java.lang.reflect
進行反射調用的時候,如果類沒有初始化,那就需要初始化; - 3.當初始化一個類的時候,若其父類尚未初始化,那就先要讓其父類初始化,然后再初始化本類;
- 4.當虛擬機啟動時,虛擬機會首先初始化帶有
main方法
的類,即主類; - 5.當使用 JDK.7 的動態語言支持時,如果一個
java.lang.invoke.MethodHandle
實例最后的解析結果為 REF_getStatic, REF_putStatic, REF_invokeStatic的方法句柄,并且這個方法句柄所對應的類沒有進行過初始化,則需要先觸發其初始化。
注意:
對于靜態字段,只有直接定義這個字段的類才會被初始化。
通過數組定義引用類,不會觸發此類的初始化。
常量在編譯階段會存入調用類的常量池,因此不會觸發定義類的初始化。
//父類
public class SuperClass{
static{
System.out.println("SuperClass init!");
}
public static int value = 1;
}
//子類
public class SubClass extends SuperClass{
static{
System.out.println("SubClass init!");
}
}
//只會輸出SuperClass init!,也就是說子類沒有被初始化。
public static void main(String[] args) {
SubClass.value;
}
//不會輸出任何的內容,被動引用不會觸發此類的初始化。
public static void main(String[] args) {
SuperClass scas = new SuperClass[8];
}
public class ConstClass{
static{
System.out.println("ConstClass init!");
}
public static final int VALUE = 1;
}
//不會輸出內容,常量在編譯階段直接存入常量池中,因此不會觸發定義類的初始化。
public static void main(String[] args) {
ConstClass.VALUE;
}
對于接口而言,只有類的第3種情況有區別:一個接口在初始化時,并不要求其父接口全部都完成了初始化,只有在真正使用到父接口的時候(如引用接口中定義的常量)才會初始化。
二.類加載過程
類加載過程就是加載、驗證、準備、解析和初始化這5個階段所執行的具體動作。
1.加載
- 通過一個類的全限定名來獲取定義此類的二進制字節流。
JVM規范沒有限制從本地讀取.class文件,所以可以從jar包或者其他方式來讀取;
最終會返回ClassFileStream對象的指針。(也可以運行時計算生成,比如動態代理技術。Proxy)
- 將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構。
獲取到指針后,會創建實例,并且解析ClassFileStream結構:
1.讀取魔數并校驗 校驗jdk版本信息
2.讀取常量池引用 包括常量和符號應用,符號引用指父類,接口,字段,放法等
3.讀取訪問標識并校驗 類的類型class還是interface,訪問類型public 還是抽象的
4.獲取類的全限定名 讀取當前類的索引,并在常量池中找到當前類的全限定名,讀取常量池時,會獲得常量池句柄,會標識全限定名的地址
5.獲取父類或者接口信息,如果有繼承或者實現,則需先加載父類和接口,如果已經加載則直接獲取它們的句柄記錄到本類中,并簡單校驗類名
6.讀取字段信息和方法信息加載到本類的信息中,之后會評估類的大小,類加載完成之后,大小不會發生改變
- 在內存中生成一個代表這個類的
java.lang.Class
對象,作為方法區這個類的各種數據的訪問入口。
1.計算虛擬函數表和接口函數表的大小
2.創建instanceKlass對象(.class文件對應的所有類信息)
3.創建Java鏡像類并初始化靜態域,通知JVM加載完成,方法區創建該類的元數據
注意:對于數組而言,數組本身不通過類加載器創建,它是由Java虛擬機直接創建的。只有數組的元素類型是最終是靠類加載去創建。
加載階段完成后,虛擬機外部的二進制字節流就按照虛擬機所需的格式存儲在方法區之中,方法區中的數據存儲格式由虛擬機實現自行定義,虛擬機規范未規定此區域的具體數據結構。然后在內存中實例化一個java.lang.Class
類的對象(并沒有明確規定是在Java堆中,對于HotSpot虛擬機而言,Class對象比較特殊,它雖然是對象,但是存放在方法區里面),這個對象將作為程序訪問方法區中的這些類型數據的外部接口。
2.驗證
這一階段的目的是為了確保Class文件的字節流中包含的信息符合當前虛擬機的要求。
比如:將一個對象轉型為它并未實現的類型、跳轉到不存在的代碼行之類的事情,如果這樣做了,編譯器將拒絕編譯。但是Class文件并不一定要求用Java源碼編譯而來,可以使用任何途徑產生,甚至包括用十六進制編輯器直接編寫來產生Class文件。在字節碼語言層面上,上述Java代碼無法做到的事情都是可以實現的。虛擬機如果不檢查輸入的字節流,很可能會因為載入了有害的字節流而導致系統崩潰,所以驗證是虛擬機對自身保護的一項重要工作。
加載和驗證是交叉進行的,驗證在各個階段都是存在的,驗證二進制字節流代表的字節碼文件是否合格,主要從一下幾方面判斷:
文件格式:參看class文件格式詳解,經過文件格式驗證之后的字節流才能進入方法區分配內存來存儲。
元數據驗證:是否符合java語言規范。
字節碼驗證:數據流和控制流的分析,這一步最復雜。
符號引用驗證:符號引用轉化為直接引用時(解析階段),檢測對類自身以外的信息進行存在性、可訪問性驗證。
3.準備
準備階段是正式為類變量分配內存并設置類變量初始值的階段,這些變量所使用的內存都將在方法區中進行分配。這時候進行內存分配的僅包括類變量(被static修飾的變量),而不包括實例變量,實例變量將會在對象實例化時隨著對象一起分配在Java堆中。其次,這里所說的初始值“通常情況”下是數據類型的零值,public static int value = 8;
變量value在準備階段過后初始值為0而不是8,因為此時還為開始執行任何的Java方法。
4.解析
解析階段是虛擬機將常量池內的符號引用(存在class文件中)替換為直接引用的過程。包括類,接口,字段,類方法和接口方法的解析。
符號引用(Symbolic References):符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可。
直接引用(Direct References):直接引用可以是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄。如果有了直接引用,那么引用的目標一定是已經存在于內存中。
5.初始化
類初始化階段是類加載過程的最后一步,前面的類加載過程中,除了在加載階段用戶應用程序可以通過自定義類加載器參與之外,其余動作完全由虛擬機主導和控制。到了初始化階段,才真正開始執行類中定義的Java程序代碼(或者說是字節碼)。在準備階段,變量已經賦過一次系統要求的初始值,而在初始化階段,則根據程序員通過程序制定的主觀計劃去初始化類變量和其他資源。
初始化階段是執行類構造器<clinit>()方法的過程。
類構造器<clinit>()方法是由編譯器自動收藏類中的所有類變量的賦值動作和靜態語句塊(static塊)中的語句合并產生。
當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。
虛擬機會保證一個類的<clinit>()方法在多線程環境中被正確加鎖和同步。
三.類加載器
通過一個類的全限定名來獲取描述此類的二進制字節流,實現這個動作的代碼模塊稱為“類加載器”。
1.類與類加載器
對于任意一個類,都需要由加載它的類加載器和這個類本身一同確立其在Java虛擬機中的唯一性。每一個類加載器,都擁有一個獨立的類名稱空間。也就是說,比較兩個類是否“相等”,只有在這兩個類是由同一個類加載器加載的前提下才有意義,否則,即使這兩個類來源于同一個Class文件,被同一個虛擬機加載,只要加載它們的類加載器不同,那這兩個類就必定不相等。
類加載器分類
- 啟動類加載器
負責將存放在
<JAVA_HOME>/lib
目錄中的,或者被-Xbootclasspath
參數所指定的路徑中的,并且是虛擬機按照文件名識別的(如rt.jar,名字不符合的類庫即使放在lib目錄中也不會被加載)類庫加載到虛擬機內存中。
- 擴展類加載器
由
sun.misc.Launcher$ExtClassLoader
實現。負責加載<JAVA_HOME>/lib/ext
目錄中的,或者被java.ext.dirs
系統變量所指定的路徑中的所有類庫。開發者可以直接使用擴展類加載器。
- 應用程序類加載器
由
sun.misc.Launcher$AppClassLoader
實現。由于這個類加載器是ClassLoader.getSystemClassLoader()
方法的返回值,所以一般也稱它為系統類加載器。
它負責加載用戶類路徑ClassPath
上所指定的類庫,開發者可以直接使用這個類加載器。如果應用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認的類加載器。
- 自定義的類加載器
JVM建議用戶將應用程序類加載器作為自定義類加載器的父類加載器。
2.雙親委派機制
雙親委派模型要求除了頂層的啟動類加載器外,其余的類加載器都應當有自己的父類加載器。
“雙親”只是“parents”的直譯,實際上并不表示漢語中的父母雙親,而是一代一代很多parent,即parents。
雙親委派模型的工作過程是:
- 如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成。
- 每一個層次的類加載器都是如此。因此,所有的加載請求最終都應該傳送到頂層的啟動類加載器中。
- 只有當父加載器反饋自己無法完成這個加載請求時(搜索范圍中沒有找到所需的類),子加載器才會嘗試自己去加載。
使用雙親委派模型來組織類加載器之間的關系,有一個顯而易見的好處就是Java類隨著它的類加載器一起具備了一種帶有優先級的層次關系。
例如類`java.lang.Object`,它存放在`rt.jar`之中,無論哪一個類加載器要加載這個類,最終都是委派給處于模型最頂端的啟動類加載器進行加載;
因此Object類在程序的各種類加載器環境中都是同一個類。相反,如果沒有使用雙親委派模型,由各個類加載器自行去加載的話,如果用戶自己編寫了
一個稱為`java.lang.Object`的類,并放在程序的`ClassPath`中,那系統中將會出現多個不同的Object類,Java類型體系中最基礎的行為也就無法保證,
應用程序也將會變得一片混亂。
自己編寫的重名的類可以正常編譯,但是永遠無法運行。