一.概述
Java不同于C/C++這類傳統的編譯型語言,也不同于php這一類動态的腳本語言。可以說Java是一種半編譯語言,我們所寫的類會先被編譯成.class檔案,這個.class是一串二進制的位元組流。然後當要使用這個類的時候,就會将這個類對應的.class檔案加載進記憶體中。而将這個.class的内容加載進記憶體,正是通過Jvm類加載機制實作的。
虛拟機把描述類的資料從class檔案加載到記憶體,并對資料進行校驗,轉換解析和初始化,最終形成可以被虛拟機直接使用的Java類型,這就是虛拟機的類加載機制。
二.類加載的各個步驟
加載
加載時“類加載”過程的第一步,在加載過程中,虛拟機需要完成以下三件事
- 通過一個類的全限定名來擷取定義此類的二進制位元組流。
- 将這個位元組流所代表的靜态存儲結構轉化為方法區的運作時資料結構。
- 在記憶體中生成一個代表這個類的java.lang.Class對象,作為方法區的這個類的各種資料的通路入口。
值得一提的是,在加載階段既可以使用系統提供的引導類加載器來完成,也可以由使用者自定義的類加載器來完成,相對而是比較自由的,但對于數組則不是這樣了,數組類本身不通過類加載建立,它是由Java虛拟機直接建立的。但資料所存放的元素類型是需要類加載器去建立的。
加載階段與下一階段的連接配接部分是交叉進行的,但加載階段和連接配接階段的開始時間仍然會保持固定的先後順序。
驗證
驗證時連接配接階段的第一步,這一階段的目的是為了確定Class檔案的位元組流中包含的資訊複合目前虛拟機的要求,并且不會危害虛拟機自身的安全。雖然說數組越界,将對象胡亂轉型這些操作會被編譯器拒絕編譯,但.class檔案并不一定要求從Java源碼編譯而來,可以從其他途徑産生,故而需要對.class檔案的二進制流進行驗證。
驗證階段的重要性是不言而喻的,這一階段是否嚴謹,直接決定了Java虛拟機是否能承受惡意代碼的攻擊,從執行性能的角度上講,驗證階段的工作量在虛拟機的類加載系統中又占了相當大的一部分。
從整體上看,驗證階段大緻可分為4部分的檢驗動作:檔案格式驗證,中繼資料驗證,位元組碼驗證,符号引用驗證。
- 符号驗證:主要目的是保證輸入的位元組流能正确地解析并存儲于方法區之内,格式上符合描述一個Java類型資訊的要求。這一部分是基于二進制流驗證的,之後會加載到記憶體中,後續驗證是在記憶體中驗證。
- 中繼資料驗證:這一驗證主要是對類的中繼資料資訊進行語義校驗,保證不存在不符合Java語言規範的中繼資料資訊。
- 位元組碼驗證:這一部分是驗證階段中最複雜的一階段,主要目的是通過資料流和控制流分析,确定程式是合法的,符合邏輯的。
- 符号引用驗證:符号引用是發生在虛拟機将符号引用轉化為直接引用的時候,目的是卻好解析動作能正常執行。
準備
準備階段是為正式類變量(靜态變量)配置設定記憶體并設定類變量初始值的階段,這些變量所使用的記憶體都講在方法區中進行配置設定的。值得一提的是,這時候進行配置設定的僅為類變量(靜态變量),而不包括執行個體變量。
通常情況下,設定類變量初始值,這個初始值指的是資料類型的預設值,比如int型則是0。但若類變量被final修飾,則情況又不一樣,那樣的話會直接對給定值進行指派。
解析
解析階段是虛拟機将常量池内的符号引用替換為直接引用的過程。這裡解釋以下什麼是符号引用,什麼是直接引用。
符号引用:符号引用以一組符号來描述所引用的目标,符号可以是任何形式的字面量,隻要使用時能無歧義得定位到目标即可。
直接引用:直接引用可以是指向目标的指針,相對偏移量或是一個能間接定位到目标的句柄。
解析動作主要針對類或接口,字段,類方法,接口方法,方法類型,方法句柄和調用點限定符7類符号引用進行。
初始化
類初始化階段是類加載過程的最後一步,前面的類加載過程,除了在加載階段使用者應用程式可以通過自定義類加載器參與之外,其餘動作完全由虛拟機主導和控制。到了初始化階段,才會真正開始執行類中定義的Java代碼。
在準備階段,變量已經賦過一次系統要求的初始值,而在初始化階段,則根據程式員通過程式制定的計劃區初始化類變量和其他資源。
三.有意思的代碼段
public class StaticTest
{
public static void main(String[] args)
{
staticFunction();
}
static StaticTest st = new StaticTest();
static
{
System.out.println("1");
}
{
System.out.println("2");
}
StaticTest()
{
System.out.println("3");
System.out.println("a="+a+",b="+b);
}
public static void staticFunction(){
System.out.println("4");
}
int a=110;
static int b =112;
}
這段代碼的運作結果是什麼呢?
答案是:
2
3
a=110,b=0
1
4
這是為什麼呢,大家不妨思考以下。
了解這段代碼不光是要明白Java的類加載機制,還需要明白初始化階段,靜态代碼塊與靜态成員變量的初始化順是與代碼順序有關的。
類加載的過程是:裝載–>連接配接(驗證,準備,解析)–>初始化。
1.在準備階段,會為類變量設定預設值,是以在案例一中:st=null,b=0,
2.在初始化階段,會先執行類構造器,
換句話說,就是執行static修飾的代碼塊和為static修飾的變量指派而已。而static修飾的代碼塊和類變量的執行順序是按照它在檔案中的先後順序執行的。而static StaticTest st = new StaticTest()排在第一,是以會執行 new StaticTest(),也就是進行對象的初始化
2.1.在對象的初始化過程中,會先執行成員變量(代碼塊),然後再執行構造方法.成員變量的執行順序也是誰先聲明,誰先執行,是以排在第一的代碼塊
2.2成員變量執行完後,執行構造方法.此時,a=110,b=0;
3.由static StaticTest st = new StaticTest();觸發的非靜态代碼的初始化過程到此結束,接下來繼續執行靜态代碼的初始化,于是輸出 1 。
4.整個類加載到此結束,執行代碼,輸出 4 。
再看下一道
public class StaticTest
{
public static void main(String[] args)
{
staticFunction();
}
static
{
System.out.println("1");
}
{
System.out.println("2");
}
StaticTest()
{
System.out.println("3");
System.out.println("a="+a+",b="+b);
}
public static void staticFunction(){
System.out.println("4");
}
int a=110;
static int b =112;
static StaticTest st = new StaticTest(); //将這條語句放到最下面
}
僅僅是改變一條語句,而這段代碼的運作結果是
a=110,b=112
大家不妨運用上面的知識,想想是為什麼。