JVM
類加載
JVM整個流程圖

一個java檔案被編譯為class檔案后,剩下的操作都交給jvm來執行,其中第一步就是將class檔案加載到jvm,而這一步就是由類加載器來完成的
類加載的流程又分為加載(Loading),驗證(Verification),準備(Preparation),決議(Resolution),初始化(Initialization)
而其中驗證,準備,決議這三步統稱為連接(Linking)

類加載器只負責加載class檔案,至于加載的class是否能正常執行則是由執行引擎決定
加載
這里的加載只是類加載器中的一步操作,也叫加載而已,這一步完成三個事情
- 通過一個類的全限定名來獲取定義此類的二進制位元組流
- 通過這個位元組流所代表的靜態存盤結構轉化為方法區的運行時資料結構
- 在記憶體中生成一個所代表這個類的java.long.Class物件,作為方法區這個類的各種資料的訪問入口
其中這三步在java虛擬機規范中并沒有要求特別具體,java虛擬機實作的靈活度相當大,例如第一步中獲取二進制位元組流
- 從zip壓縮包中讀取,最終成為日后的jar,war檔案格式的基礎
- 從網路中讀取,這種場景最經典的應用就是Web Applet
- 運行時計算生成,這種場景最多的就是動態代理技術
- 其他檔案生成,例如JSP應用,由JSP生成Class檔案
- ...
相對于類加載其他階段,非陣列型別的加載階段,加載階段中是開發人員可控性最強的階段,加載階段既可以使用java虛擬機內置的引導類加載
器來完成,也可以使用用戶自定義加載器來完成
連接
驗證
驗證是連接階段的第一步,目的是為了確保Class檔案的位元組流中包含的資訊符合java虛擬機規范的全部約束要求,保證這些資訊被當做代碼后運行不會對java虛擬機產生危害
而驗證有包含四種驗證,檔案格式驗證,元資料驗證,位元組碼驗證,符號參考驗證
檔案格式驗證
第一階段要確保位元組流符合Class檔案格式規范,并能被當前版本虛擬機處理,這一階段可能包含下面這些驗證點
- 是否以魔術 0xCAFEBABE 開頭
- 主,次版本號是否在當前java虛擬機接收范圍內
- 常連池的常量中是否有不被支持的常量型別(檢查常量的tag標志)
- ....
實際上第一階段的驗證點遠遠不止這些,上面所列出的只是從HotSpot虛擬機中的一小部分,該驗證階段的主要目的就是為了保證輸入的位元組流能正確的決議并存盤在方法區中,格式上符合描述一個java型別資訊的要求.
這個階段驗證是基于二進制位元組流進行的,只有通過這個階段的驗證過后,這段位元組流才會被允許進入java虛擬機記憶體的方法區中進行存盤,后面的三個驗證階段全部都是基于方法區中的存盤結構上進行的,不會再進行直接讀取,操作位元組流了
元資料驗證
第二階段是對位元組碼描述的資訊進行語意分析,以確保其描述的資訊符合java語言的規范,這個階段可能包括驗證點如下
- 這個類是否有父類 (除了java.lang.Object之外,所有的類都應該有父類)
- 這個類的父類是否繼承了不允許繼承的類 (被final修飾的類)
- 如果這個類不是抽象類,是否實作了其父類或介面之中要求實作的所有方法
- 類中的欄位,方法是否與父類產生矛盾 (例如覆寫父類final欄位,或者出現不符合規則的方法多載,例如方法引數一致,回傳型別卻不同)
- ...
位元組碼驗證
第三階段是整個驗證程序最復雜的一個階段,目的是通過資料類分析和控制流分析,確定程式語意是合法的,符合邏輯的,在第二階段對元資料資訊中的資料型別校驗完畢后,這個階段就是要對類的方法體(Class檔案中的Code屬性)進行校驗分析,保證被校驗類的方法在運行時不會出現對虛擬機危害的行為,例如
- 保證任意時刻運算元堆疊的資料型別與指令代碼序列都能配合作業,例如不會出現類似于"在操作堆疊放置一個int型別資料,使用時卻按照long型別來加載到本地變數表中"這樣的情況
- 確保任何跳轉指令都不會跳轉到方法體以外的位元組碼指令上
- 保證方法中型別轉換總是有效的,例如把一個子類物件賦值給父類資料型別,這是安全的,但是把父類物件賦值給子類資料型別,甚至賦值給與它沒有繼承關系的完全不相干的一個資料型別,則是危險和不合法的
- ...
如果一個型別中有方法體的位元組碼沒有通過位元組碼驗證,那么它肯定是有問題的,但是如果一個方法體通過了位元組碼驗證,也并不能保證它一定就是安全的,即使位元組碼驗證階段進行再大量,再嚴密的檢查,也不能保證這一點
符號參考驗證
最后一個階段的校驗行為發生在虛擬機將符號參考轉化為直接參考的時候,這個轉化動作將在連接的第三階段決議階段中發生,符號參考驗證可以看做是類自身以外(常連池中各種符號參考)的型別資訊進行匹配性校驗,通俗的來說就是檢查該類是否缺少或被禁止訪問它所依賴的外部某些外部類,方法,欄位等資源,本階段通常校驗下列內容
- 符號參考中通過字串描述的全限定名是否能找到對應的類
- 在指定類中是否存在符合方法的欄位描述符及簡單名稱描述的方法和欄位
- 符號參考中的類,欄位,方法的可訪問性,(private,protected,public,< package >)是否可被當前類訪問
- ...
符號參考驗證主要目的就是確保決議行為能正常運行,如果無法通過符號參考驗證,java虛擬機將拋出一個java.lang.IncompatibleCLassChangeError的子類例外
驗證階段對于虛擬機的類加載機制來說,是非常重要的,但卻不是必須執行的階段,因為驗證階段只有通過或者不通過的區別,只要通過了驗證,其后就對程式運行期沒有任何影響了,如果程式運行的全部代碼都已經被反復使用和驗證過,在生成環境的實施階段可以考慮使用-Xverify:none引數來關閉大部分的類驗證措施,以縮短虛擬機類加載的時間
準備
準備階段是正式的為類中定義的變數(即靜態變數,被static修飾的變數),分配記憶體并設定變數的初始值的階段
關于準備階段,首先是這時候進行記憶體分配僅包括類變數,而不是實體變數,實體變數將會在物件實體化時隨著物件一起分配在java堆中
在這個階段中,所有的基本型別都會被賦值為零值
? 基本資料型別的零值
| 資料型別 | 零 值 |
|---|---|
| int | 0 |
| long | 0L |
| short | (short) 0 |
| char | '\u0000' |
| byte | (byte) 0 |
| boolean | false |
| float | 0.0f |
| double | 0.0d |
| reference | null |
這里不包含final修飾的static,因為final在編譯期間就已經分配值了,準備階段會顯式初始化
決議
決議階段是java虛擬機將常量池內的符號參考替換為直接參考的程序
- 符號參考(Symbolic Reference):符號參考以一組符號來描述所參考的目標,符號參考可以是任何形式的字面兩,只要使用時能無歧義的定位到目標即可,符號參考于虛擬機實作的記憶體布局無關,符號參考的目標并不一定已經被加載到虛擬機記憶體中的記憶體,符號參考的字面量形式明確定義在java虛擬機規范的Class檔案格式中
- 直接參考(Direct Reference) 直接參考是可以直接指向目標的指標,相對偏移量,或者是一個能間接定位到目標的句柄,直接參考是和虛擬機記憶體布局相關的,同一個符號參考在不同虛擬機實體上翻譯出來的直接參考一般不會相同,如果有直接參考,那么參考的目標必須已經在虛擬機的記憶體中存在
決議動作一般針對類或介面,欄位,類方法,介面方法,方法型別等,對應常量池中的CONSTANT_Class_info,CONSTANT_Fieldref_info
CONSTANT_MethodHandle_info等
初始化
類的初始化時類加載的最后一個步驟,進行準備階段時,變數已經賦值過一次系統要求的零值,而在初始化階段,則會根據程式員通程序式編碼制定的主觀計劃去初始化類變數和其他資源,初始化階段其實就是執行類構造器clinit方法的程序,clinit并不是程式員在java代碼中撰寫的方法,它是由javac編譯器的自動產生物
clinit方法是由編譯器自動收集類中所有類變數賦值(靜態的變數)的動作和靜態陳述句塊 static{} 塊中的合并產生的,收集的順序是由陳述句在源檔案中出現的順序決定的
靜態陳述句塊中只能訪問到定義靜態陳述句塊之前的變數,而定義在它之后的變數,可以賦值,不能訪問
public class JvmDemo{
static{
//在定義之前可以進行賦值
count=4;
//但是不能進行訪問,這句代碼報錯,非法向前參考
System.out.println(count);
}
private static int count=1;
}
clinit方法和類的建構式(即在虛擬機視角中的實體構造器init方法)不同,它不需要顯示呼叫父類構造器,java虛擬機會保證在呼叫子類clinit方法前,父類的clinit方法已經執行完畢,因此java虛擬機中第一個執行的clinit方法的型別一定是java.long.Object
clinit方法對于類來說并不是必須的,如果類中沒有對類變數賦值的操作,同時也沒有靜態代碼塊,那么編譯器可以不為這個類生成clinit方法
介面中不能使用靜態陳述句塊,但仍然有變數賦值的操作,因此介面與類一樣都會生成clinit方法,但與介面不同的是,執行介面clinit方法不需要先執行父介面的clinit方法,因為只有父介面中定義的變數被使用時,父介面才會被實體化,此外,介面的實作類初始化時也一樣不會執行介面中的clinit方法
java虛擬機必須保證一個類的clinit方法在多執行緒情況下能夠正確的加鎖同步,如果有多個執行緒同時初始化一個類,那么只會有其中一個執行緒執行類的clinit方法,其他執行緒都需要阻塞等待,知道活動執行緒執行完畢clinit方法
類加載器
從java虛擬機角度來看,只存在兩種類加載器器:一種啟動類加載器(Bootstrap ClassLoader),這個類加載器使用C++實作,是虛擬機的一部分,另一種就是其他所有的類加載器,這些都由java實作,獨立于虛擬機外部,并且全部繼承于java.lang.ClassLoader
而從程式角度來劃分了類加載器
啟動類加載器(引導類加載器,Bootstrap ClassLoader)
使用C++實作,嵌套在JVM內部,這個類加載器用來負責加載存放在<JAVA_HOME>\lib目錄,用于加載JVM自身需要的類,并不繼承java.lang.ClassLoader,沒有父加載器,加載應用類加載器和擴展類加載器,并指定為它們的父類加載器
擴展類加載器(Extension ClassLoader)
這個類加載器在類sun.misc.Launcher$ExtClassLoader中以java代碼實作,它負責加載<JAVA_HOME>\lib\ext目錄中,或被java.ext.dirs系統變數指定的路徑中所有類別庫
程式類加載器(系統類加載器 Application ClassLoader)
這個類加載器由sun.misc.Launcher$AppClassLoader實作,負責加載用戶類路徑ClassPath上所有類別庫,父類加載器為擴展類加載器,該類是程式中的默認加載器,一般情況java應用的類都是由它來完成,通過ClassLoader#getSystemClassLoader()方法可以獲取到該類加載器
繼承關系

