1. 程式人生 > >jvm載入過程

jvm載入過程

bottom 用戶 目的 否則 javac project -h 將在 獨立

類載入過程

類從被載入到虛擬機內存中開始,到卸載出內存為止,它的整個生命周期包含:載入、驗證、準備、解析、初始化、使用和卸載七個階段。它們開始的順序例如以下圖所看到的:

技術分享

當中類載入的過程包含了載入、驗證、準備、解析、初始化五個階段。在這五個階段中,載入、驗證、準備和初始化這四個階段發生的順序是確定的。而解析階段則不一定,它在某些情況下能夠在初始化階段之後開始,這是為了支持 Java 語言的執行時綁定(也成為動態綁定或晚期綁定)。

另外註意這裏的幾個階段是按順序開始,而不是按順序進行或完畢,由於這些階段通常都是互相交叉地混合進行的,通常在一個階段執行的過程中調用或激活還有一個階段。

這裏簡要說明下 Java 中的綁定:綁定指的是把一個方法的調用與方法所在的類(方法主體)關聯起來,對 Java 來說。綁定分為靜態綁定和動態綁定:

  • 靜態綁定:即前期綁定。

    在程序執行前方法已經被綁定,此時由編譯器或其他連接程序實現。針對 Java。簡單的能夠理解為程序編譯期的綁定。

    Java 其中的方法僅僅有 final,static,private 和構造方法是前期綁定的。

  • 動態綁定:即晚期綁定。也叫執行時綁定。

    在執行時依據詳細對象的類型進行綁定。在 Java 中,差點兒全部的方法都是後期綁定的。

以下具體講述類載入過程中每一個階段所做的工作。

載入

載入時類載入過程的第一個階段,在載入階段,虛擬機須要完畢下面三件事情:

  • 通過一個類的全限定名來獲取其定義的二進制字節流。
  • 將這個字節流所代表的靜態存儲結構轉化為方法區的執行時數據結構。
  • 在 Java 堆中生成一個代表這個類的 java.lang.Class 對象,作為對方法區中這些數據的訪問入口。

註意,這裏第 1 條中的二進制字節流並不僅僅是單純地從 Class 文件裏獲取,比方它還能夠從 Jar 包中獲取、從網絡中獲取(最典型的應用便是 Applet)、由其它文件生成(JSP 應用)等。

相對於類載入的其它階段而言,載入階段(準確地說,是載入階段獲取類的二進制字節流的動作)是可控性最強的階段,由於開發者既能夠使用系統提供的類載入器來完畢載入,也能夠自己定義自己的類載入器來完畢載入。

載入階段完畢後。虛擬機外部的 二進制字節流就依照虛擬機所需的格式存儲在方法區之中,並且在 Java 堆中也創建一個 java.lang.Class 類的對象,這樣便能夠通過該對象訪問方法區中的這些數據。

說到載入,不得不提到類載入器,以下就詳細講述下類載入器。

類載入器盡管僅僅用於實現類的載入動作,但它在 Java 程序中起到的作用卻遠遠不限於類的載入階段。對於隨意一個類,都須要由它的類載入器和這個類本身一同確定其在就 Java 虛擬機中的唯一性。也就是說,即使兩個類來源於同一個 Class 文件,僅僅要載入它們的類載入器不同,那這兩個類就必然不相等。這裏的“相等”包含了代表類的 Class 對象的 equals()、isAssignableFrom()、isInstance()等方法的返回結果,也包含了使用 instanceof keyword對對象所屬關系的判定結果。

站在 Java 虛擬機的角度來講。僅僅存在兩種不同的類載入器:

  • 啟動類載入器:它使用 C++ 實現(這裏僅限於 Hotspot,也就是 JDK1.5 之後默認的虛擬機。有非常多其它的虛擬機是用 Java 語言實現的),是虛擬機自身的一部分。
  • 所有其它的類載入器:這些類載入器都由 Java 語言實現,獨立於虛擬機之外,而且所有繼承自抽象類 java.lang.ClassLoader,這些類載入器須要由啟動類載入器載入到內存中之後才幹去載入其它的類。

