JVM類載入機制詳解(一)JVM類載入過程
首先Throws(丟擲)幾個自己學習過程中一直疑惑的問題:
1、什麼是類載入?什麼時候進行類載入?
2、什麼是類初始化?什麼時候進行類初始化?
3、什麼時候會為變數分配記憶體?
4、什麼時候會為變數賦預設初值?什麼時候會為變數賦程式設定的初值?
5、類載入器是什麼?
6、如何編寫一個自定義的類載入器?
首先,在程式碼編譯後,就會生成JVM(Java虛擬機器)能夠識別的二進位制位元組流檔案(*.class)。而JVM把Class檔案中的類描述資料從檔案載入到記憶體,並對資料進行校驗、轉換解析、初始化,使這些資料最終成為可以被JVM直接使用的Java型別,這個說來簡單但實際複雜的過程叫做JVM的類載入機制
Class檔案中的“類”從載入到JVM記憶體中,到卸載出記憶體過程有七個生命週期階段。類載入機制包括了前五個階段。
如下圖所示:
其中,載入、驗證、準備、初始化、解除安裝的開始順序是確定的,注意,只是按順序開始,進行與結束的順序並不一定。解析階段可能在初始化之後開始。
另外,類載入無需等到程式中“首次使用”的時候才開始,JVM預先載入某些類也是被允許的。(類載入的時機)
一、類的載入
我們平常說的載入大多不是指的類載入機制,只是類載入機制中的第一步載入。在這個階段,JVM主要完成三件事:
1、通過一個類的全限定名(包名與類名)來獲取定義此類的二進位制位元組流(Class檔案)。而獲取的方式,可以通過jar包、war包、網路中獲取、JSP檔案生成等方式。
2、將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構。這裡只是轉化了資料結構,並未合併資料。(方法區就是用來存放已被載入的類資訊,常量,靜態變數,編譯後的程式碼的執行時記憶體區域)
3、在記憶體中生成一個代表這個類的java.lang.Class物件,作為方法區這個類的各種資料的訪問入口。這個Class物件並沒有規定是在Java堆記憶體中,它比較特殊,雖為物件,但存放在方法區中。
二、類的連線
類的載入過程後生成了類的java.lang.Class物件,接著會進入連線階段,連線階段負責將類的二進位制資料合併入JRE(Java執行時環境)中。類的連線大致分三個階段。
1、驗證:
2、準備:為類的靜態變數(static filed)在方法區分配記憶體,並賦預設初值(0值或null值)。如static int a = 100;
靜態變數a就會在準備階段被賦預設值0。
對於一般的成員變數是在類例項化時候,隨物件一起分配在堆記憶體中。
另外,靜態常量(static final filed)會在準備階段賦程式設定的初值,如static final int a = 666; 靜態常量a就會在準備階段被直接賦值為666,對於靜態變數,這個操作是在初始化階段進行的。
3、解析:將類的二進位制資料中的符號引用換為直接引用。
三、類的初始化
類初始化是類載入的最後一步,除了載入階段,使用者可以通過自定義的類載入器參與,其他階段都完全由虛擬機器主導和控制。到了初始化階段才真正執行Java程式碼。
類的初始化的主要工作是為靜態變數賦程式設定的初值。
如static int a = 100;在準備階段,a被賦預設值0,在初始化階段就會被賦值為100。
Java虛擬機器規範中嚴格規定了有且只有五種情況必須對類進行初始化:
1、使用new位元組碼指令建立類的例項,或者使用getstatic、putstatic讀取或設定一個靜態欄位的值(放入常量池中的常量除外),或者呼叫一個靜態方法的時候,對應類必須進行過初始化。
2、通過java.lang.reflect包的方法對類進行反射呼叫的時候,如果類沒有進行過初始化,則要首先進行初始化。
3、當初始化一個類的時候,如果發現其父類沒有進行過初始化,則首先觸發父類初始化。
4、當虛擬機器啟動時,使用者需要指定一個主類(包含main()方法的類),虛擬機器會首先初始化這個類。
5、使用jdk1.7的動態語言支援時,如果一個java.lang.invoke.MethodHandle例項最後的解析結果REF_getStatic、REF_putStatic、RE_invokeStatic的方法控制代碼,並且這個方法控制代碼對應的類沒有進行初始化,則需要先觸發其初始化。
注意,虛擬機器規範使用了“有且只有”這個詞描述,這五種情況被稱為“主動引用”,除了這五種情況,所有其他的類引用方式都不會觸發類初始化,被稱為“被動引用”。
被動引用的例子一:
通過子類引用父類的靜態欄位,對於父類屬於“主動引用”的第一種情況,對於子類,沒有符合“主動引用”的情況,故子類不會進行初始化。程式碼如下:
//父類
public class SuperClass {
//靜態變數value
public static int value = 666;
//靜態塊,父類初始化時會呼叫
static{
System.out.println("父類初始化!");
}
}
//子類
public class SubClass extends SuperClass{
//靜態塊,子類初始化時會呼叫
static{
System.out.println("子類初始化!");
}
}
//主類、測試類
public class NotInit {
public static void main(String[] args){
System.out.println(SubClass.value);
}
}
輸出結果:
被動引用的例子之二:
通過陣列來引用類,不會觸發類的初始化,因為是陣列new,而類沒有被new,所以沒有觸發任何“主動引用”條款,屬於“被動引用”。程式碼如下:
//父類
public class SuperClass {
//靜態變數value
public static int value = 666;
//靜態塊,父類初始化時會呼叫
static{
System.out.println("父類初始化!");
}
}
//主類、測試類
public class NotInit {
public static void main(String[] args){
SuperClass[] test = new SuperClass[10];
}
}
沒有任何結果輸出!
被動引用的例子之三:
剛剛講解時也提到,靜態常量在編譯階段就會被存入呼叫類的常量池中,不會引用到定義常量的類,這是一個特例,需要特別記憶,不會觸發類的初始化!
//常量類
public class ConstClass {
static{
System.out.println("常量類初始化!");
}
public static final String HELLOWORLD = "hello world!";
}
//主類、測試類
public class NotInit {
public static void main(String[] args){
System.out.println(ConstClass.HELLOWORLD);
}
}