深入理解jvm-2Edition-虛擬機類加載機制

Lqblalala 2021-08-15 13:48:03 阅读数:839

本文一共[544]字,预计阅读时长:1分钟~
深入 入理 理解 jvm-2edition- jvm

1、概述-什麼是類加載

將Class文件從其他地方(外存、字節流甚至是網絡流中)載入內存,

並對其中數據進行校驗、轉換解析和初始化,最終從其中提取出能够被虛擬機使用的Java類型。

用圖紙造模子,該模子能够用於生產對象。

運行時再進行類型的加載、鏈接和初始化雖然帶來了一些性能上的影響,

但是也使得Java可以動態擴展。這也是反射等特性的支撐。

類的生命周期:(宏觀上的,具體可能會相互交叉嵌套)

1、加載(載入內存,真正被虛擬機看見)

2、驗證(格式、內容邏輯)

3、准備

4、解析

5、初始化

6、使用

7、卸載

2、3、4也被統稱為鏈接階段。

2、什麼時候要進行類加載?

虛擬機規範裏面沒有規定何時加載,只確定了這五種情况要初始化(那就肯定要先加載啦):

1、遇到new、getstatic、putstatic或invokestatic字節碼指令時,如果類沒有進行過初始化,則要出發其初始化。

就是使用new實例化、訪問靜態字段/方法時。

2、使用java.lang.reflect包的方法對類進行反射調用時。如果沒有初始化,也要觸發初始化。

3、初始化一個類,但是其父類沒有初始化過,也要先對父類進行初始化。

父類初始化一定在子類之前,Object類是最先初始化的)。

4、虛擬機啟動時,需指定要執行的主類,虛擬機會先初始化主類

5、使用動態語言支持時,如果一個java.lang.invoke.MethodHandle實例

解析結果是REF_getStatic、REF_putStatic、REF_invokeStatic方法句柄

並且該句柄對應的類沒有初始化過,那麼就要初始化。

以上五種行為稱為對一個類進行主動引用,當且僅當這些情况會出生發初始化。

如以下情况不是主動引用(那就是被動引用囉):

1、子類引用父類的靜態字段不會初始化子類,但是會初始化父類。

2、通過數組定義引用類,不會出發被引用類的初始化

因為類和引用該類創建的數組不是一個類虛擬機會生成一個直接繼承自Object的類來錶示數組。

創建指令為newarray。該類封裝了對數組的訪問,而不是向C/C++一樣直接去操縱指針,因此更安全

3、對類的編譯時常量(static final修飾,並且值在編譯時可以確定的字段)的訪問不會導致初始化

因為編譯時常量會直接存入類的常量池中,對它的訪問本質上沒有引用到定義它的類。

接口的加載過程與類的加載過程有一點不同,接口也有初始化過程,

但是接口初始化時不要求其父接口都完成了初始化。父接口只有在真正用到時才被初始化。

3、深入類加載過程

1、加載

加載是類加載過程的一個階段,在這個階段需要完成三件事:

1、通過類的全限定名來獲取此類的二進制流

沒有說從哪裏獲取,那就大有可為了,

可以從Jar包、網絡、由其他文件生成(JSP)、數據庫中讀取、甚至運行時生成(動態代理)。

2、將二進制流中錶示的靜態的存儲結構轉化為方法區中的運行時數據結構

3、在內存中(具體是堆還是方法區由JVM具體實現决定)生成一個代錶此類的java.lang.Class對象

此對象作為方法區中的數據的訪問入口

但是如果是數組類呢?數組類是由JVM直接創建的,

但是畢竟還是要用到最內層的元素類型(Element Type)的類,所以與類加載器由密切關系。

數組的創建過程

1、如果該數組類的組件類型(Component Type,指該數組去掉一個維度的類型)

引用類型,那就遞歸的去加載這個組件類型

該數組類會和加載它的組件類型的類加載器關聯(類的唯一性由它本身和它的類加載器一起確定)。

2、如果組件不是引用類型,JVM會將該數組類和引導類加載器(Bootstrap ClassLoader)關聯。

3、數組類的可見性(訪問權限)和它的組件類型一致。

