JVM總結(四):JVM類加載機制

這一節咱們來總結一下JVM類加載機制。具體目錄以下: 
java

  虛擬機把描述類的數據從Class文件加載到內存,並對數據進行校驗、轉換解析和初始化,最終造成能夠被虛擬機直接使用的Java類型,這就是Java虛擬機的類加載機制。程序員

  在Java中,類型的加載、鏈接和初始化過程都在程序運行期間完成的,這種策略雖然會使類加載時增長一些性能開銷,可是提供了高度的靈活性,Java裏天生能夠動態擴展的語言特就是依賴於運行期動態加載和動態鏈接的特色實現的。 
  Class文件指的是一串二進制的字節流。實際上,每一個Class文件都有可能表明着Java語言中的一個類或者接口。
數組

類加載的過程

類加載過程歸納

 
  在這七個過程當中,加載、驗證、準備、初始化、卸載這5個階段的順序是必定的,類的加載過程必須按照這種順序循序漸進地開始,而解析過程則不必定:它在某個狀況下能夠在初始化階段以後再開始,這是爲了支持Java語言語言的運行時綁定(也叫動態綁定和晚期綁定)。 
  這裏強調的是:類加載階段都是互相交叉地混合式進行的,一般是在一個階段執行的過程當中調用、激活另外一階段。
安全

說說引用

  對類的初始化操做可分爲主動引用和被動引用 
主動引用:在如下5種狀況下會進行類的主動引用的初始化操做:
數據結構

  • 遇到new、getstatic、putstatic或invokestatic這4條字節碼指令時,若是類沒有進行初始化,則須要先觸發其初始化。生成這4條指令最多見的代碼情景是:使用new關鍵字實例化對象、讀取過設置一個類的靜態字段(被final修飾、已在編譯期把結果放進常量池的靜態字段除外)的時候,以及調用一個類的靜態方法的時候。
  • 使用java.lang.reflect包對類進行反射調用時,若是類沒有進行過初始化,則應須要先觸發其初始化。
  • 當初始化一個類時,若是發現其父類尚未進行過初始化,會觸發其父類實例化。
  • 當虛擬機啓動時,用戶須要指定一個要執行的主類(包含main方法類),虛擬機會先實例化那個類。
  • 當使用JDK1.7的動態語言支持時,若是一個java.lang.invoke.MethodHandle實例的最後解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,而且這個方法句柄所對應的類沒有進行過初始化,則要先觸發其初始化。 
    被動引用:全部引用類的方式都不會觸發初始化。 
    對於靜態字段,只有直接定義這個字段的類纔會被初始化,所以經過其子類來引用父類中定義的靜態字段,只會觸發父類的初始化而不會觸發子類的初始化。 
    經過數組定義引用類,不會觸發此類初始化:當初始化對象數組時,並不會實際觸發對象的初始化操做。可是會觸發一個是由虛擬機自動生成的、直接繼承於java.lang.Object的子類,建立動做由字節碼指令newarray觸發。值得注意的是:該類表明了實際的對象數組,數組中應有的方法和屬性都實如今這個類裏。Java語言對數組的訪問比C/C++相對安全是由於這個類分裝了數組元素的訪問方法。 
    常量在編譯階段會存入調用類的常量池中,本質上並無直接引用到定義常量的類,所以不會觸發定義類的初始化。

值得注意的是: 
  接口也有本身的初始化過程:編譯器會爲接口生成「()」類構造器,用於初始化接口中所定義的成員變量。 
  接口和類初始化的區別:當一個類在初始化時,其父類都基本上初始化過了,然而接口在初始化的時候,只有真正用到父接口的時候(如引用接口中定義的常量)纔會進行初始化。
性能

詳解類加載全過程:

加載

在加載階段,虛擬機須要完成如下3件事情:spa

  • 經過一個類的全限定名來獲取定義此類的二進制字節流。
  • 將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構。
  • 在內存中生成一個表明這個類的java.lang.Class對象,做爲方法區這個類的各類數據的訪問接口。

  非數組類的加載是可控性最強的。用戶除了使用系統提供的引導類加載類來完成,也能夠由用戶自定義的類加載器去加載(重寫一個類加載器的loadClass())。 
  注意:數組類自己不經過類加載器建立,它是由JVM直接建立的。但數組類和類加載器仍有很緊密的關係,由於數組類的元素類型最終是靠類加載器去建立。 
  加載完成後,虛擬機外部的二進制字節流就按照虛擬機所需格式存儲在方法區之中,方法區中的數據存儲格式由虛擬機實現自行定義。而後在內存中實例化一個java.lang.Class類的對象(能夠在Java堆中,也能夠在方法區中),該對象將做爲程序去訪問方法區中的這些類型數據的外部接口。 
  加載階段與鏈接階段的部份內容(如一部分字節碼文件格式驗證動做)是交叉進行的,加載階段還沒有結束,鏈接階段就可能開始了。可是夾在加載階段進行的動做,仍然屬於鏈接階段的內容。
