前言
虛擬機把描述類的數據從Class文件加載到內存,並對數據進行校驗、轉換解析和初始化,最終形成可以被虛擬機直接使用的Java類型,這個過程就稱爲JVM的類加載機制。今天我們主要從下面兩個方面說下類加載:類加載時機和類加載過程。
類加載時機
類從被加載到虛擬機內存中開始,到卸載出內存爲止,它的整個生命週期包括:加載、驗證、準備、解析、初始化、使用和卸載7個階段,其中驗證+準備+解析3個階段稱爲連接。
加載、驗證、準備、初始化和卸載5個階段的順序是確定的,類加載過程必須按這5個順序開始,解析階段則不一定,在某些情況下可以在初始化階段之後再開始,這是爲了支持Java的運行時綁定;
什麼時候對類進行初始化?
-
遇到new、getstatic、putstatic或者invokestatic這4條字節碼指令時,如果類沒有進行初始化,則需要先觸發其初始化(例如使用new關鍵字實例化對象,調用一個類的靜態方法);
-
使用java.lang.reflect包的方法對類進行反射調用的時候;
-
初始化一個類的時候,如果發現其父類還沒有進行初始化,則需要先觸發其父類的初始化;
-
當虛擬機啓動時,用戶需要指定一個要執行的主類(包含main方法的類),虛擬機會先初始化這個主類;
-
當使用JDK1.7的動態語言支持時,如果一個java.lang.invoke.MethodHandle實例最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,並且這個方法句柄對應的類沒有進行過初始化,則需要先觸發其初始化;
上述五種場景行爲稱爲對一個類進行主動引用,除此之外,所有的引用類的方式都不會觸發初始化,稱爲被動引用。
接口的加載過程與類加載過程有一些不同,主要體現在上述第三點:當一個類在初始化時,要求其父類全部都已經初始化過,但是接口在初始化時,並不要求其父接口全部完成初始化,只有在真正使用到父類接口的時候(如引用接口中定義的常量)纔會初始化。
類加載過程
加載:加載是類加載過程的一個階段,在加載階段,虛擬機要完成以下3個階段
-
通過一個類的全限定名來獲取定義此類的二進制字節流
-
將這個字節流所代表的靜態存儲結構轉化爲方法區的運行時數據結構
-
在內存中生成一個代表這個類的java.lang.Class對象,作爲方法區這個類的各種數據訪問入口
對於數組類而言,情況有所不同,數組類本身不通過類加載器創建,它是由Java虛擬機直接創建。而且數組類的元素類型最終是要靠類加載器去創建,一個數組類創建過程需要遵循以下規則:
-
如果數組的組件類型是引用類型,那就遞歸採用加載過程去加載這個組件類型,該數組將在加載該組件類型的類加載器的類名稱空間上被標識
-
如果數組的組件類型不是引用類型,Java虛擬機將會把數組標記爲與引導類加載器關聯
-
數組類的可見性與它的組件類型的可見性一致,如果組件類型不是引用類型,那數組類的可見性將默認爲public
加載階段與連接階段的部分內容是交叉進行的,加載階段尚未完成,連接階段可能已經開始,但是這兩個階段的開始時間仍然保持着固定的先後順序。
驗證:驗證是連接的第一步,這一步階段的目的是爲了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身安全;
驗證階段大致上會完成下面4個階段的檢驗動作:
-
文件格式驗證:第一階段主要驗證字節流是否符合Class文件格式的規範,並且能被當前版本的虛擬機處理,該驗證階段的主要目的是保證輸入的字節流能正確的解析並存儲與方法區之內,格式上符合描述一個Java類型信息的要求。該階段的驗證都是基於二進制字節流進行的,通過了該階段驗證,字節流纔會進入內存的方法區中進行存儲,後續階段全部是基於方法區的存儲結構進行的,不會再直接操作字節流;
主要驗證點如下:
-
是否以魔數(起始的幾個字節的內容是固定的)開頭
-
主、次版本號是否在當前虛擬機處理範圍之內
-
常量池的常量中是否有不被支持的常量類型
-
指向常量的各種索引值是否有指向不存在的常量或不符合類型的常量
-
Class文件中各個部分及文件本身是否有被刪除的或附加的其他信息
-
... ...
-
-
元數據驗證:第二階段是對字節碼描述的信息進行語義分析,以保證其描述的信息符合Java語言規範的要求,該階段的主要目的是對類的元數據信息進行語義校驗,保證不存在不符合Java語言規範的元數據信息
主要驗證點如下:
-
該類是否有父類
-
該類的父類是否繼承了不允許被繼承的類(final修飾的類)
-
如果這個類不是抽象類,是否實現了其父類或接口之中要求實現的所有方法
-
類中的字段、方法是否與父類產生矛盾
-
... ...
-
-
字節碼驗證:該階段的目的是通過數據流和控制流分析,確定程序語義是合法的,符合邏輯的,在上個階段完成對元數據信息中的數據類型做完校驗後,這個階段將對類的方法體進行校驗分析
-
符號引用驗證:該階段的校驗發生在虛擬機將符號引用轉化爲直接引用的時候,該階段可以看作是對類自身以外的信息進行匹配性校驗,符號引用的目的是確保解析動作能正常執行,該階段主要需要校驗下列內容:
-
符號引用中通過字符串描述的全限定名是否能找到對應的類
-
在指定類中是否存在符合方法的字段描述符以及簡單名稱所描述的方法和字段
-
符號引用中的類、字段、方法的訪問性是否可被當前類訪問
-
準備:該階段是正式爲類變量分配內存並設置類變量初始值的階段,這些變量所使用的內存都將在方法區中進行分配。
解析:解析階段是虛擬機將常量池內的符號引用替換爲直接引用過程,那麼直接引用和符號引用又有啥關聯呢?
符號引用:以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用的時候能無歧義定位到目標即可,符號引用與虛擬機內存佈局無關,引用的目標並不一定已經加載到內存中;
直接引用:可以是直接指向目標的指針、相對偏移量或者是一個能間接定位到目標的句柄,直接引用和虛擬機內存佈局相關,同一個符號引用在不同虛擬機實例上翻譯出來的直接引用一般不會相同,如果有直接引用,那麼引用的目標一定已經存在於內存中;
解析的動作主要針對下面幾個方面進行:
-
類或者接口
-
字段解析
-
類方法解析
-
接口方法解析
-
方法類型
-
方法句柄
-
調用點限定符
初始化:類初始化是類加載的最後一步,這一步纔是真正開始執行類中定義的Java代碼或者說是字節碼,此階段根據程序員通過程序制定的主觀計劃去初始化類變量和其他資源,或者可以理解爲:初始化階段是執行類構造器<clinit>()方法的過程。那麼我們也簡單瞭解下<clinit>()方法的特點和細節:
-
<clinit>()方法是由編譯器自動收集類中的所有類變量的賦值動作和靜態語句塊中的語句合併產生的
-
<clinit>()與類構造函數不同,它不需要顯式的調用父類構造器,JVM保證在子類<clinit>()方法執行之前,父類的<clinit>()已經執行完畢
-
因爲父類的<clinit>()先執行,也就是說父類中定義的靜態語句塊要優先於子類的變量賦值操作
-
<clinit>()對於類和接口來說不是必需的,如果一個類沒有靜態語句也沒有對變量的賦值操作,那麼就不會對該類生成<clinit>()
-
接口中不能使用靜態語句塊,但仍然有變量初始化的賦值操作,因此接口也會生成<clinit>(),但是與類不同的是,接口的<clinit>()方法不需要先執行父類的<clinit>(),只有當父類中定義的變量被使用時,父接口才會初始化
-
JVM保證一個類的<clinit>()在多線程下能被正確的加鎖、同步、如果多線程同時去初始化一個類,那麼只會又一個線程去執行這個類的<clinit>()方法,其他線程阻塞等待,直到<clinit>()執行完畢
使用和卸載就不用多說了
以上就是我們今天所說的類加載時機和類加載過程,希望讀者對類加載有個初步的認識