ClassLoader
常用方法
- Class loadClass(String name) :name引數指定類裝載器需要裝載類的名字,必須使用全限定類名,如:com.smart.bean.Car,該方法有一個多載方法 loadClass(String name,boolean resolve),resolve引數告訴類裝載器時候需要決議該類,在初始化之前,因考慮進行類決議的作業,但并不是所有的類都需要決議,如果JVM只需要知道該類是否存在或找出該類的超類,那么就不需要進行決議,
- Class defineClass(String name,byte[] b,int len):將類檔案的位元組陣列轉換成JVM內部的java.lang.Class物件,位元組陣列可以從本地檔案系統、遠程網路獲取,引數name為位元組陣列對應的全限定類名,
- Class findSystemClass(String name):從本地檔案系統在來Class檔案,如果本地系統不存在該Class檔案,則拋出ClassNotFoundException例外,該方法是JVM默認使用的裝載機制
- Class findLoadedClass(String name):呼叫該方法來查看ClassLoader是否已載入某個類,如果已載入,那么回傳java.lang.Class物件;否則回傳null,如果強行裝載某個已存在的類,那么則拋出鏈接錯誤,
- ClassLoader getParent():獲取類裝載器的父裝載器,除根裝載器外,所有的類裝載器都有且僅有一個父裝載器,ExtClassLoader的父裝載器是根裝載器,因為根裝載器非java語言撰寫,所以無法獲取,將回傳null,
雙親委派機制

