該知道 JVM 加載機制了-
類加載
Java 虛擬機類加載過程是把 Class 類文件加載到內存,並對 Class 文件中的數據進行校驗、轉換解析和初始化,最終形成可以被虛擬機直接使用的 java 類型的過程
類加載的時機
| 序號 | 內容 | | --- | --- | | 1 | 遇到 new、getstatic、putstatic、或 invokestatic 這四條字節碼指令 | | 2 | 使用 java.lang.reflect 包的方法對類進行反射調用的時候 | | 3 | 初始化類時,父類沒有被初始化,先初始化父類 | | 4 | 虛擬機啓動時,用戶指定的主類 (包含 main() 的那個類) | | 5 | 當使用 JDK1.7 動態語言支持的時,如果一個 java.lang.invoke.MethodHandle 實例最後解析的結果 REFgetStatic、REFputStatic、REF_invokeStatic 的方法句柄,並且這個方法句柄鎖對應的類沒有進行過初始化時 |
-
使用 new 關鍵字實例化對象時
-
讀取類的靜態變量時(被 final 修飾,已在編譯期把結果放入常量池的靜態字段除外)
-
設置類的靜態變量時
-
調用一個類的靜態方法時
-
類的實例化是指創建一個類的實例 (對象) 的過程;
-
類的初始化是指爲類各個成員賦初始值的過程,是類生命週期中的一個階段;
-
通過子類引用父類的靜態字段,不會導致子類初始化
1/** 2 * @program: jvm 3 * @ClassName Test1 4 * @Description:通過子類引用父類的靜態字段,不會導致子類初始化 5 * @author: 牧小農 6 * @create: 2021-02-27 11:42 7 * @Version 1.0 8 **/ 9public class Test1 { 10 static { 11 System.out.println("Init Superclass!!!"); 12 } 13 public static void main(String[] args) { 14 int x = Son.count; 15 } 16} 17class Father extends Test1{ 18 static int count = 1; 19 static { 20 System.out.println("Init father!!!"); 21 } 22} 23class Son extends Father{ 24 static { 25 System.out.println("Init son!!!"); 26 } 27} 28
1Init Superclass!!!
2Init father!!!
3
對於靜態字段,只有直接定義這個字段的類纔會被初始化,因此通過其子類來引用父類中定義的靜態字段,只會觸發父類的初始化而不會觸發子類的初始化。至於是否要觸發子類的加載和驗證,在虛擬機中並未明確規定,這點取決於虛擬機的具體實現。對於 Sun HotSpot 虛擬機來說,可通過 - XX:+TraceClassLoading 參數觀察到此操作會導致子類的加載。
上面的案例中,由於 count 字段是在 Father 類中定義的,因此該類會被初始化,此外,在初始化類 Father 的時候,虛擬機發現其父類 Test1 還沒被初始化,因此虛擬機將先初始化其父類 Test1 ,然後初始化子類 Father,而 Son 始終不會被初始化;
-
通過數組定義來引用類,不會觸發此類的初始化
1/** 2 * @program: jvm 3 * @ClassName Test2 4 * @description: 5 * @author: muxiaonong 6 * @create: 2021-02-27 12:03 7 * @Version 1.0 8 **/ 9public class Test2 { 10 public static void main(String[] args) { 11 M[] m = new M[8]; 12 } 13} 14class M{ 15 static { 16 System.out.println("Init M!!!"); 17 } 18} 19
運行之後我們會發現沒有輸出 "Init M!!!",說明沒有觸發類的初始化階段
-
常量在編譯階段會存入調用類的常量池中,本質上並沒有直接引用到定義常量的類,因此不會觸發定義常量的類的初始化
-
1/** 2 * @program: jvm 3 * @ClassName Test3 4 * @description: 5 * @author: muxiaonong 6 * @create: 2021-02-27 12:05 7 * @Version 1.0 8 **/ 9public class Test3 { 10 public static void main(String[] args) { 11 System.out.println(ConstClass.COUNT); 12 } 13} 14class ConstClass{ 15 static final int COUNT = 1; 16 static{ 17 System.out.println("Init ConstClass!!!"); 18 } 19} 20
上面代碼運行後也沒有輸出 InitConstClass!!!
,這是因爲雖然在 Java 源碼中引用了 ConstClass 類中的常量 COUNT ,但其實在編譯階段通過常量傳播優化,已經將常量的值 "1"
存儲到 Test3 常量池中了,對常量 ConstClass.COUNT 的引用實際都被轉化爲 Test3 類對自身常量池的引用了,也就是說,實際上 Test3 的 Class 文件之中並沒有 ConstClass 類的符號引用入口,這兩個類在編譯爲 Class 文件之後就不存在關係
類加載過程
有一個名叫 Class 文件,它靜靜的躺在了硬盤上,喫香的喝辣的,他究竟需要一個怎麼樣的過程經歷了什麼,才能夠從舒服的硬盤中到內存中呢?class 進入內存總共有三大步。
-
加載 (Loading)
-
連接 (Linking)
-
初始化 (Initlalizing)
1、加載
加載 是 類加載 (Class Loading) 過程的一個階段,加載 是 類加載 (Class Loading) 過程的一個階段,加載是指將當前類的 class 文件讀入內存中,並且創建一個 java.lang.Class
的對象,也就是說,當程序中使用任何類的時候,系統都會創建一個叫 java.lang.Class
對象
在加載階段,虛擬機需要完成以下三個事情:
-
通過一個類的全限定名類獲取定義此類的二進制字節流 (沒有指明只能從一個 Class 文件中獲取,可以從其他渠道,如:網絡、動態生成、數據庫等)
-
將這個字節流所代表的靜態存儲結構轉化爲方法區的運行時數據結構
-
在內存中生成一個代表這個類的
java.lang.Class
對象,作爲方法區這個類的各種數據的訪問入口類加載器通常無須等到 “首次使用” 該類時才加載該類,Java 虛擬機規範允許系統預先加載某些類。加載階段與連接階段的部分內容是交叉進行的,加載階段尚未完成,連接階段可能已經開始,但這些夾在夾在階段之中進行的動作,仍然屬於連接階段的內容,這兩個階段的開始時間仍然保持着固定的先後順序。
2、連接
當類被加載之後,系統會生成一個對應的 Class 對象,就會進入 連接階段,連接階段負責把類的二進制數據合併到 JRE 中,連接階段又分爲三個小階段
1.1 驗證
驗證是連接階段的第一步,這一階段的主要目的是爲了確保 Class 文件的字節流中包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。Java 語言相對於 C/C++ 來說本身是相對安全的語言,驗證階段是非常重要的,這個階段是否嚴謹,決定了 Java 虛擬機能不能承受惡意代碼的攻擊,當驗證輸入的字節流不符合 Class 文件格式的約束時,虛擬機會拋出一個 java.lang.VerifyError
異常或者子類異常,從大體來說驗證主要分爲四個校驗動作:文件格式驗證、元數據驗證、字節碼驗證、符號引用驗證
文件格式驗證: 主要驗證字節流是否符合 Class 文件格式的規範,並且能被當前版本的虛擬機處理。主要包含以下幾個方面:
-
文件格式是否以
CAFEBABE
開頭 -
主次版本是否在虛擬機處理的範圍內
-
常量池的常量是否有不被支持的常量類型
-
指向常量的各種索引值是否有指向不存在的常量或者不符合類型的常量
-
CONSTANTUtf8info 型的常量是否有不符合 UTF8 編碼的數據
-
Class 文件中各個部分及文件本身是否有被刪除的活附件的信息
元數據驗證: 主要是對字節碼描述的信息進行語義分析,主要目的是對類的元數據進行語義校驗,分析是否符合 Java 的 語言語法的規範,保證不存在不符合 Java 語言的規範的元數據的信息,該階段主要驗證的方面包含以下幾個方面:
-
這個類是否有父類 (除 java.lang.Object)
-
這個類的父類是否繼承了不允許被繼承的類 (被 final 修飾的類)
-
如果這個類不是抽象類,是否實現了父類或接口之中要求的所有方法
-
類中的字段、方法是否和父類產生矛盾
字節碼驗證: 最重要也是最複雜的校驗環節,通過數據流和控制流分析程序語義是否合法、符合邏輯的。主要針對類的方法體進行校驗分析,保證被校驗的類在運行時不會危害虛擬機安全的事情
-
保證任何時候操作數棧的數據類型和指令代碼序列都能配合工作(例如在操作棧上有一個 int 類型的數據,保證不會在使用的時候按照 long 類型來加載到本地變量表中)
-
跳轉指令不會條狀到方法體以外的字節碼指令上
-
保證方法體中的數據轉換是有效的,例如可以把一個子類對象賦值給父類數據類型,但是不能把父類賦值給子類數據類型
符號引用驗證: 針對符號引用轉換直接引用的時候,這個裝換工作會在第三階段(字節碼驗證)解析階段中發生。主要是保證引用一定會被訪問到,不會出現類無法訪問的問題。
1.2 準備
爲類變量 分配內存並設置類變量初始值的階段,這些變量所使用的內存都會在方法區進行分配,在準備階段是把 class 文件靜態變量賦默認值,注意:不是賦初始值,比如我們 publicstaticinti=8
,在這個步驟 並不是把 i 賦值成 8 ,而是先賦值爲 0
基本類型的默認值:
| 數據類型 | 默認值 | | --- | --- | | int | 0 | | long | 0L | | short | (short)0 | | char | '\u0000' | | byte | (byte)0 | | boolean | false | | float | 0.0f | | double | 0.0d | | reference | null |
在通常情況下初始值是 0,但是如果我們把上面的常量加一個 final 類修飾的話,那麼這個時候初始值就會編程我們指定的值 publicstaticfinalinti=8
編譯的時候 Javac 會把 i 的初始值變爲 8,
1.3 解析
把 class 文件常量池裏面用到的符號引用轉換爲直接內存地址,直接可以訪問到的內容 符號引用:以一組符號來描述所引用的目標,符號可以是任何字面形式的字面量,只要不會出現衝突能夠定位到就可以 直接引用:可以是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄,如果有了直接引用,那引用的目標必定已經在內存中存在了
3、初始化
初始化是給類的靜態變量賦正確的初始值,剛纔我們有講到準備階段是複製默認值,而初始化是給靜態變量賦值初始值,看下面的語句:
public static int i = 8
首先字節碼文件被加載到內存後,先進行連接驗證,通過準備階段,給 i 分配內存,因爲是 static,所以這個時候 i 等於 int 類型的默認初始值是 0,所以 i 現在是 0,到了初始化的時候,纔會真正把 i 賦值爲 8
類加載器
類加載器負責加載所有的類,並且爲載入內存中的類生成一個 java.lang.Class 實例對象,如果一個類被加載到 JVM 中後,同一個類不會再次被載入,就像對象有一個唯一的標識,同樣載入的 JVM 的類也有一個唯一的標識。JVM 本身有一個類加載器的層次,這個類加載器本身就是一個普通的 Class,所有的 Class 都是被類加載器加載到內存中,我們可以稱之爲 ClassLoader,一個頂級的父類,也是一個 abstract 抽象類。
Bootstrap: 類加載器的加載過程,分成不同的層次來進行加載,不同的類加載器加載不同的 Class,作爲最頂層的 Bootstrap,它加載 lib 裏 JDK 最核心的內容,比如說 rt.jar charset.jar 等核心類,當我們調用 getClassLoader() 拿到這個加載器結果是一個 Null 的時候,代表我們已經達到了最頂層的加載器
Extension: Extension 加載器擴展類,加載擴展包裏的各種各樣的文件,這些擴展包在 JDK 安裝目錄 jre/lib/ext 下的 jar
App: 就是我們平時用到的 application ,用來加載 classpath 指定的內容
Custom ClassLoader: 自定義 ClassLoader,加載自己自定義的加載器 Custom ClassLoader 的父類加載器是 application 的父類加載器是 Extension 的父類加載器是 Bootstrap
注意:他們不是繼承關係,而是委託關係
1public class ClassLoaderTest {
2 public static void main(String[] args) {
3 // 查看是誰Load到內存的,執行結果是null,因爲Bootstrap使用C++實現的
4 // 在Java裏面沒有class和它對應
5 System.out.println(String.class.getClassLoader());
6 //這個是核心類庫某個包裏的類執行,執行結果是Null,因爲該類也是被Bootstrap加載的
7 System.out.println(sun.awt.HKSCS.class.getClassLoader());
8 //這個類是位於ext目錄下某個jar文件裏面,當我們調用他執行結果就是sun.misc.Launcher$ExtClassLoader@a09ee92
9 System.out.println(sun.net.spi.nameservice.dns.DNSNameService.class.getClassLoader());
10 // 這個是我們自己寫的ClassLoad加載器,由sun.misc.Launcher$AppClassLoader@18b4aac2加載
11 System.out.println(ClassLoaderTest.class.getClassLoader());
12 // 是Exe的ClassLoader 調用它的getclass(),它本身也是一個class,調用它的getClassLoader,他的ClassLoader的ClassLoader就是我們的Bootstrap所以結果爲Null
13 System.out.println(sun.net.spi.nameservice.dns.DNSNameService.class.getClassLoader().getClass().getClassLoader());
14 }
15}
16
類加載器繼承關係
雙親委派
父加載器: 父加載器不是 "類加載器的加載器",也不是 "類加載器的父類加載器" 雙親委派是一個孩子向父親的方向,然後父親向孩子方向的雙親委派過程
這個是類加載器必問的一個面試題。
主要爲了安全,如果任何一個 Class 都可以把他 load 到內存中的話,那麼我寫一個 java.lang.String,如果我寫入了有危險的代碼,是不是就會發生安全問題,並且可以保證 Java 核心 api 中定義的類型不會被隨意替換,可以防止 API 內庫被隨意更改,其次是效率問題,如果有緩存在,直接從緩存裏面拿,就不用一遍一遍的去遍歷查詢我們的父類或者子類了。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/6uK8_0ltPa5fEAUCflpJmw