虛擬機把描述類的數據從Class文件加載到內存,並對數據進行校驗、轉換解析和初始化,最終造成能夠被虛擬機直接使用的Java類型,這就是虛擬機的類加載機制java
在Java語言裏面,類型的加載、鏈接和初始化過程都是在程序運行期間完成的,雖然會令類加載時稍微增長一些性能開銷,可是會爲Java應用程序提供高度的靈活性,Java裏天生能夠動態擴展的語言特性,就是以來運行期動態加載和動態鏈接這個特色實現的。程序員
類加載的時機
類從被加載到虛擬機內存中開始,到卸載出內存爲止,它的生命週期包括:加載(Loading)->驗證(Verification)->準備(Preparation)->解析(Resolution)->初始化(Initialization)->使用(Using)->卸載(Unloading)。其中,驗證,準備,解析3部分統稱爲鏈接。如圖數據庫
加載、驗證、準備、初始化和卸載這5個階段的順序是肯定的,類的加載過程必須按照這種順序循序漸進地開始;
在某些狀況下,解析階段能夠在初始化階段以後再開始,這是爲了支持Java語言的運行時綁定(也稱爲動態綁定或晚期綁定)。
這些階段通常都是互相交叉地混合式進行的,在一個階段執行的過程當中一般會調用、激活另一個階段。
類加載第一過程:加載,Java虛擬機規範中並無進行強制約束,這點能夠交給虛擬機的具體實現來自由把握。對於初始化,虛擬機規範則是嚴格規定了有且僅有 7種狀況必須當即對類進行「初始化」(而加載、驗證、準備天然須要在此以前)數組
使用new關鍵字實例化對象的時候
讀取或設置一個類的靜態字段(被final修飾、已在編譯器把結果放入常量池的靜態字段除外)
調用一個類的靜態方法的時候
使用Java.lang.reflect包的方法對類進行反射調用的時候
初始化子類,會先初始化父類
虛擬機啓動時,用戶須要指定一個要執行的主類(包含main方法的那個類),虛擬機會先初始化這個主類
當使用jdk1.7的動態語言支持時,若是一個java.lang.invoke.MethodHandle實例最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,而且這個方法句柄所對應的類沒有進行過初始化,則須要先觸發其初始化。
類加載的過程
加載
加載時類加載(Class Loading)過程的一個階段。在加載階段,虛擬機須要完成如下三件事:安全
經過一個類的全限定名稱來獲取定義此類的二進制流。
將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構
在內存中生成一個表明這個類的java.lang.Class對象,做爲方法區這個類的各類數據的訪問入口
虛擬機實現與具體應用的靈活度都是至關大的,如第一條,虛擬機設計團隊設計了以下舉足輕重的技術:服務器
從ZIP包中讀取,這很常見,最終成爲往後JAR、EAR、WAR格式的基礎
從網絡中獲取,這種場景最典型的應用就是Applet
運行時計算生成,動態代理技術,在Proxy類中,就是用了ProxyGenerator.generateProxyClass來爲特定接口生成形式爲「*$Proxy」的代理類的二進制字節流
由其餘文件生成,典型場景就是JSP應用,即由JSP文件生成對應的Class類
從數據庫中讀取,若有些中間件服務器(如SAP Netweaver)能夠選擇把程序安裝到數據庫中來完成程序代碼在集羣間的分發。
一個非數組類的加載階段(加載階段中獲取類的二進制字節碼)時開發人員可控性最強的。開發人員能夠經過定義本身的類加載器去控制字節流的獲取方式,即重寫一個類加載器的loadClass()方法網絡
對於數組類而言,數組類自己不經過類加載器建立,它是由Java虛擬機直接建立的,一個數組類建立過程就遵循如下規則數據結構
若是數組的組件類型(Component Type,指的是數組去掉一個維度的類型)是引用類型,那就遞歸採用本節中定義的加載過程去加載這個組件類型,數組將在加載該組件類型的類加載器的類名稱空間上被標識(一個類必須與類加載器一塊兒肯定惟一性)。
若是數組的組件類型不是引用類型(例如int[]數組),Java虛擬機會把數組標記爲與引導類加載器關聯。
數組類的可見性與它的組件類型的可見性一致,若是組件類型不是引用類型,那數組類的可見性將默認爲public。
加載階段完成後,虛擬機外部的二進制字節流就按照虛擬機所需的格式存儲在方法區之中,方法區中的數據存儲格式由虛擬機實現自行定義。而後在內存中實例化一個java.lang.Class類的對象,這個對象將做爲程序訪問方法區中的這些類型數據的外部接口。多線程
注:對於HotSpot虛擬機而言,Class對象比較特殊,它雖然是對象,但存放在方法區裏面。佈局
加載階段與鏈接階段的部份內容(如一部分字節碼文件格式驗證動做)是交叉進行的,加載階段還沒有完成,鏈接階段可能已經開始,但這些夾在加載階段之中進行的動做,仍然屬於鏈接階段的內容,這兩個階段的開始時間仍然保持着固定的前後順序。
驗證
驗證是鏈接階段的第一步,這一階段的目的是爲了確保Class文件的字節流中包含的信息符合當前虛擬機的需求,而且不會危害虛擬機自身的安全。從總體上看,驗證階段大體上會完成下面4個階段的校驗動做:
文件格式驗證
驗證字節流是否符合Class文件格式的規範,而且能被當前版本的虛擬機處理。可能包括如下驗證點:
是否以魔數0xCAFEBABE開頭。
主、次版本號是否在當前虛擬機處理範圍以內。
常量池的常量中是否有不被支持的常量類型(檢查常量tag標誌)。
指向常量的各類索引值中是否有指向不存在的常量或不符合類型的常量。
CONSTANT_Utf8_info型的常量中是否有不符合UTF8編碼的數據。
Class文件中各個部分及文件自己是否有被刪除的或附加的其餘信息。
...
這階段的驗證是基於二進制字節流進行的,只有經過了這個階段的驗證後,字節流纔會進入內存的方法區中進行存儲,因此後面的3個驗證階段所有是基於方法區的存儲結構進行的,不會再驗證字節流
元數據驗證
第二階段是對字節碼描述的信息進行語義分析,以保證其描述的信息符合Java語言規範的要求,這個階段可能的驗證點以下:
這個類是否有父類(除了java.lang.Object以外,全部類都應當有父類)。
這個類是否繼承了不容許被繼承的類(被final修飾的類)。
若是這個類不是抽象類,是否實現了其父類或接口之中所要求實現的全部方法。
類中的字段、方法是否與父類產生矛盾(例如覆蓋了父類的final字段,或者出現不符合規則的方法重載,例如方法參數都一致,但返回值類型卻不一樣等等)。
...
字節碼驗證
主要目的是經過數據流和控制流分析,肯定程序語義是合法的、符合邏輯的。在第二階段對元數據中的數據類型作完校驗後,這個階段將對方法體進行校驗分析,例如:
保證任意時刻操做數棧的數據類型與指令代碼序列都能配合工做,例如不會出現相似這樣的狀況:在操做數棧放置了一個int類型的數據,使用時卻按long類型來加載入本地變量表中。
保證跳轉指令不會跳轉到方法體之外的字節碼指令上。
保證方法體中的類型轉換是有效的,例如能夠把一個子類對象賦值給父類數據類型,可是把父類對象賦值給子類數據類型,甚至把對象賦值給與它毫無繼承關係、徹底不相干的一個數據類型,則是危險不合法的。
...
若是一個類方法體的字節碼沒有經過字節碼驗證,那確定是有問題的;但若是一個方法體經過了字節碼驗證,也不能說明其必定就是安全的。即便字節碼驗證之中進行了大量的檢查,也不能保證這一點。
因爲數據流驗證的高複雜性,虛擬機設計團隊爲了不過多的時間消耗在字節碼驗證階段,在JDK 1.6以後的Javac編譯器和Java虛擬機中給方法體的Code屬性的屬性表中增長了一項StackMapTable屬性,用於描述了方法體中全部的基本塊(Basic Block,按照控制流拆分的代碼塊)開始時本地變量表和操做棧應有的狀態,在字節碼驗證期間,就不須要根據程序推導這些狀態的合法性,只須要檢查StackMapTable屬性中的記錄是否合法便可。這樣將字節碼驗證的類型推導轉變爲類型檢查從而節省一些時間。
在JDK 1.6的HotSpot虛擬機中提供了-XX:-UseSplitVerifier選項來關閉這項優化,或者使用參數-XX:+FailOverToOldVerifier要求在類型校驗失敗的時候退回到舊的類型推導方式進行校驗。而在JDK 1.7以後,對於主版本號大於50的Class文件,使用類型檢查來完成數據流分析校驗則是惟一的選擇,不容許再退回到類型推導的校驗方式。
符號引用驗證
符號引用驗證階段的校驗發生在虛擬機將符號引用轉化爲直接引用的時候,這個轉化動做將在鏈接的第三階段——解析階段中發生。符號引用驗證能夠看作是對類自身之外(常量池中的各類符號引用)的信息進行匹配性校驗,一般須要校驗下列內容:
符號引用中經過字符串描述的全限定名是否能找到對應的類。
在指定類中是否存在符合方法的字段描述符以及簡單名稱所描述的方法和字段。
符號引用中的類、字段、方法的訪問性(private、protected、public、default)是否可被當前類訪問
...
符號引用驗證的目的是確保解析動做能正常執行,若是沒法經過符號引用驗證,那麼將會拋出一個java.lang.IncompatibleClassChangeError異常的子類,如java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError等。
對於虛擬機的類加載機制來講,驗證階段是一個很是重要的、但不是必定必要(由於對程序運行期沒有影響)的階段。若是所運行的所有代碼(包括本身編寫的及第三方包中的代碼)都已經被反覆使用和驗證過,那麼在實施階段就能夠考慮使用-Xverify:none參數來關閉大部分的類驗證措施,以縮短虛擬機類加載的時間。
準備
準備階段是正式爲類變量分配內存並設置類變量初始值的階段,這些變量所使用的內存都將在方法區中進行分配。這個階段中有兩個容易產生混淆的概念須要強調一下,首先,這時候進行內存分配的僅包括類變量(被static修飾的變量),而不包括實例變量,實例變量將會在對象實例化時隨着對象一塊兒分配在Java堆中。其次,這裏所說的初始值「一般狀況」下是數據類型的零值,假設一個類變量的定義爲public static int value = 123,那變量value在準備階段事後的初始值爲0而不是123,由於這時候還沒有開始執行任何Java方法,而把value賦值爲123的putstatic指令是程序被編譯後,存放於類構造器()方法之中,因此把value賦值爲123的動做將在初始化階段纔會執行。下表列出了Java中全部基本數據類型的零值:
數據類型
零值
int
0
long
0L
short
(short)0
char
‘\u0000’
byte
(byte)0
boolean
false
float
0.0f
double
0.0d
reference
null
假設上面類變量value的定義被修飾爲final,則value會變爲常量,此時value的字段屬性表中存在ConstantValue屬性,在準備階段變量value會被初始化爲ConstantValue屬性所指定的值,編譯時Javac將會爲value生成ConstantValue屬性,在準備階段虛擬機就會根據ConstantValue的設置將value賦值爲123。
解析
解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程。
符號引用(Symbolic References):符號引用以一組符號來描述所引用的目標,符號能夠是任何形式的字面量,只要使用時能無歧義地定位到目標便可。符號引用與虛擬機實現的內存佈局無關,引用的目標並不必定已經加載到內存中。各類虛擬機實現的內存佈局能夠各不相同,可是它們能接受的符號引用必須都是一致的,由於符號引用的字面量形式明肯定義在Java虛擬機規範的Class文件格式中。
直接引用(Direct References):直接引用能夠是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄。直接引用是和虛擬機實現的內存佈局相關的,同一個符號引用在不一樣虛擬機實例上翻譯出來的直接引用通常不會相同。若是有了直接引用,那引用的目標一定已經在內存中存在。
解析動做主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號引用進行,分別對應於常量池的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info、CONSTANT_MethodType_info、CONSTANT_MethodHandle_info和CONSTANT_InvokeDynamic_info這7種常量類型。下面先介紹前4種。
類或接口的解析
假設當前類爲D,須要將未解析過的符號引用N解析爲一個類或接口C的直接引用,主要有三個步驟:
若是C是非數組類型,D的類加載器會根據表明N的全限定名去加載C,加載過程當中可能會觸發其餘相關類的加載。
若是C是數組類型,且數組的元素類型爲非基本類型,那麼首先會按第一步中的方式加載數組元素類型,而後生成一個表明此數組維度和元素的數組對象。
若是前兩步沒有出現任何異常,那麼C在虛擬機中實際上已經成爲一個有效的類或接口了,但在解析完成以前還要進行符號引用驗證,確認D是否具有對C的訪問權限。若是發現不具有訪問權限,將拋出java.lang.IllegalAccessError異常。
字段的解析
要解析一個未被解析過的字段符號引用,首先將會對字段表的第一個index索引項指向的CONSTANT_Class_info符號引用進行解析,也就是字段所在的類或接口的符號引用。若是在解析這個類或接口符號引用的過程當中出現了任何異常,都會致使字段符號引用解析的失敗。若是解析成功完成,那將這個字段所屬的類或接口用C表示,虛擬機規範要求按照以下步驟對C進行後續字段的搜索:
若是C自己就包含了簡單名稱和字段描述符都與目標相匹配的字段,則返回這個字段的直接引用,查找結束。
不然,若是在C中實現了接口,將會按照繼承關係從下往上遞歸搜索各個接口和它的父接口,若是接口中包含了簡單名稱和字段描述符都與目標相匹配的字段,則返回這個字段的直接引用,查找結束。
不然,若是C不是java.lang.Object的話,將會按照繼承關係從下往上遞歸搜索其父類,若是在父類中包含了簡單名稱和字段描述符都與目標相匹配的字段,則返回這個字段的直接引用,查找結束。
不然,查找失敗,拋出java.lang.NoSuchFieldError異常。 若是有一個同名字段同時出如今C的接口和父類中,或者同時在本身或父類的多個接口中出現,那編譯器將可能拒絕編譯。
若是查找過程成功返回了引用,將會對這個字段進行權限驗證,若是發現不具有對字段的訪問權限,將拋出java.lang.IllegalAccessError異常。
類方法的解析
類方法解析的第一個步驟與字段解析同樣,也須要先解析出類方法表的第一個index索引項指向的方法所屬的類或接口的符號引用,若是解析成功,咱們依然用C表示這個類,接下來虛擬機將會按照以下步驟進行後續的類方法搜索:
類方法和接口方法符號引用的常量類型定義是分開的,若是在類方法表中發現第一個index索引項指向的C是個接口,那就直接拋出java.lang.IncompatibleClassChangeError異常。
若是經過了第1步,在類C中查找是否有簡單名稱和描述符都與目標相匹配的方法,若是有則返回這個方法的直接引用,查找結束。
不然,在類C的父類中遞歸查找是否有簡單名稱和描述符都與目標相匹配的方法,若是有則返回這個方法的直接引用,查找結束。
不然,在類C實現的接口列表及它們的父接口之中遞歸查找是否有簡單名稱和描述符都與目標相匹配的方法,若是存在匹配的方法,說明類C是一個抽象類,這時查找結束,拋出java.lang.AbstractMethodError異常。
不然,宣告方法查找失敗,拋出java.lang.NoSuchMethodError。 最後,若是查找過程成功返回了直接引用,將會對這個方法進行權限驗證,若是發現不具有對此方法的訪問權限,將拋出java.lang.IllegalAccessError異常。
接口方法的解析
接口方法也須要先解析出接口方法表中指向聲明方法的類或接口描述符的符號引用,若是解析成功,依然用C表示這個接口,接下來虛擬機將會按照以下步驟進行後續的接口方法搜索:
與類方法解析不一樣,若是在接口方法表中發現聲明方法的類或接口描述符中的索引C是個類而不是接口,那就直接拋出java.lang.IncompatibleClassChangeError異常。
不然,在接口C中查找是否有簡單名稱和描述符都與目標相匹配的方法,若是有則返回這個方法的直接引用,查找結束。
不然,在接口C的父接口中遞歸查找,直到java.lang.Object類(查找範圍會包括Object類)爲止,看是否有簡單名稱和描述符都與目標相匹配的方法,若是有則返回這個方法的直接引用,查找結束。
不然,宣告方法查找失敗,拋出java.lang.NoSuchMethodError異常。 因爲接口中的全部方法默認都是public的,因此不存在訪問權限的問題,所以接口方法的符號解析應當不會拋出java.lang.IllegalAccessError異常。
初始化
初始化階段是執行類構造器<clinit>()方法的過程。在準備階段,變量已經賦過一次系統要求的初始值,而在初始化階段,則根據程序員經過程序制定的主觀計劃去初始化類變量和其餘資源。
<clinit>()方法是由編譯器自動收集類中的全部類變量的賦值動做和靜態語句塊(static{}代碼塊)中的語句合併產生的.
<clinit>()方法不須要顯式地調用父類構造器,虛擬機會保證在子類的<clinit>()方法執行以前,父類的<clinit>()方法已經執行完畢。所以在虛擬機中第一個被執行的<clinit>()方法的類確定是java.lang.Object。
父類的<clinit>()方法先執行,也就意味着父類中定義的靜態語句塊要優先於子類的變量賦值操做 -若是一個類中沒有靜態語句塊,也沒有對變量的賦值操做,那麼編譯器能夠不爲這個類生成<clinit>()方法。
接口中不能使用靜態語句塊,但仍然有變量初始化的賦值操做,所以接口與類同樣都會生成<clinit>()方法。與類不一樣的是,執行接口的<clinit>()方法不須要先執行父接口的<clinit>()方法。只有當父接口中定義的變量使用時,父接口才會初始化。另外,接口的實現類在初始化時也同樣不會執行接口的<clinit>()方法。
虛擬機會保證一個類的<clinit>()方法在多線程環境中被正確地加鎖、同步,若是多個線程同時去初始化一個類,那麼只會有一個線程去執行這個類的<clinit>()方法,其餘線程都須要阻塞等待,直到活動線程執行<clinit>()方法完畢。若是在一個類的<clinit>()方法中有耗時很長的操做,就可能形成多個線程阻塞,在實際應用中這種阻塞每每是很隱蔽的。須要注意的是,其餘線程雖然會被阻塞,但若是執行<clinit>()方法的那條線程退出<clinit>()方法後,其餘線程喚醒以後不會再次進入<clinit>()方法。同一個類加載器下,一個類型只會初始化一次。