1.加載
虛擬機需要干三件事:
①、通過一個類的的全限定名來獲取定義此類的二進制字節流(沒有規定二進制字節流從那里獲取,怎樣獲取,許多java技術也都建立在這基礎上)
②將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構(將常量池轉變成運行時常量池)
③在內存中生成一個代表這個類的java.lang.Class對象,作為方法區着各類的各種數據的訪問入口。
相比較於類加載過程的其他階段,非數組類獲取類的二進制字節流的動作是開發人員可控性最強的,因為加載階段既可以使用系統提供的引導類加載器來完成,也可以由用戶自定義的類加載器去完成
,開發人員可以自己重寫一個類加載器的loadClass()方法
對於數組類,不通過類加載器創建,由java虛擬機直接創建的。但是數組類的元素類型(去掉所有維度的類型)最終是要通過類加載器去創建。
數組類的創建過程要遵循以下原則:
①、如果數組的組件類型(數組去掉一個維度的類型)是引用類型,就遞歸加載過程去加載這個組件類型,數組C將在加載該組件類型的類加載器的類名稱空間上被標識。
②、如果數組的組件類型不是引用類型(例如int[]數組),java虛擬機將會把數組C標記為與引導類加載器關聯
③、數組類的可見性與它的組件類型的可見性一致,如果組件類型不是引用類型,那數組類的可見性將默認為public
加載階段完成后,虛擬機外部的二進制字節流就按照虛擬機所需的格式存儲在方法區之中,方法區中的數據存儲格式由虛擬機自行定義。然后在內存中實例化一個java.lang.Classleide duix (沒有明確規定在java堆中嗎、,對於Hotspot來說,Class對象比較特殊,存放在方法區里面)。這個對象作為程序訪問方法區中這些類型數據的外部接口。
連接階段:
2 .驗證
驗證的目的是確保Class文件的字節流中包含的信息符合當前虛擬機的要求,並且不會危害虛擬機的安全。
整體上看,驗證階段大致上會完成下面四個階段的檢驗工作:
(1)基於字節流檢驗:
文件格式驗證:基於二進制字節流進行的,只有經過這個階段的驗證,字節流才會進入內存的方法區中存儲。目的是保證輸入的字節流能正確解析並存儲於方法區之內
(2)基於方法區的存儲結構:
元數據驗證:對字節碼描述的信息進行語義分析,保證不存在不符合JAVA語言規范的元數據信息。
字節碼驗證:最復雜的一個階段,對類的方法體進行校驗,保證被叫嚴磊的方法在運行時不會做出還虛擬機的事。目的是通過數據流和控制流分析,確定程序語義是合法的、符合邏輯的。
符號引用驗證:發生在虛擬機將符號引用轉化為直接引用的時候,這個轉化動作將在解析階段中發生。目的是確保解析動作能正常執行。
3.准備
准備階段是正式為類變量分配內存並設置初始值的階段,這些變量所使用的內存都在方法區中進行分配。
注意兩點:一點是類變量,另一點是初始值“通常情況下”是數據類型的零值。例如“public static int value=123”在准備過程value的值為0而不是123,因為這時候尚未開始執行任何java方法,而把value賦值為123的puststatic指令是程序被變異后,存放於類構造器<clint>()方法之中,所以把value賦值為123的動作在初始化時候才會進行
特殊:如果類字段的字段屬性表中存在ConstacntValue屬性,那么准備階段變量value就會被初始化為所指定的值
public static final int value=123;
這是value被賦值為123.
4.解析
解析階段是虛擬機將常量池內的符號引用替換為直接引用的過程。
符號引用和直接引用又有什么關聯呢?
符號引用:符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時可以無歧義的定位到目標即可。符號引用與虛擬機實現的內存布局無關,引用的目標並不一定已經加載到內存中,各種虛擬機實現的內存布局可以各不相同,但是它們能接收的符號引用必須都是一致的,因為符號引用的字面量形式明確定義在java虛擬機規范的Class文件格式中。
直接引用:直接引用可以是直接指向目標的指針、相對偏移量或是一個嫩詳見定位到目標的句柄。直接引用時和虛擬機實現的內存布局相關的,通過一個符號引用在不同虛擬機實例上翻譯出來的直接引用一般不會相同。如果有了直接引用,被引用的目標必在內存中存在。
看上圖的常量池,有”//”注釋的就是符號引用常量.它在常量池中的信息是通過一個引用值來標識的.其他可以直接獲取到的值,它其實是直接指向目標的指針,偏移量或者句柄.
解析這個步驟做的事情通俗一點說,就是把”//”后面的數據拿到.
解析的具體時間沒有規定,只要求了在執行anewarray checkcast getfield getstatic instanceof invokedynamic invokeinterface invokespecial invokestatic invokevirtual ldc multianewarray new putfield putstatic 這十六個用於操作符號引用的字節碼指令之前,先對他們所使用的符號引用進行解析
對一個符號引用進行多次解析請求,除了invokedynamic指令以外,虛擬機可以緩存第一次解析的結果,之后再請求解析,可以直接調用避免重復進行。
invokedynamic指令是“動態調用點限定符”動態也就是必須等到程序實際運行到這條指令的時候,解析才進行的。
解析動作主要針對
類或接口解析 :
假設當前代碼所處的類為D,如果要把一個從未解析過的符號引用N解析為一個類或接口C的直接引用,那虛擬機完成這個解析的過程需要以下3個步驟
①:C不是一個數組類型,虛擬機會把代表N的全限定名傳遞給D的類加載器去加載這個類C。在加載過程中,由於元數據驗證、字節碼驗證的需要,有可能觸發其他相關類的加載動作。
②:如果C是一個數組類型,並且數組的元素類型為對象,例如N是“[Ljava/lang/Integer”的形式,那將會按照第1點的規則加載數組元素類型,如果N的描述符是前面那樣,需要加載的元素類型就是“java.lang,Integer”,接着由虛擬機生一個代表此數組維度和元素的數組對象。
③:如果上面的步驟沒有出現異常,C已經在虛擬機中成為了一個有效的類和接口了,但是解析完成之前還有進行符號引用驗證,確保D是否具備對C的訪問權限。
字段解析:
要解析字段符號引用,首先要對字段表內字段所屬的類或接口的符號引用進行解析,如果解析成功,那這個字段所屬的類或接口用C表示,虛擬機規范要求安好如下步驟對C進行后續字段的搜索
①:如果C本身就包含了簡單名稱和字段描述符都與目標相匹配的字段,則返回這個字段的直接引用,查找結束
②:否則如果在C中實現了接口,將會按照繼承關系從下往上遞歸搜索各個接口和它的父接口,如果接口中包含了簡單名稱和字段描述符都與目標相匹配的字段,則返回這個字段的直接引用,查找結束
③:否則,如果C不是java.lang.Object的話,將會按照繼承關系從下往上遞歸搜索其父類,如果父類包含了簡單名稱和字段描述符都與目標相匹配的字段,則返回這個字段的直接引用,查找結束
④:否則查找失敗,拋出異常java.lang.NoSuchMethodError
⑤::如果上面的步驟沒有出現異常,但是解析完成之前還有進行符號引用驗證,確保是否具備對字段的訪問權限。
public class FieldResolution{ interface Interface0{ intA=1; } interface Interface1 extends Interface0{ int A=2; } static class Parent implements Interface1{ public static int A=3; } public static void main(Strings[] args){ System.out.println(Parent.A); } }
如果一個字段出現在C的接口和父類中,或者同時在自己或父類的多個接口中出現,那編譯器將可能拒絕編譯
類方法解析:
要解析類方法符號引用,首先要對類方法表中方法所屬的類或接口的符號引用進行解析,如果解析成功,那這個方法所屬的類或接口用C表示,虛擬機規范要求安好如下步驟對C進行后續類方法的搜索
①:類方法和接口房符號引用的常量類型定義是分開的,如果在類方法表中發現class_index中索引的C是個接口,直接拋出java.lang.IncompatibleClassChangeError異常
②:如果C本身就包含了簡單名稱和描述符都與目標相匹配的方法,則返回這個方法的直接引用,查找結束
③:否則,在C的父類中遞歸查找,如果父類包含了簡單名稱和描述符都與目標相匹配的方法,則返回這個方法的直接引用,查找結束
④:在類C實現的接口列表及它們的父接口之中遞歸查找是否有簡單名稱和描述符都與目標相匹配的方法, 如果存在匹配的方法,說明類C是一個抽象類,這時查找結束,拋出java.lang,AbstractMethodError異常
這個需要這么理解,如果是普通的類去實現某一個接口的方法的話,那么它肯定在第(2)步已經直接返回.如果能執行到第(4)步,則說明C本身的常量池中並沒有對應的直接引用.那么只能是說明這個方法是抽象方法.包含抽象方法的類必定是抽象類,所以這里有個結論就是C是抽象類.
⑤:否則查找失敗,拋出異常java.lang.NoSuchMethodError
⑥::如果上面的步驟沒有出現異常,但是解析完成之前還有進行符號引用驗證,確保是否具備對方法的訪問權限。
接口方法解析:
要解析接口方法符號引用,首先要對接口方法表中方法所屬的類或接口的符號引用進行解析,如果解析成功,那這個方法所屬接口用C表示,虛擬機規范要求安好如下步驟對C進行后續類方法的搜索
①:類方法和接口房符號引用的常量類型定義是分開的,如果在類方法表中發現class_index中索引的C是個類而不是接口,直接拋出java.lang.IncompatibleClassChangeError異常
②:如果C本身就包含了簡單名稱和描述符都與目標相匹配的方法,則返回這個方法的直接引用,查找結束
③:否則,在C的父接口中遞歸查找,直到找到java.lang.Object類位置。如果包含了簡單名稱和描述符都與目標相匹配的方法,則返回這個方法的直接引用,查找結束
④:否則查找失敗,拋出異常java.lang.NoSuchMethodError
接口中方法默認都是public的,因此不存在訪問權限的事
方法類型解析:
方法句柄解析:
調用點限定符解析:
7類符號引用進行解析
5.初始化
類加載過程的最后一步。到了初始化階段,才真正開始執行類中定義的java程序代碼。
准備階段,已經賦過一次系統要求的初始值,而在初始化階段,就要根據程序員的要求來賦值了。從另一個角度來表達:初始化階段是執行類構造器<clinit>()方法的過程。
<clinit>()一些可能會影響程序運行行為的特點和細節。
①<clinit>()是由編譯器按順序收集類中所有的類變量和靜態語句塊中的語句合並產生的。這里注意一點,靜態語句塊只能訪問定義在他前面的類變量,對於定義在他后面的,他只能賦值,而不能訪問。
public class Test{ static{ i=0; //這句賦值正常編譯通過 System.out.print(i); //這句訪問就不行 } static int i=0; }
②<clinit>()與類的構造函數不同,他不需要顯式地調用父類構造器,虛擬機會保證在子類的<clinit>()方法執行之前,父類的<clinit>()已經執行完畢。因此,在虛擬機中第一個被執行的<clinit>()方法的類肯定是java.lang.Object
③ 由於父類的<clinit>()先執行,si所以父類的靜態語句塊要優於子類的變量復制操作。
static class Parent{ public static int A=1; static{ A=2; } static class Sub extends Parent{ public static int B=A; } public static void main(String[] args){ System.out.println(Sub.b); } }
④<clinit>()對於類或接口來說不是必須的,如果一個類中沒有靜態語句塊,也沒有對變量的賦值操作,那么編譯器可以不為這個類生成<clinit>()方法
⑤接口中不能使用靜態語句塊,但仍然有變量初始化的賦值操作,因此接口和雷一樣會生成<clinit>()方法,但是不同的是,執行接口的<clinit>()方法不需要先執行父接口的<clinit>()方法。只有當父接口中定義的變量使用時,父接口才會初始化。另外,接口的實現類在初始化時一樣不會執行接口的<clinit>()。
⑥虛擬機會保證一個類的<clinit>()方法在多線程環境中被正確地加鎖、同步,如果多個線程同時去初始化一個類,那么只會有一個線程去執行這個類的<clinit>()方法,其他線程都需要阻塞等待,直到活動線程的<clinit>()方法完畢。