一.概述 Java不同於C/C++這類傳統的編譯型語言,也不同於php這一類動態的腳本語言。可以說Java是一種半編譯語言,我們所寫的類會先被編譯成.class文件,這個.class是一串二進位的位元組流。然後當要使用這個類的時候,就會將這個類對應的.class文件載入進記憶體中。而將這個.class的 ...
一.概述
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(); //將這條語句放到最下麵
}
僅僅是改變一條語句,而這段代碼的運行結果是
1
2
3
a=110,b=112
4
大家不妨運用上面的知識,想想是為什麼。