雙親委派模型的作業程序:當接收到類加載請求時,類加載器首先會委托父類加載器(注意這里說的父類一般不是繼承關系,而是通常使用組合關系來復用父類加載器的代碼)進行加載,每一層都是如此,因此所有類加載請求最后都會到達啟動類加載器(Bootstrap ClassLoader),只有父類加載器無法完成這個加載請求時(在它的搜索范圍內沒有找到所需的類),子類加載器才會嘗試自己去完成加載
使用雙親委派模型來組織類加載器之間的關系,一個顯而易見的好處就是java中的類隨著它的類加載器一起具備了一種帶有優先級的層次關系
例如java.lang.Object,它存放在rt.jar中,無論哪一個類加載器要加載這個類,最終委托還是派給模型中處于頂端的類加載器進行加載,因此Object類在程式的各種類加載器環境中都能保證是同一個類,同時雙親委派模型也可以保證java核心API的安全性,例如自己也在專案中創建一個java.lang.String,如果加載了自定義的String類,程式將變得十分混亂
雙親委派模型實作全部集中在loadClass()方法中
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
//首先檢查請求的類是否已經被加載過了
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//如果拋出ClassNotFound例外說明父類無法加載這個類
//那么將呼叫當前類的findClass()來加載
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
long t1 = System.nanoTime();
c = findClass(name);
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
例如下面自定義一個String,全限定名為java.lang.String
package java.lang;
/**
* @author : Jame
* @date : 2021-01-25 13:49
**/
public class String {
public static void main(String[] args) {
System.out.println("自定義String");
}
}
運行出現下面錯誤

