代碼編譯的結果從本地機器碼變爲字節碼,是儲存格式發展的一小步,倒是編程語言發展的一大步——《深刻理解Java虛擬機》java
虛擬機把描述類的數據從Class文件加載到內存,並對數據進行校驗、轉化解析和初始化,最終造成了能夠被虛擬機直接使用的Java類型,這就是虛擬機的類加載機制。編程
類型的加載、鏈接和初始化都是在程序運行期間完成的,雖然說加大了運行時期的開銷,可是大大增長了Java的靈活度,方便動態加載和鏈接。Java不只能夠從Class文件獲取屬於,也能夠從其餘地方例如網絡中直接獲取二進制流數據,這極大提升了Java的延展性。數組
類從開始加載到卸載一共通過了七個過程,以下圖。緩存
其中驗證、準備、解析統稱爲鏈接。另外,加載、驗證、準備、初始化和卸載這5個過程只是開始要按照順序,能夠同時執行,不用等待上一個過程結束以後才執行。例如,我在9點開始準備,9點10分開始初始化,9點20準備結束。bash
有且只有下面五種狀況,才能夠稱爲「初始化」:網絡
除此以外,全部引用類的方式都不會觸發初始化,僅被稱爲被動引用。數據結構
開個小差,在一個類的靜態代碼塊中,若是某變量提早被被賦值,就能夠被使用;若是某變量以後才賦值的,在靜態代碼塊中使用就會報錯。可是不管什麼時候賦值,只要聲明瞭,在靜態代碼塊中再賦值是被容許的。看下這個例子:編程語言
public class Test{
static{
i=0;//給變量賦值能夠正常編譯經過
System.out.print(i);//這句編譯器會提示"非法向前引用"
}
static int i=1;
}
複製代碼
對於接口來講,有且僅有前三種狀況纔會被稱爲初始化。另外,對於接口,不須要知足提早讓父接口初始化,除非你有用到父接口的時候。函數
逐步看下加載、驗證、準備、解析和初始化這5個過程。佈局
加載過程須要完成如下三個事情:
對於非數組的類,加載能夠經過虛擬提供的類加載器,也能夠經過一用戶自定義的加載器。對於數組類,數組自己不是經過加載器加載的,而是經過Java虛擬機直接建立的,數組中的元素是經過加載器建立的。
加載過程結束後,內存中就會獲得一個該類的java.lang.Class對象,爲後續鋪墊。
在加載開始的同時,驗證擇機開啓。驗證是爲了確保Class文件的字節流種包含的信息符合上章講的規格,不會危害虛擬機自己。這個階段是否嚴謹,直接決定了Java虛擬機是否能承受惡意代碼的攻擊,從執行性能的角度講,驗證階段的工做量在虛擬機類加載子系統中又佔了至關大的一部分。
首先須要驗證是否符合Class文件格式的規範,好比魔數(咖啡寶貝)是否存在,主次版本號是否能夠被當前虛擬機運行、常量類型的tag標誌等等。這個階段的驗證時基於二進制字節流進行的,只有經過了這個階段的驗證後,字節流纔會進入內存的方法區中進行儲存,後面三個驗證階段全是基於方法區的儲存結構進行的,再也不直接進行字節流操做。
此過程包含驗證是否有父類、父類是否容許被繼承啊,各類修飾符是否衝突啊等等。
主要目的時經過數據流和控制流分析肯定程序語義是合法的、符合邏輯的。此過程保證任意時刻的操做數棧的數據類型與指令代碼序列都能配合工做,保證跳轉指令不會跳轉到方法體之外的字節碼指令上,保證類型轉化是正常的,保證父類和子類之間的字段不衝突等等。
因爲數據流驗證很是複雜,爲了減緩消耗的時間,自JDK1.6開始,方法體的Code屬性的屬性表中增長了一項爲「StackMapTable」的屬性,這項屬性描述了方法體中全部的基本塊。在字節碼驗證期間,就不須要根據程序推到這些狀態的合法性,只須要檢驗StackMapTable屬性中的記錄是否合法便可。大大節省了字節碼驗證的時間。
此階段發生在虛擬機將符號引用轉化成直接引用的時候,這個轉化動做將在鏈接的第三個階段解析的時候發生。須要驗證是否能夠經過字符串的全限定名找到這個類,指定的類中是否符合方法的字段描述符以及簡單名稱所描述的方法和字段,類、方法、字段的訪問性等等。
準備階段是正式爲類變量分配內存並設置類變量初始值的階段。此時給靜態變量設置初始值是零值,並非代碼中設置的具體值,具體值還須要在putstatic指令執行時纔會初始代碼中設置的值。除非此static變量被final修飾了們就會在此時直接設置代碼中的值。
解析階段是虛擬機將常量池內的符號引用替換成直接引用的過程。
符號引用:符號引用以一組符號來描述所引用的目標,符號能夠是任何形式的字面量,只要使用時能無歧義地定位到目標便可。符號引用與虛擬機實現的內存分佈無關,引用的目標並不必定已經加載到內存中。
直接引用:直接引用能夠是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄。直接引用是和虛擬機實現的內存佈局相關的,同一個符號引用在不一樣虛擬機實例上翻譯出來的直接引用通常不會相同。若是有了直接引用,那引用的目標一定已經在內存中存在。
除了invokedynamic指令之外,虛擬機實現能夠對第一次解析的結果進行緩存。invokedynamic指令是可動態語言支持相關的指令,因此沒法作到緩存。
類初始化時類加載過程的最後一步。前面的操做除了自定義的類加載器以外,都是虛擬機主導的操做,初始化階段,開始整整執行類中定義的Java代碼了。
初始化階段時執行類構造器<client>()方法的過程。<client>()方法是由編譯器自動收集類中的全部類變量的賦值動做和靜態語句塊中的語句合併產生的,編譯器收集的順序是由語句在源文件中出現的順序所決定的。<client>()方法不須要顯示的構造父類的構造函數,已經本身構造好了,而且父類的靜態代碼塊是先於子類的靜態代碼塊的。而且<client>()方法執行時帶鎖的,不一樣線程執行這個方法可能會出現線程阻塞的現象。
虛擬機設計團隊把類加載階段中「經過一個類的全限定名來獲取此類的二進制字節流」這個動做放到Java虛擬機外部去實現了,實現這個動做的代碼塊叫作類加載器。
對於任意一個類,都須要由加載它的類加載器和這個類自己一同肯定其在Java虛擬機中的惟一性。**若是說某個類相等,那麼這兩個類必定是在同一個類加載器下加載完成的。**這裏的相等可使用Class的equals方法、isAssignableFrom()方法、isInstance()方法驗證,也可使用instanceof關鍵字作對象所屬關係的判斷。例如全限定名都是com.pjjlt.MyTest。一個用虛擬機本身的類加載器加載,一個用用戶自定義的類加載器加載,那麼這兩個類就不相等,分別產生的對象實例用instanceof關鍵字只能做用域本身的類上纔會是true。
那麼問題來了,我要用自定義的類加載器加載一個Object放到內存中,那豈不是整個Java的基礎功能全廢了。其實否則,新建的Object類也會和原生的那個Object類是被同樣對待的。這就涉及了雙親委派機制。
對於虛擬機的角度來講,只有虛擬機的類加載器和用戶自定義的類記載器。對於用戶來講有啓動類加載器(Bootstrap ClassLoader)、拓展類加載器(Extension ClassLoader)、應用程序類加載器(Application ClassLoader)這麼幾種,並且他們是一種組合關係來複用父加載器。
雙親委派機制工做原理:若是一個類加載器收到了類加載的請求,它首先不會本身去嘗試加載這個類,而是把這個請求委派給父加載器去完成,每一層次的類加載器都是如此,所以全部的加載請求最終都應該傳送到頂層的啓動類加載器中,只有它反饋本身沒法加載的時候,纔會交給子加載器加載。
這也解釋了爲何你寫的Object加載器創造出來的類和原生的是同一款了,由於人家就沒有被你本身寫的類加載器所加載,而是某父層的加載器加載了。