JVM实战与原理---类加载机制
JVM實戰與原理
目錄
類加載機制
1. 類加載生命周期
1.1 加載
1.2 驗證
1.3 準備
1.4 解析
1.5 初始化
2. 類加載器
類加載機制
章節目的:了解虛擬機如何加載Class文件?Class文件的信息進入到虛擬機后會發生什么變化
引言:在了解了Class文件結構后,我們就會想知道Class文件是怎么被虛擬機加載的
下面便是類加載機制的介紹。
1. 類加載生命周期
類的加載生命周期分為七步,加載->驗證->準備->解析->初始化->使用->卸載
1.1 加載
加載階段,虛擬機需完成以下三件事:
1. 通過一個類的全限定名來獲取定義此類的二進制字節流,比如java.lang.Object就是一個類的全限定名
2. 將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構
3. 在內存中生成一個代表這個類的java.lang.Class對象,作為方法區這個類的各種數據的訪問入口
1.2 驗證
驗證階段是為了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,并且不會危害虛擬機本身。驗證階段,虛擬機會完成下面4個校驗動作
1. 文件格式驗證:該階段主要為了保證輸入的字節流能正確地解析并存儲于方法區之內,大致包含:
是否以CAFEBABE開頭、版本號是否在JVM處理范圍內、常量池tag標志是否支持、索引指向異常、編碼不符合規范等
2. 元數據驗證:該階段主要為了對類的元數據信息進行語義校驗,大致包含:
是否有父類、是否繼承了不允許繼承的類、非抽象類是否實現所有方法、是否與父類沖突等
3. 字節碼驗證:該階段主要為了對類的方法體進行校驗分析,保證類方法在運行時不會做出傷害虛擬機安全的事件,大致包含:
操作數棧的數據類型與指令代碼序列能配合工作、跳轉指令不會跳轉到方法體以外的字節碼指令上、類型轉換是有效的等
4. 符號引用驗證:該階段主要為了確保解析動作能正常執行,大致包含:
符號引用中通過字符串描述的全限定名是否能找到對應的類、符號引用中的類、字段、方法的訪問性是否可被當前類訪問等
1.3 準備
準備階段是正式為類變量分配內存并設置類變量初始值的階段,變量所使用的內存都將在方法區中進行分配。有兩點需強調
1. 此時內存分配的僅包括被static修飾的變量,實例變需等對象實例化時隨著對象一起分配在堆中
2. 初始值是數據類型的零值,比如static int value = 123;初始值是0而不是123。如果字段屬性表中存在ConstantValue屬性,那么準備階段變量value就會被初始化為ConstantValue屬性所設置的值,如static final int value = 123;則初始值就是123
1.4 解析
解析階段是虛擬機將變量池內的符號引用替換為直接引用的過程。解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符 7類符號引用進行。
分別對應CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info、CONSTANT_MethodType_info、CONSTANT_MedhodType_info、CONSTANT_MethodHandle_info、CONSTANT_InvokeDynamic_info。我們抽幾個來講
1.4.1?類或接口的解析
假設有一個D類,需將符號引用N解析為一個類或接口C的直接引用,需完成以下3步
1 如果C不是數組,虛擬機會把代表N的全限定名傳遞給D的類加載器去加載類C
2 如果C是數組,并且數組的元素類型為對象,N的描述符會是類似Ljava/lang/Integer形式,會按照1.1加載數組元素類型(java.lang.Integer),接著虛擬機生成一個代表次數組維度和元素的數組對象
3 經過上面兩步,C在虛擬機已經此成為一個有效的類或接口,接著進行符號引用驗證,確認D是否具備對C的訪問權限,不具備權限則拋出java.lang.IllegalAccessError異常。
1.4.2?字段解析
要解析字段符號引用,需現對字段表內class_index項中索引的CONSTANT_Class_info符號引用解析,如解析異常,則字段符號引用解析也會失敗。字段解析分四步
假設字段所屬的類或接口用C表示
1 如果C本身就包含簡單名稱和字段描述符都與目標匹配的字段,則返回這個字段的直接引用,查找結束
2 否則,如果在C中實現了接口,會按照繼承關系從下往上遞歸搜索各個接口和它的父接口,如果接口中包含了簡單名稱和字段描述符都與目標相匹配的字段,則返回這個字段的直接引用,查找結束
3 否則,如果C不是java.lang.Object的話,講會按照繼承關系從下往上遞歸搜索其父類,如果在父類中包含了簡單名稱和字段描述符都與目標相匹配的字段,則返回這個字段的直接引用,查找結束
4 否則,查找失敗,拋出java.lang.NoSuchFieldError異常。如果查找成功返回了引用,將會對這個字段進行權限驗證,如不具備訪問權限,則拋出java.lang.IllegalAccessError異常。
1.4.3?類方法解析
類方法解析的第一個步驟與字段解析一樣,需要先解析出類方法表的class_index項中索引的方法所屬的類或接口的符號引用。類方法解析分五步
假設C表示這個類
1 如果在類方法表中發表class_index中索引的C是個接口,就直接拋出java.lang.IncompatibleClassChangeError異常
2 在類C中查找是否有簡單名稱和描述符都與目標相匹配的方法,如果有則返回這個方法的直接引用,查找結束。
3 否則,在類C的父類中遞歸查找是否有簡單名稱和描述符都與目標相匹配的方法,如果有則返回這個方法的直接引用,查找結束。
4 否則,在類C實現的接口列表及它們的父接口之中遞歸查找是否有簡單名稱和描述符都與目標相匹配的方法,如果存在匹配的方法,說明C是一個抽象類,這時查找結束,拋出java.lang.AbstractMethodError異常。
5 否則,宣告方法查找失敗,拋出java.lang.NoSuchMethodError。
如果查找過程返回了直接引用,會對方法進行權限驗證,如不具備訪問權限,則拋出java.lang.IllegalAccessError異常。
1.4.4?接口方法解析
接口方法也需要先解析接口方法表的class_index項中索引的方法所屬的類或接口的符號引用。接口方法解析分四步
假設C表示這個類
1?如果在類方法表中發表class_index中索引的C是個類而不是接口,就直接拋出java.lang.IncompatibleClassChangeError異常
2 在接口C中查找是否有簡單名稱和描述符都與目標相匹配的方法,如果有則返回這個方法的直接引用,查找結束。
3 否則,在接口C的父接口中遞歸查找,直到java.lang.Object類為止,看是否有簡單名稱和描述符都與目標相匹配的方法,如果有則返回這個方法的直接引用,查找結束。
4 否則,宣告方法查找失敗,拋出java.lang.NoSuchMethodError。
因為接口方法默認都是public的,所以不存在訪問權限的問題。
1.5 初始化
初始化階段會根據程序去初始化變量和其他資源,即執行類構造器<clinit>方法的過程,<clinit>方法有以下特點
1. <clinit>方法是由編譯器收集類中所有類變量的賦值動作和靜態語句塊中的語句合并產生的
2. <clinit>方法與類的狗喊函數<init>方法不同,不需要顯式調用父類構造器,虛擬機會保證在子類<clinit>執行前,父類<clinit>已執行完成
3. 由于父類<clinit>先執行,故父類定義的靜態語句塊要優先于子類
4. 如果一個類沒有靜態語句塊,也沒有對變量的賦值操作,那么編譯器可以不生車給<clinit>方法
5. 接口沒有靜態語句塊,但又類變量賦值,因此接口也會生成<clinit>方法。接口<clinit>方法不需要先執行父接口的<clinit>方法,接口實現類初始化時也不會執行接口的<clinit>方法
6. 虛擬機會保證<clinit>方法在多線程環境被正確地枷鎖、同步,如果多個線程同時初始化一個類,指揮有一個線程執行類的<clinit>方法,其他線程都阻塞等待,知道<clinit>方法執行完畢。如果類的<clinit>方法耗時長,則可能導致多個進程阻塞。
2. 類加載器
作用:實現類的加載動作,同時類本身與加載它的類加載器一同確定在Java虛擬機中的唯一性。
比如使用equals方法,instanceof關鍵字時,必須保證同一個Class文件,被同一個虛擬機,用同樣的類加載器加載,兩個類才是相等的。
雙親委派模型
如果我們自己新寫一個java.lang.Object類放入ClassPath中,此時,虛擬機會使用rt.jar包里的Object作為運行類還是我們新寫的Object作為運行類呢?
答案是虛擬機還是使用rt.jar包中的Object類,而我們新寫的Object類永遠無法被加載運行,這樣保證了Java程序的穩定運行。
那么為什么虛擬機會去加載rt.jar包里的Object類呢?答案就是雙親委派模型。
雙親委派模型是什么:
雙親委派模型將類加載器分為四層,除了頂層,其余類加載器都有自己的父類加載器。頂層類加載器由C++實現,其余由Java實現。
啟動類加載器:頂層類加載器,負責將存放在<JAVA_HOME>\lib目錄或被-Xbootclasspath參數所指定路徑下,固定文件名(如rt.jar)的類庫加載到虛擬機內存中。
擴展類加載器:第二層類加載器,負責將存放在<JAVA_HOME>\lib\ext目錄或被java.ext.dirs系統變量所指定的路徑下的類庫加載到虛擬機內存中
應用程序類加載器:第三層類加載器,負責將存放在用戶類路徑(ClassPaht)目錄下的類庫加載到虛擬機內存中
自定義類加載器:第四層加載器,由用戶編寫的類加載器
雙親委派模型的工作過程:
如果一個類加載器收到了類加載的請求,它不會自己先去加載這個類,而是把請求委派給父類請求加載器完成,因此所有加載請求最終都會送到頂層的啟動類加載器,只有當父加載器反饋自己無法完成這個加載請求時(搜索范圍中沒有找到所需的類),子加載器才會嘗試自己去加載。
總結
以上是生活随笔為你收集整理的JVM实战与原理---类加载机制的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java数据库篇7——数据库设计
- 下一篇: JVM实战与原理---字节码执行引擎