站在 Java 開發者的角度來看。類載入器能夠大致劃分為下面三類:

  • 啟動類載入器:Bootstrap ClassLoader。跟上面同樣。它負責載入存放在JDK\jre\li(JDK 代表 JDK 的安裝文件夾,下同)下,或被-Xbootclasspath參數指定的路徑中的,而且能被虛擬機識別的類庫(如 rt.jar,全部的java.*開頭的類均被 Bootstrap ClassLoader 載入)。啟動類載入器是無法被 Java 程序直接引用的。
  • 擴展類載入器:Extension ClassLoader,該載入器由sun.misc.Launcher$ExtClassLoader實現。它負責載入JDK\jre\lib\ext文件夾中,或者由 java.ext.dirs 系統變量指定的路徑中的全部類庫(如javax.*開頭的類)。開發人員能夠直接使用擴展類載入器。
  • 應用程序類載入器:Application ClassLoader,該類載入器由 sun.misc.Launcher$AppClassLoader 來實現,它負責載入用戶類路徑(ClassPath)所指定的類。開發人員能夠直接使用該類載入器。假設應用程序中沒有自己定義過自己的類載入器,普通情況下這個就是程序中默認的類載入器。

應用程序都是由這三種類載入器互相配合進行載入的,假設有必要。我們還能夠增加自己定義的類載入器。由於 JVM 自帶的 ClassLoader 僅僅是懂得從本地文件系統載入標準的 java class 文件,因此假設編寫了自己的 ClassLoader,便能夠做到例如以下幾點:

  • 在運行非置信代碼之前,自己主動驗證數字簽名。

  • 動態地創建符合用戶特定須要的定制化構建類。

  • 從特定的場所取得 java class,比如數據庫中和網絡中。

其實當使用 Applet 的時候,就用到了特定的 ClassLoader。由於這時須要從網絡上載入 java class,而且要檢查相關的安全信息,應用server也大都使用了自己定義的 ClassLoader 技術。

這幾種類載入器的層次關系例如以下圖所看到的:

技術分享

這樣的層次關系稱為類載入器的雙親委派模型。我們把每一層上面的類載入器叫做當前層類載入器的父載入器。當然,它們之間的父子關系並非通過繼承關系來實現的,而是使用組合關系來復用父載入器中的代碼。該模型在 JDK1.2 期間被引入並廣泛應用於之後差點兒全部的 Java 程序中。但它並非一個強制性的約束模型。而是 Java 設計者們推薦給開發人員的一種類的載入器實現方式。

雙親委派模型的工作流程是:假設一個類載入器收到了類載入的請求,它首先不會自己去嘗試載入這個類,而是把請求托付給父載入器去完畢。依次向上。因此,全部的類載入請求終於都應該被傳遞到頂層的啟動類載入器中,僅僅有當父載入器在它的搜索範圍中沒有找到所需的類時。即無法完畢該載入。子載入器才會嘗試自己去載入該類。

使用雙親委派模型來組織類載入器之間的關系,有一個非常明顯的優點。就是 Java 類隨著它的類載入器(說白了,就是它所在的文件夾)一起具備了一種帶有優先級的層次關系,這對於保證 Java 程序的穩定運作非常重要。比如,類java.lang.Object 類存放在JDK\jre\lib下的 rt.jar 之中,因此不管是哪個類載入器要載入此類,終於都會委派給啟動類載入器進行載入,這邊保證了 Object 類在程序中的各種類載入器中都是同一個類。

驗證