如果組件類型不是引用類型,那麼訪問權限默認為public。

加載階段和鏈接階段是交叉進行的,還有可能加載階段尚未完成,鏈接階段就已經開始了。

2、驗證

確保字節流中的內容是符合規範的,是JVM安全性的保證之一。

1、文件格式驗證

驗證字節流符合Class文件規範

包括:魔數、版本號、常量池常量類型、索引值的指向等。

2、元數據驗證

字節碼進行語義分析,保證其信息符合Java語言規範的要求。

包括:類是否有父類(唯一根類要求)、父類是否允許被繼承(繼承關系的正確性)、

非抽象類是否實現了其父類或接口中要求實現的所有方法(abstract方法)、類的字段是否沖突等。

3、字節碼驗證

通過數據流和控制流分析,對類的方法體進行校驗分析,確保被驗證類的方法沒有安全隱患。

JDK1.6後加入了StackMapTable屬性,描述了方法體中所有基本塊(Basic Block,按照控制流拆分的代碼塊)

開始時本地變量錶和操作數棧應有的狀態,用於輔助驗證。

包括:任意時刻操作數棧的數據類型是否和字節碼指令匹配,跳轉指令的跳轉比特置是否恰當, 類型轉換是否有效等。

4、符號引用驗證

發生在JVM將符號引用轉換為直接引用的時候,在解析階段中發生。對類自身以外的信息進行匹配項校驗。

包括:符號引用中全限定名是否能找到指定類、

類中描述符和簡單名描述的方法和字段是否存在、符號引用中的類、方法、字段是否能被訪問等。

3、准備

正式為類變量(static)分配內存及設定初始值。類變量使用的內存在方法區中分配

類變量的初始值就是把內存區域置零,除非類變量為編譯時常量。

編譯時常量會在字段屬性錶中有ConstantValue屬性記錄它在編譯期確定的值,此時它能被直接初始化為該值。

4、解析

將常量池中的符號引用替換為直接引用的過程。

符號引用Symbolic References:用符號來描述所引用的目標,

與虛擬機的內存布局無關,引用目標並不一定已經加載到內存中。

直接引用Direct Reference:直接指向目標所在地址的指針、相對偏移量或間接定比特的句柄。

和虛擬機內存布局相關

沒有規定解析階段發生的具體時間,但是在執行操縱符號引用的字節碼指令之前,先要對它們使用的符號引用進行解析

解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符等7類符號引用進行。

就是CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、

CONSTANT_InterfaceMethodref_info、CONSTANT_MethodType_info、

CONSTANT_MethodHandle_info、CONSTANT_InvokeDynamic_info 這7個常量類型。

1、類或接口解析 類D 要把符號引用N  解析為對類或接口C的直接引用

1、如果C不是數組類型,那麼JVM會將N中C的全限定名傳遞給D的類加載器,讓其去加載C。

加載過程可能觸發其他類或接口的加載,如C的父類或接口。

2、如果C是數組類型,並且C的元素類型為對象,也就是N類似於"[Ljava/lang/String"的形式,

那麼會以(1)的方式來加載元素類型,如"java.lang.String"。

接著JVM生成一個C類型和維度的數組對象。

3、上面兩部無异常,則會進行最後一步:符號引用驗證,驗證C能被D訪問(訪問權限)。

不滿足則拋出java.lang.IllegalAccessError异常。

2、字段解析

首先會對字段錶內class_index項中的索引CONSTANT_Class_info進行解析,確定字段所屬的類

用C錶示字段的類:

1、如果C中存在簡單名和字段描述符都與目標字段匹配的字段,那麼,返回該字段的直接引用,查找結束。

2、否則,到C的接口樹上找。找到(簡單名、字段描述符)則返回。

3、否則,到C的繼承鏈上找。找到(簡單名、字段描述符)則返回。

4、否則,找不到啦,直接報錯!java.lang.NoSuchFieldError

同樣,最後也要驗證訪問權限。不滿足則拋出java.lang.IllegalAccessError异常。

3、類方法解析

