3.類加載的全過程java
java虛擬機中類加載的全過程,也就是加載、驗證、準備、解析和初始化這五個階段所執行的具體動做。數組
1.加載安全
加載階段,虛擬機須要完成如下3件事情:數據結構
①經過一個類的全限定名來獲取此類的二進制流。——開發人員可控性最強的階段(常見從jar包中讀取)多線程
②將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構。函數
③在內存中生成一個表明這個類的java.lang.Class對象,做爲方法區這個累的各類數據的訪問入口。——這是一個特殊的對象,它存放在方法區中佈局
這裏對加載的第一階段還須要進行更加詳細的描述:spa
相對於類加載過程的其餘階段,一個非數組類的加載階段(準確地說,是加載階段中獲取類的二進制字節流的階段)是開發人員可控性最強的,由於加載階段既可使用系統提供的引導類加載器來完成,也能夠由用戶自定義的類加載器去完成。開完人員能夠經過定義本身的類加載器去控制字節流的獲取方式(即重寫一個類加載器的loadClass()方法)。線程
對於數組類而言,狀況就有所不一樣,數組類自己不經過類加載器建立,它是由java虛擬機直接建立的。可是數組類與類加載器仍然有密切的關係,由於數組類的元素類型(Element Type,指的是數組去掉全部維度的類型)最終是要靠類加載器去建立,一個數組類建立的過程就遵循如下規則:翻譯
①若是數組的組件類型是引用類型,那就遞歸採用本節中定義的加載過程去加載這個組件類型,數組C將在加載該組件類型的類加載器的類名稱空間上被標誌(這點很重要,以後會介紹到,一個類必須與類加載器一塊兒肯定惟一性)。
②若是數組的組件類型不是引用類型(例如int[]數組),java虛擬機將會把數組c標記爲與引導類加載器關聯。
③數組類的可見性與它的組件類型的可見性一致,若是組件類型不是引用類型,那數組的可見性將默認爲public。
加載階段還未結束,鏈接階段可能已經開始了,雖然兩個階段可能交叉進行,可是兩個階段的開始時間仍然保持着固定的前後順序。
2.驗證
這一階段的目的是爲了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,且不會危害虛擬機自身的安全。
爲何有驗證階段?
Java語言自己是相對安全的語言(依然是相對於C/C++來講),使用純粹的Java代碼沒法作到諸如訪問數組邊界之外的數據、將一個對象轉型爲它併爲實現的類型、跳轉到不存在的代碼行之類的事情,若是這樣作了,編譯器將拒絕編譯。
可是前面也說過Class文件並不必定從Java源碼編譯而來,甚至能夠直接編寫Class文件。在字節碼層面上,上述Java語言沒法作到的事都是能夠實現的,所以必須有驗證階段,防止輸入有害字節流而致使系統崩潰,因此驗證是虛擬機對自身保護的一項重要工做。
若是驗證到輸入的字節流不符合Class文件格式的越是,虛擬機就應拋出一個java.lang.VerifyError異常或其子類異常。
驗證階段大體上會完成下面4個階段的校驗動做:
①文件格式驗證:驗證字節流是否符合Class文件格式的規範,而且能被當前版本的虛擬機處理。
主要目的是保證輸入的字節流能正確的解析並存儲與方法區內,格式上符合描述一個Java類型信息的要求。只有經過了這個階段的驗證後,字節流纔會進入內存的方法區內進行存儲,因此後面的3個階段都是基於方法區的存儲結構進行的,不會再直接操做字節流,
②元數據驗證:對字節碼描述的信息進行語義分析,以保證其描述的信息符合Java語言規範的要求。
③字節碼驗證:主要目的是經過數據流和控制流分析,肯定程序語義是合法的、符合邏輯的。在第二階段對元數據中的數據類型作完校驗以後,這個階段將對累的方法體進行校驗分析,保證北郊眼淚的方法在運行時不會作出危害虛擬機安全的事件。
④符號引用驗證:最後一個階段的校驗發生在虛擬機將符號引用轉換爲直接引用的時候,這個轉換動做將在鏈接的第三階段——解析階段中發生。符號引用驗證能夠看作是對類自身之外(常量池中的各類符號引用)的信息進行匹配性校驗。
目的是確保解析動做可以正確執行,若是沒法經過符號引用驗證,那麼將會拋出一個java.lang.IncompatibloeClassChangeError異常的子類。
3.準備
準備階段是正式爲類變量分配內存並設置類變量初始值(零值)的階段。這些變量所使用內存都將在方法區中進行分配。
兩點須要強調:①這時候進行內存分配的僅包括類變量,而不包括實例變量,實例變量將會在對象實例化時隨着對象一塊兒分配在java堆中。②這裏所說的初始值「一般狀況」下是數據類型的零值。相對也有一些是特殊狀況,若是類字段的字段屬性表存在ConstantValue屬性,那麼準備階段變量value就會被初始化爲ConstantValue屬性所指定的值,如代碼
public static final int value=123;
由於有了final,因此編譯時javac將會爲value生成ConstantValue屬性,在準備階段虛擬機就會根據ConstantValue將value賦值爲123。
4.解析
解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程。
符號引用(Symbolic Reference):符號引用可使任何形式的字面量,只要使用時能無歧義的定位到目標便可。符號引用與虛擬機實現的內存佈局無關,引用的目標並不必定已經加載到內存中。各類虛擬機實現內存佈局能夠各不相同,可是他們能接受的符號引用必須都是一致的,由於符號引用的字面量形式明肯定義在java虛擬機規範的class文件格式中。
直接引用(Direct Reference):直接引用可使直接指向目標的指針,相對偏移量或是一個能間接定位到目標的句柄。直接引用是和虛擬機實現的內存佈局相關的,同一個符號引用在不一樣虛擬機實例上翻譯出來的直接引用通常會不一樣。若是有了直接引用,那引用的目標一定已經在內存中存在。
解析動做主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號引用進行。
5.初始化
類初始化階段是類加載過程的最後一步,前面的類加載過程當中,除了在加載階段用戶應用程序能夠經過自定義類加載器參與以外,其他動做徹底是由虛擬機主導和控制。到了初始化階段,才真正開始執行類中定義的Java程序代碼(或者說字節碼)。
初始化階段是執行類構造器<clinit>()方法的過程。
①<clinit>()方法是由編譯器自動收集類中的全部類變量的賦值動做和靜態語句塊中的語句合併而成的。
須要注意的是,靜態語句塊中只能訪問到定義在靜態語句塊以前的變量,定義在它以後的變量,在前面的靜態語句塊能夠賦值,可是不能訪問。
public class Test{ static{ i=0; //給變量賦值能夠正常編譯經過 System.out.print(i); //這句編譯器會提示「非法向前引用」 } static int i=1; }
②<clinit>()方法與類的構造函數(或者說實例構造器<init>()方法)不一樣,它不須要顯示的調用父類構造器,虛擬機會保證在子類的<clinit>()方法執行以前,父類的<clinit>()方法已經執行完畢。所以第一個被執行的<clinit>()方法的類確定是java.lang.Object
③因爲父類的<clinit>()方法先執行,也就意味着父類中定義的靜態語句塊要優先於子類的變量賦值操做。
④<clinit>()方法對於類或接口來講不是必須的,若是一個類中沒有靜態與太快,也沒有對變量的賦值操做,編譯器能夠不爲這個類生成<clinit>()方法。
⑤接口中不能使用靜態語句塊,但仍然有變量初始化的賦值操做,須要注意的是,執行接口的<clinit>()方法不須要先執行父接口的<clinit>()方法。只有當父接口中定義的變量使用時,父接口才會初始化。另外,接口的實現類在初始化時,也同樣不會執行接口的<clinit>()方法。
⑥虛擬機會保證一個類的<clinit>()方法在多線程環境中被正確地加鎖同步,若是多個線程同時去初始化一個類,那麼只會有一個線程去執行這個類的<clinit>()方法。