驗證的目的是為了確保 Class 文件裏的字節流包括的信息符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。不同的虛擬機對類驗證的實現可能會有所不同,但大致都會完畢下面四個階段的驗證:文件格式的驗證、元數據的驗證、字節碼驗證和符號引用驗證。

  • 文件格式的驗證:驗證字節流是否符合 Class 文件格式的規範,而且能被當前版本號的虛擬機處理。該驗證的主要目的是保證輸入的字節流能正確地解析並存儲於方法區之內。經過該階段的驗證後,字節流才會進入內存的方法區中進行存儲。後面的三個驗證都是基於方法區的存儲結構進行的。
  • 元數據驗證:對類的元數據信息進行語義校驗(事實上就是對類中的各數據類型進行語法校驗)。保證不存在不符合 Java 語法規範的元數據信息。
  • 字節碼驗證:該階段驗證的主要工作是進行數據流和控制流分析,對類的方法體進行校驗分析,以保證被校驗的類的方法在執行時不會做出危害虛擬機安全的行為。
  • 符號引用驗證:這是最後一個階段的驗證,它發生在虛擬機將符號引用轉化為直接引用的時候(解析階段中發生該轉化,後面會有解說),主要是對類自身以外的信息(常量池中的各種符號引用)進行匹配性的校驗。

準備

準備階段是正式為類變量分配內存並設置類變量初始值的階段,這些內存都將在方法區中分配。對於該階段有下面幾點須要註意:

  • 這時候進行內存分配的僅包含類變量(static),而不包含實例變量。實例變量會在對象實例化時隨著對象一塊分配在 Java 堆中。

  • 這裏所設置的初始值通常情況下是數據類型默認的零值(如 0、0L、null、false 等),而不是被在 Java 代碼中被顯式地賦予的值。

如果一個類變量的定義為:

public static int value = 3。

那麽變量 value 在準備階段過後的初始值為 0,而不是 3,由於這時候尚未開始運行不論什麽 Java 方法,而把 value 賦值為 3 的 putstatic 指令是在程序編譯後。存放於類構造器 ()方法之中的,所以把 value 賦值為 3 的動作將在初始化階段才會運行。

下表列出了 Java 中全部基本數據類型以及 reference 類型的默認零值:

技術分享

這裏還須要註意例如以下幾點:

  • 對基本數據類型來說。對於類變量(static)和全局變量,假設不顯式地對其賦值而直接使用,則系統會為其賦予默認的零值。而對於局部變量來說。在使用前必須顯式地為其賦值,否則編譯時不通過。
  • 對於同一時候被 static 和 final 修飾的常量。必須在聲明的時候就為其顯式地賦值,否則編譯時不通過。而僅僅被 final 修飾的常量則既能夠在聲明時顯式地為其賦值。也能夠在類初始化時顯式地為其賦值。總之,在使用前必須為其顯式地賦值。系統不會為其賦予默認零值。
  • 對於引用數據類型 reference 來說。如數組引用、對象引用等,假設沒有對其進行顯式地賦值而直接使用,系統都會為其賦予默認的零值。即null。
  • 假設在數組初始化時沒有對數組中的各元素賦值,那麽當中的元素將依據相應的數據類型而被賦予默認的零值。

假設類字段的字段屬性表中存在 ConstantValue 屬性。即同一時候被 final 和 static 修飾,那麽在準備階段變量 value 就會被初始化為 ConstValue 屬性所指定的值。

如果上面的類變量 value 被定義為:

public static final int value = 3。

編譯時 Javac 將會為 value 生成 ConstantValue 屬性,在準備階段虛擬機就會依據 ConstantValue 的設置將 value 賦值為 3。

回顧上一篇博文中對象被動引用的第 2 個樣例。便是這樣的情況。我們能夠理解為 static final 常量在編譯期就將其結果放入了調用它的類的常量池中。

解析

解析階段是虛擬機將常量池中的符號引用轉化為直接引用的過程。在 Class 類文件結構一文中已經比較過了符號引用和直接引用的差別和關聯。這裏不再贅述。