首先確定方法所屬的類,即對方法錶內class_index項中的索引CONSTANT_Class_info進行解析。

C錶示方法所屬類:

1、類方法和接口方法的符號引用的常量類型是分開的,

分別是CONSTANT_Methodref_infoCONSTANT_InterfaceMethodref_info

如果發現方法錶內class_index項中的索引CONSTANT_Class_info指向的是一個接口

則拋出java.lang.IncompatibleClassChangeError

2、否則,如果C中存在簡單名和方法描述符都與目標方法匹配的字段,

那麼,返回該方法的直接引用,查找結束。

3、否則,到C的接口樹上找。找到(簡單名、方法描述符)則返回。

4、否則,到C的繼承鏈上找。找到(簡單名、方法描述符)則返回。

5、否則,找不到啦,直接報錯!java.lang.NoSuchMethodError

同樣,最後也要驗證訪問權限。不滿足則拋出java.lang.IllegalAccessError异常。

4、接口方法解析

首先確定方法所屬的接口,即對方法錶class_index項中的索引CONSTANT_Class_info進行解析。

C錶示方法所屬接口:

1、類方法和接口方法的符號引用的常量類型是分開的,

分別是CONSTANT_Methodref_info和CONSTANT_InterfaceMethodref_info

如果發現方法錶內class_index項中的索引CONSTANT_Class_info指向的是一個

則拋出java.lang.IncompatibleClassChangeError

2、否則,如果C中存在簡單名和方法描述符都與目標方法匹配的字段,

那麼,返回該方法的直接引用,查找結束。

3、否則,到C的接口樹上找。找到(簡單名、方法描述符)則返回。

4、否則,找不到啦,直接報錯!java.lang.NoSuchMethodError

接口默認都是public的,不存在訪問問題,因此不會拋出java.lang.IllegalAccessError异常。

5、初始化

類加載的最後一步。到了初始化,才真正開始執行Java字節碼。

准備階段,類在分配類變量內存時被初始化了一次,那是為了讓類變量的初始值滿足系統要求。

初始化階段,是為了讓類變量(static)的初始值滿足程序員預先定義的初始值

初始化可以看作是執行類構造器<clinit>的過程。

<clinit>方法特點

1、<clinit>方法是由編譯器自動收集類中所有類變量的賦值動作和靜態語句塊(static{})

中的語句合並產生的。收集順序按在源文件中的出現順序决定。

靜態語句塊中,只能訪問到定義在該靜態語句塊之前的變量,之後的可以賦值,但不能訪問(不能取出值)。

2、與構造函數不同,<clinit>方法不用顯示調用父類構造器,

因為父類的初始化一定在子類方法之前完成,第一個執行<clinit>方法的類肯定是Object類。

3、由於父類的<clinit>方法先執行,因此父類的類變量和靜態語句中的語句先被執行。

4、<clinit>方法不是必須的

如果沒有類變量的賦值操做,也沒有靜態語句塊,那麼就不會生成<clinit>方法。

5、虛擬機要保證<clinit>方法在多線程環境下的線程安全性

因此,如果多個線程同時初始化一個類,那麼只有一個線程會去執行<clinit>方法。

如果<clinit>方法要耗費很長時間,則可能會造成多線程阻塞

4、類加載器

通過類的全限定名來獲取類的二進制字節流是放在虛擬機之外實現的,程序員可以自己决定怎麼去加載。

1、類和類加載器的關系

每一個類加載器都有一個獨立的類名稱空間

任意一個類,都要由它本身和加載它的類加載器一起來確定它在虛擬機中的唯一性。

因此,比較兩個類是否相等,要在它們都是同一個類加載器加載的才有意義。

2、雙親委派模型

從虛擬機角度看,只有兩種類加載器:

1、Bootstrap ClassLoader 啟動類加載器

JVM一部分,用於JVM啟動時的依賴類加載。

2、其他類加載器

不屬於JVM,都繼承自java.lang.ClassLoader

從開發人員角度:

1、Bootstrap ClassLoader 啟動類加載器

加載<JAVA_HOME>\lib目錄下,

或者被-Xbootclasspath參數指定的目錄下的虛擬機識別(僅按照文件名識別)的類庫。