原因就是類加載器向上委托直到引導類加載器,引導類加載器發現可以加載java.lang下的類,于是加載了java自帶的String類,之后呼叫java自帶的String類的main方法,發現沒有該方法,于是拋出例外
其他
在JVM中判斷兩個類是否相同有兩個條件
- 兩個類的全限定名一致
- 加載這兩個類的類加載器(ClassLoader實體物件)必須一樣
即使兩個類的類物件(Class物件)來源于同一個class檔案,被同一個的類加載器加載,只要加載它們的ClassLoader物件實體不同,那么這兩個類物件也是不相等的
對類加載器的參考
JVM必須知道一個型別是由啟動加載器還是用戶類加載器加載的,如果一個型別是用戶類加載器加載的,那么JVM會將這個類加載器的一個參考型別資訊的一部分保存在方法區中,當決議一個型別到另一個型別的參考的時候,JVM需要保證這兩個型別的類加載器時相同的
類的使用和被動使用
java程式對類的使用方式分為:主動使用和被動使用
-
主動使用,分為七種情況:
-
創建類的實體
-
訪問某個類或介面的靜態變數,或者對靜態變數賦值
-
呼叫類的靜態方法
-
反射 (例如:Class.forName("com.jame.Test"))
-
初始化一個類的子類
-
Java虛擬機啟動時被標明為啟動的類
-
JDK7 開始提供的動態語言支持:
java.lang.invoke.MethodHandle實體的決議結果
REF_getStatic,REF_putStatic,REF_invokeStatic句柄對應類沒有初始化,則初始化
-
-
除了以上七種情況,其他事宜Java類的方式都被看做為對類的被動使用,都不會導致類的初始化
這里的類的初始化指的是類加載中3大步驟中的最后一步初始化步驟
本文僅個人理解,如果有不對的地方歡迎評論指出或私信,謝謝?(?>?<?)?
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/252434.html
標籤:Java