前面說解析階段可能開始於初始化之前,也可能在初始化之後開始,虛擬機會依據須要來推斷,究竟是在類被載入器載入時就對常量池中的符號引用進行解析(初始化之前),還是等到一個符號引用將要被使用前才去解析它(初始化之後)。

對同一個符號引用進行多次解析請求時非經常見的事情,虛擬機實現可能會對第一次解析的結果進行緩存(在執行時常量池中記錄直接引用,並把常量標示為已解析狀態),從而避免解析動作反復進行。

解析動作主要針對類或接口、字段、類方法、接口方法四類符號引用進行,分別相應於常量池中的 CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info 四種常量類型。

1、類或接口的解析:推斷所要轉化成的直接引用是對數組類型,還是普通的對象類型的引用。從而進行不同的解析。

2、字段解析:對字段進行解析時。會先在本類中查找是否包括有簡單名稱和字段描寫敘述符都與目標相匹配的字段。假設有,則查找結束。假設沒有,則會依照繼承關系從上往下遞歸搜索該類所實現的各個接口和它們的父接口,還沒有。則依照繼承關系從上往下遞歸搜索其父類,直至查找結束,查找流程例如以下圖所看到的:

技術分享

從以下一段代碼的運行結果中非常easy看出來字段解析的搜索順序:

class Super{  
    public static int m = 11;  
    static{  
        System.out.println("運行了super類靜態語句塊");  
    }  
}  

class Father extends Super{  
    public static int m = 33;  
    static{  
        System.out.println("運行了父類靜態語句塊");  
    }  
}  

class Child extends Father{  
    static{  
        System.out.println("運行了子類靜態語句塊");  
    }  
}  

public class StaticTest{  
    public static void main(String[] args){  
        System.out.println(Child.m);  
    }  
}  

運行結果例如以下:

 運行了super類靜態語句塊
 運行了父類靜態語句塊
 33

假設凝視掉 Father 類中對 m 定義的那一行,則輸出結果例如以下:

運行了super類靜態語句塊
11

另外,非常明顯這就是上篇博文中的第 1 個樣例的情況,這裏我們便能夠分析例如以下:static 變量發生在靜態解析階段,也即是初始化之前。此時已經將字段的符號引用轉化為了內存引用,也便將它與相應的類關聯在了一起,因為在子類中沒有查找到與 m 相匹配的字段。那麽 m 便不會與子類關聯在一起,因此並不會觸發子類的初始化。

最後須要註意:理論上是依照上述順序進行搜索解析,但在實際應用中,虛擬機的編譯器實現可能要比上述規範要求的更嚴格一些。

假設有一個同名字段同一時候出如今該類的接口和父類中。或同一時候在自己或父類的接口中出現。編譯器可能會拒絕編譯。假設對上面的代碼做些改動,將 Super 改為接口。並將 Child 類繼承 Father 類且實現 Super 接口。那麽在編譯時會報出例如以下錯誤:

StaticTest.java:24: 對 m 的引用不明白,Father 中的 變量 m 和 Super 中的 變量 m
都匹配
                System.out.println(Child.m);
                                        ^
1 錯誤

3、類方法解析:對類方法的解析與對字段解析的搜索步驟幾乎相同,僅僅是多了推斷該方法所處的是類還是接口的步驟,並且對類方法的匹配搜索。是先搜索父類。再搜索接口。

4、接口方法解析:與類方法解析步驟類似,知識接口不會有父類,因此。僅僅遞歸向上搜索父接口即可了。

初始化

初始化是類載入過程的最後一步。到了此階段。才真正開始運行類中定義的 Java 程序代碼。在準備階段。類變量已經被賦過一次系統要求的初始值,而在初始化階段,則是依據程序猿通過程序指定的主觀計劃去初始化類變量和其它資源,或者能夠從還有一個角度來表達:初始化階段是運行類構造器()方法的過程。

