JVM006_类加载的过程
類加載
類加載時機
類加載的過程
新術語
類加載器 簡單的理解為將類轉換為二進制流的類或接口。
數組的元素類型 數組去掉所有維度的類型。
數組的組件類型 數組去掉一個維度的類型。
基本塊 按照控制流拆分的代碼塊。
1. 加載
加載是類加載過程的一個階段。加載階段主要完成三件事情:
在上述的1中,沒有限定此類的格式,所以可以是一個class文件,可以是一個jar包,也可以是運行時生成等等。我們可以通過重寫一個類加載器的findclass()方法或者loadClass方法來自定義字節流的獲取方法。
數組與類加載器
數組類本身是由JVM在內存中直接構造的,但是又與類加載器緊密聯系,其遵循規則如下:
如果數組的組件類型是一個引用類型,那么會遞歸的使用加載過程去加載該組件類型,數組將被標識在加載該組件類型的類加載器的類名稱空間上。
若組件類型是基本類型,JVM會將該數組標記為與BootStrapClassLoader關聯
數組類的可訪問性與其組件類型的可訪問性一致。
2. 驗證
驗證是連接的第一步,器目的是確保Class文件的字節流中包含的信息完全符合《JVM規范》中的全部約束條件,保證這些信息在運行時不會威脅到JVM的安全。其大致可分為四個階段:文件格式驗證,元數據驗證,字節碼驗證和符號引用驗證。
文件格式驗證
這一階段要驗證字節流是否符合Class文件格式的規范,以保證輸入的字節流能夠正確的解析并存儲到方法區內。格式上要符合一個Java類型信息的要求。只有文件格式驗證通過后,才能將字節流中的信息存儲到方法區中,所有后面的是三個驗證,都是基于方法區的存儲結構進行的,而不是字節流。
元數據驗證
這個階段要求對字節碼的描述信息進行語義分析,也就是保證其描述的信息符合《Java語言規范》的要求。
字節碼驗證
該驗證的目的是通過數據流分析和控制流分析,確定程序語義的合法性、合邏輯性。在元數據驗證通過后,該階段對類的方法體(也就是Class文件中的Code屬性)進行校驗分析,保證類的方法不會在運行時危害到JVM。
在JDK6后,將盡可能多的校驗輔助措施挪到javac編譯器中,具體的做法是在Code屬性中增加了一個StackMapTable屬性,該屬性描述了方法體所有的基本塊開始時本地變量和操作棧應有的狀態。在字節碼驗證時,只需要檢查StackMapTable中記錄是否合法即可,而不用根據程序推導這些狀態的合法性。
符號引用驗證
該階段發生在JVM將符號引用轉為直接引用的時候,其在連接的第三階段解析階段才發生。是對類自身以外的各類信息進行匹配性校驗,比如說該類是否缺少或禁止訪問它依賴的某些額外部類等,若無法通過驗證,會排除java.lang.IncompatibleClassChangeError的子類異常(java.lang.NoSuchFieldError等)。
3. 準備
準備階段是正式為被static修飾的變量(類變量)分配內存并設置初始值的階段。
特別注意:
-
此階段的內存分配,僅僅包括類變量,不包括實例變量。
-
若類變量同時被final修飾(也就是通常說的常量),那么其賦值不會是基本類型的零值,而是指定的值。
例如public staitc final int INIT_VALUE=99,那么在準備階段INIT_VALUE會被賦值為99,而不是0,這是因為被static final同時修飾是,在javac編譯時,字段屬性表中會有一個ConstantValue屬性,在準備階段,該變量值就會被初始化為ConstantValue屬性所指定的初始值。
基本數據的零值
| int | 0 |
| long | 0L |
| short | (short)0 |
| char | ‘\u0000’ |
| byte | (byte)0 |
| boolean | false |
| float | 0.0f |
| double | 0.0d |
| reference | null |
4. 解析
解析是JVM將符號引用轉換為直接引用的過程。
符號引用 以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要在使用時能夠無歧義的定義到目標即可。符號引用與JVM的內存布局無關。
直接引用 是可以直接指向目標的指針、相對偏移量或者一個能夠間接定位到目標的句柄。其與JVM內存布局直接相關。
句柄 是由系統所管理的引用標識,該標識可以被系統重新定位到一個內存地址上。
解析動作主要針對類或結構、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符這七類符號醫用進行,其對應著8種常量類型。
4.1 類或者接口的解析
假設當前的類為D,要將其中未解析過的符號引用N解析為一個類或者接口C的直接引用。大概過程如下:
4.2 字段解析
若要對一個沒有經過解析的字段進行解析,首先我們需要看字段表內的class_index項中的CONSTANT_class_info符號引用進行解析(參考類文件結構),也就是對字段對應的類或者接口的引用的解析。
假設字段對應的類或接口為C,那么在解析類或接口成功后,會根據《JVM規范》對字段進行搜索:
4.3 方法解析
方法解析的第一步也是對方法表內的class_index項中的CONSTANT_class_info符號引用進行解析(參考類文件結構)。若解析成功則會按照如下的規則來搜尋對應的方法。
4.4 接口方法解析
基本同方法解析。若接口解析成功,接下來的方法搜索規則如下:
5. 初始化
類初始化是類加載過程的最后一個階段。在這個階段JVM才真正開始執行類中編寫的程序代碼,將主導權交給程序。在準備階段,已經對類變量賦了零值,在這一階段,將會根據程序編碼去初始化類變量和其它資源。也可以說初始化就是執行類構造器()方法的過程。
接下來我們對<clinit>()方法做一些說明。
- <clinit>()方法是由javac編譯器自動生成的,是由編譯器自動收集類中的所有類變量的賦值動作和靜態語句塊(static{})中的語句合并產生的,編譯器收集的順序是由語句在源文件中出現的順序覺得的,靜態語句塊中只能訪問到在靜態語句塊之前的變量,定義在其之后的變量,在該語句塊中只能賦值不能訪問。
- <clinit>()方法與<init>()方法(類的構造函數)不同,它不需要顯示的調用父類構造器,JVM會保證在子類的<clinit>()執行前,父類的<clinit>()已經執行完畢。可以推論出,JVM中第一個被執行的<clinit>()方法是java.lang.Object的。
- 由于父類的<clinit>()先執行,所以父類的靜態語句塊要優先于子類的變量賦值操作。
- <clinit>()對于接口來說是非必要的。
- 接口中不能使用靜態代碼塊,但是可能存在類變量的賦值操作,因而接口也會生成<clinit>()方法。但是當接口的<clinit>()方法執行時,不要求父接口的<clinit>()方法先執行,只有當父類中定義的變量被使用時,父接口才會被初始化。此外,接口的實現類在初始化時也不會執行接口的<clinit>()方法。
- JVM必須保證一個類的<clinit>()方法在多線程環境下被正確的加鎖同步。若一個類的<clinit>()方法中有耗時很長的操作,那就可能造成阻塞。
類加載器
對于任意一個類,都必須由加載它的類加載器和這個類本身一起確立其在JVM中的唯一性。
雙親委派模式
從虛擬機的視角類加載器可以分為:
從使用這角度可分為:
BootStrapClassloader
負責加載在<JAVA_HOME>\lib目錄,或者被-Xbootclasspath參數所指定的路徑中存放的,并且是能夠被JVM所識別的類庫加載到虛擬機內存中。
Extension Class Loader 這個類是在sum.mis.Launcher$ExtClassLoader中以Java代碼實現的。它負責加載<JAVA_HOME>\ext\目錄中,或者被java.ext.dirs系統變量所指定的路徑中的所有類庫。
Application Class Loaser這個類由``sum.mis.Launcher$AppClassLoader`來實現,有時也被稱為“系統類加載器”,它用來加載用戶類路徑上所有的類庫。
JDK9之前的Java應用都是由這三類加載器來相互配合完成加載。通常這些類加載器按照下圖的協作關系來完成加載:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-ztTqTNW1-1617897204791)(https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg4.mukewang.com%2F5bdf01aa0001a43210380303.jpg&refer=http%3A%2F%2Fimg4.mukewang.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1620482748&t=d3df08dd58dfb0a1299f9f0207afff4c)]
這樣的模型被稱為雙親委派模型,其工作過程如下:
若一個類加載器收到了類加載請求,它首先不會自己去加載,而是將這個請求委托給父類加載器去完成,每一層次的類加載器都是如此。因此所有的加載請求最終都應該傳送到最頂層的BootStrapClassloader ,只有當父加載器反饋自己無法完成這個加載請求(及在它的搜索范圍類沒有找到所需的類),子需求才會去嘗試自己完成加載。
注意:雙親委派中的父加載器,不是繼承關系中的父子關系,而是通過組合關系來復用父加載器的代碼。
破壞雙親委派
在上一點中我們提到通常情況下,加載是按照雙親委派模型執行,意味著存在這其它方法,也就是雙親委派模型被破壞。按歷史反正可以分為下面三種情況:
第一次:
由于雙親委派模型是JDK1.2引入的,ClassLoader是在第一個版本就存在了的,并且加載的核心代碼在loadClass中(可參考《JVM》P284),所以為了兼容用戶已經自定義類加載器的情況,雙親委派在實現中做出了妥協,在loadClass方法中加了一個protected修飾的findClass方法,并引導用戶使用findClass。
第二次:
第二次破壞是基于雙親委派的模型自身的缺陷,雙親委派很好的解決了基礎類型一致性的問題,但是對于基礎類型需要回調用戶的代碼,雙親委派無能為力。這個時候引入線程上下文類加載器。!待深入研究
第三次:
這次破壞基于對代碼熱替換,模塊熱部署的追求。例如OSGi。!待深入研究
參考資料:
《深入理解Java虛擬機》
Tomcat類加載器破壞雙親委派
從JDBC看“破壞”雙親委派模型
真正理解線程上下文類加載器
服務發現機制
?
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
以上是生活随笔為你收集整理的JVM006_类加载的过程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 深入理解Tomcat和Jetty源码之第
- 下一篇: 蓝桥杯第七届决赛之---阶乘位数