指針

驗證

  驗證是鏈接的第一步,目的是爲了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,而且不會危及虛擬機自己的安全。 
驗證階段的四個步驟:文件格式檢驗、元數據檢驗、字節碼檢驗、符號引用檢驗。
對象

  • 文件格式檢驗: 
    檢驗字節流是否符合Class文件格式的規範,而且能被當前版本的虛擬機處理。檢驗可能包含下列幾種:是否以魔數開頭、主次版本號是否在虛擬機的處理範圍以內,常量池中的常量是否不被支持、文件是否被刪除或附加什麼信息等等。 
    只有經過文件格式檢驗的二進制字節流才能進入內存的方法區進行存儲,因此後面的3個檢驗階段都是基於方法區的存儲結構進行的,不會在操做字節流。
  • 元數據檢驗: 
    對字節碼描述的信息進行語義分析,以保證其描述的內容符合Java語言規範的要求。 
    驗證點包括:是否有父類(除了object)、父類是否繼承了不可被繼承的類(被final修飾的類)、若是這個類不是抽象類,是否實現了其父類或接口之中要求實現的全部方法、類中的方法和字段是否與父類產生矛盾(覆蓋了父類的final字段、出現不合規矩的方法重載等)。 
    元數據檢驗主要是對類的元數據信息進行語義校驗,保證不符合Java語言規範的元數據信息不存在。
  • 字節碼檢驗: 
    經過數據流和控制流分析,肯定程序語義是合法、符合邏輯的。第二階段是對元數據信息中的數據類型作了檢驗,這一階段是對類的方法體進行校驗分析,保證被校驗類的方法在運行時不會作出危害虛擬機安全的事情。 
    檢驗點包括:保證任意時刻操做數棧的數據類型與指令代碼序列都能配合工做、保證指令跳轉不會跳轉到方法體以外的地方、保證方法體內的類型轉換都是有效的。 
    事實上,即使是通過字節碼檢驗後的方法體也不必定是安全的。
  • 符號引用檢驗 
    最後一個檢驗發生在虛擬機將符號引用轉化爲直接引用時,這個轉化動做將在鏈接的第三階段–解析階段中發生的。符號引用檢驗能夠看做是對類自身之外(常量池中的各類符號引用)的信息進行匹配性校驗。 
    校驗點:符號引用中經過字符串描述的全限定名是否能找到對應的類、在指定類中是否存在符合方法的字段描述符以及簡單名稱所描述的方法和字段、符號引用中的類、字段、方法的訪問權限是否能讓當前類訪問到等。 
    符號引用檢驗的目的是確保解析動做的正常執行,若是沒法經過符號引用檢驗,將會拋出java.lang.IncompatibleClassChangeError異常的子類,如IllegalAccessError、NoSuchfiledError、NoSuchMethodError等。

準備

  準備階段是正式爲類變量分配內存並設置類變量初始值的階段。這些變量所使用的內存將在方法區中進行分配。此時進行內存分配的僅包括類變量(被static修飾的變量),而不包括實例變量,實例變量將會在對象實例化時隨着對象一塊兒分配在Java堆中。另外,在這裏分配的靜態類變量是將其值定義爲0等默認值,而不是咱們定義的。由於這時還沒有執行任何Java方法,咱們定義的賦值的putStatic指令是程序被編譯後,存放在類構造器()方法中,因此正確的賦值將在初始化階段執行。 
  若是類變量被final修飾,那麼在這種狀況下,在編譯時Javac將會爲該變量生成ConstantValue屬性,在準備階段虛擬機會根據該屬性設置類變量的正確值。
繼承

解析

  解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程。 
a、符號引用:以一組符號來描述所引用的目標,符號能夠是任何形式字面量,只要使用時無歧義地定位到目標就行。 
b、 直接引用:直接引用是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄。引用的目標已經在內存中存在。 
  虛擬機實現能夠根據須要來判斷到底在類被加載器加載時就對常量池中的符號引用進行解析,仍是等到一個符號引用將要被使用時纔去解析它。解析動做主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號引用進行。

初始化

類加載的最後一步,真正執行類中定義的Java程序代碼(字節碼)。 初始化階段是執行類構造器()方法的過程,根據程序員經過程序制定的主觀的計劃去初始化類變量和其餘資源。

相關文章
相關標籤/搜索