這裏簡單說明下()方法的運行規則:

1、()方法是由編譯器自己主動收集類中的全部類變量的賦值動作和靜態語句塊中的語句合並產生的,編譯器收集的順序是由語句在源文件裏出現的順序所決定的,靜態語句塊中僅僅能訪問到定義在靜態語句塊之前的變量。定義在它之後的變量,在前面的靜態語句中能夠賦值。可是不能訪問。

2、()方法與實例構造器()方法(類的構造函數)不同,它不須要顯式地調用父類構造器,虛擬機會保證在子類的()方法運行之前,父類的()方法已經運行完成。

因此,在虛擬機中第一個被運行的()方法的類肯定是java.lang.Object。

3、()方法對於類或接口來說並非必須的。假設一個類中沒有靜態語句塊,也沒有對類變量的賦值操作。那麽編譯器能夠不為這個類生成()方法。

4、接口中不能使用靜態語句塊,但仍然有類變量(final static)初始化的賦值操作,因此接口與類一樣會生成()方法。可是接口魚類不同的是:運行接口的()方法不須要先運行父接口的()方法,僅僅有當父接口中定義的變量被使用時,父接口才會被初始化。另外,接口的實現類在初始化時也一樣不會運行接口的()方法。

5、虛擬機會保證一個類的()方法在多線程環境中被正確地加鎖和同步,假設多個線程同一時候去初始化一個類。那麽僅僅會有一個線程去運行這個類的()方法。其它線程都須要堵塞等待,直到活動線程運行()方法完成。假設在一個類的()方法中有耗時非常長的操作,那就可能造成多個線程堵塞。在實際應用中這樣的堵塞往往是非常隱蔽的。

以下給出一個簡單的樣例,以便更清晰地說明如上規則:

class Father{  
    public static int a = 1;  
    static{  
        a = 2;  
    }  
}  

class Child extends Father{  
    public static int b = a;  
}  

public class ClinitTest{  
    public static void main(String[] args){  
        System.out.println(Child.b);  
    }  
}  

運行上面的代碼,會打印出 2。也就是說 b 的值被賦為了 2。

我們來看得到該結果的步驟。首先在準備階段為類變量分配內存並設置類變量初始值,這樣 A 和 B 均被賦值為默認值 0,而後再在調用()方法時給他們賦予程序中指定的值。當我們調用 Child.b 時,觸發 Child 的()方法,依據規則 2,在此之前。要先運行完其父類Father的()方法。又依據規則1,在運行()方法時,須要按 static 語句或 static 變量賦值操作等在代碼中出現的順序來運行相關的 static 語句,因此當觸發運行 Fathe r的()方法時。會先將 a 賦值為 1,再運行 static 語句塊中語句。將 a 賦值為 2,而後再運行 Child 類的()方法。這樣便會將 b 的賦值為 2。

假設我們顛倒一下 Father 類中“public static int a = 1;”語句和“static語句塊”的順序,程序運行後。則會打印出1。非常明顯是依據規則 1,運行 Father 的()方法時,依據順序先運行了 static 語句塊中的內容。後運行了“public static int a = 1;”語句。

另外,在顛倒二者的順序之後,假設在 static 語句塊中對 a 進行訪問(比方將 a 賦給某個變量)。在編譯時將會報錯,由於依據規則 1,它僅僅能對 a 進行賦值,而不能訪問。

總結

整個類載入過程中。除了在載入階段用戶應用程序能夠自己定義類載入器參與之外,其余全部的動作全然由虛擬機主導和控制。到了初始化才開始運行類中定義的 Java 程序代碼(亦及字節碼)。但這裏的運行代碼僅僅是個開端,它僅限於()方法。類載入過程中主要是將 Class 文件(準確地講,應該是類的二進制字節流)載入到虛擬機內存中,真正運行字節碼的操作,在載入完畢後才真正開始。

jvm載入過程