啟動類加載器無法被Java程序直接引用。

2、Extension ClassLoader 擴展類加載器

加載<JAVA_HOME>\lib\ext目錄下,

或者被java.ext.dirs系統變量所指定的路徑下的所有類庫。

開發人員可直接使用。

3、Application ClassLoader 應用程序類加載器

也叫系統類加載器,因為它是ClassLoader類getSystemClassLoader() 方法的返回值。

負責加載用戶類路徑ClassPath上指定的類庫,開發人員可直接使用。

應用程序如果沒有自定義自己的類加載器,那麼默認就是這個。

雙親委派模型:

 

當一個類加載器收到加載請求時,它把請求委托給它的父類加載器去完成,

直到父類無法完成該請求時,它才會嘗試自己去完成。

【職責鏈設計模式】:事件沿職責鏈往上走,直到遇到能完成它的類。

這裏也差不多,只是變成了直到遇到不能完成它的類。

雙親委派有什麼好處?

因為類的唯一性要由類加載器參與確認,因此如果我們用不同類加載器加載一個Class文件,那麼會產生不同的類。

對於java.lang.Object這些底層的類而言,就很要命了。。。

JVM中出現了很多職責行為一樣,但是卻是不同的類。混亂了!唯一根類也沒辦法滿足了。

雙親委派模型使得Java類和它的類加載器一起具備了一種帶優先級的層級關系。

<JAVA_HOME>\lib下的類都是由Bootstrap ClassLoader加載的,在程序中只會有一份。

<JAVA_HOME>\lib\ext下的類都是由Extension ClassLoader加載的,也只有一份。

用戶指定類路徑上的都是由Application ClassLoader加載。

雙親委派模型實現:

3、破壞雙親委派模型

曆史上的三次破壞:

1、雙親委派模型在JDK1.2才引入,之前就有很多代碼是繼承ClassLoader而沒有實現雙親委派的。

2、由於模型缺陷

在一些集成架構中(如JDBC、JNDI),架構的主體是在Java JDK類庫中,

Bootstrap ClassLoader來加載。

但是,架構的具體的模塊卻是由獨立廠商實現並部署在應用程序的ClassPath下的。

Bootstrap ClassLoader加載的代碼要調用應用程序的ClassPath下的代碼

怎麼辦?Bootstrap ClassLoader不能加載這些代碼啊。。

因此,引入了Thread Context ClassLoader線程上下文類加載器

Thread Context ClassLoader可由java.lang.Thread類的setContextClassLoader() 方法進行設置

如果該線程沒有設置,那麼它會從父線程那裏繼承

如果全局都沒有設置,那麼默認值是Application ClassLoader

現在執行過程就變成了:

1、架構主體代碼由Bootstrap ClassLoader來加載。

當要加載廠商模塊時:

2、先一個方法將線程的類加載器設置為自己想要的類加載器,並保存線程之前的類加載器

3、加載時,用Thread.currentThread.getContextClassLoader()方法取得類加載器。

4、加載完成,將線程的類加載器還原

3、為了實現熱替換HotSwap

即插即用,熱部署。OSGi模塊化標准

每一個程序模塊(OSGi稱為Bundle)都有一個自己的類加載器,

當要替換一個Bundle時,連同它的類加載器一起替換。

OSGi類加載委派模型:

1、以java.*開頭的類委派給父類加載器

2、否則,將委派列錶名單內的類委派給父類加載器加載。

3、否則,將import列錶中的類委派給export這個類的Bundle的類加載器加載。

4、否則,查找當前Bundle的ClassPath,使用自己的類加載器加載。

5、否則,查找類是否在自己的Fragment Bundle中,如果在,則委托給Fragment Bundle的類加載器加載。

6、否則,查找Dynamic Import列錶的Bundle,委派給對應的Bundle的類加載器加載。

1、2仍然符合雙親委派,其餘都是平級查找。

 

 

 

 

 

版权声明:本文为[Lqblalala]所创,转载请带上原文链接,感谢。 https://gsmany.com/2021/08/20210815134734581l.html