Java类的加载过程详解 面试高频!!!值得收藏!!!
受多種情況的影響,又開始看JVM 方面的知識。
1、Java 實在過于內卷,沒法不往深了學。
2、面試題問的多,被迫學習。
3、純粹的好奇。
很喜歡一句話: 八小時內謀生活,八小時外謀發展。
望別日與君相見時,君已有所成。
來自龐大的朋友圈 --地點:??? 不知道哈。
作者:阿瞞
類的加載過程詳解
- 一、概述
- 二、過程(1) :Loading(加載)階段
- 2.1、加載的理解
- 2.2、加載完成的操作
- 2.3、二進制流的獲取方式
- 2.4、類模型與 Class 實例的位置
- 說明
- 三、過程(2) :Linking(鏈接)階段
- 3.1、鏈接階段之 Verification (驗證)
- 具體說明:
- 3.2、鏈接階段之 Preparation (準備)
- 3.3、鏈接階段之 Resolution (解析)
- (1) 具體描述:
- (2) 小結
- 四、過程(2):Initialization(初始化)階段
- 1)具體描述
- 2)static 與 final 的搭配問題
- 3)(clinit) 的線程安全性
- 4) 類的初始化情況:主動使用 vs 被動使用
- 一、主動使用
- 二、被動使用
- 五、過程四:類的 Using(使用)
- 六、過程五:類的 Unloading(卸載)
- 1)類的生命周期
- 2)類的卸載
- 自言自語
一、概述
在 Java 中數據類型分為基本數據類型和引用數據類型。基本數據類型由虛 擬機預先定義,引用數據類型則需要進行類的加載。
按照 Java 虛擬機規范,從 Class 文件到加載到內存中的類,到類卸載出內 存位置,它的整個生命周期包括如下七個階段:
驗證、準備、解析 3 個部分統稱為鏈接(Linking)
程序中類的使用過程:
課間休息會哈
二、過程(1) :Loading(加載)階段
2.1、加載的理解
2.2、加載完成的操作
加載階段,簡言之,查找并加載類的二進制數據,生成 Class 的實例
加載時,Java 虛擬機必須完成以下 3 件事情:
- 通過類的全名,獲取類的二進制數據流
- 解析類的二進制數據流為方法區內的數據結構(Java 類模型)
- 創建 java.lang.Class 類的實例,表示該類型。作為方法區這個類的各種數據 的訪問入口
2.3、二進制流的獲取方式
對于類的二進制數據流,虛擬機可以通過多種途徑產生或獲得。(只要所讀取 4 的字節碼符合 JVM 規范即可)
在獲取到類的二進制信息后,Java 虛擬機就會處理這些數據,并最終轉為一 個 java.lang.Class 的實例
如果輸入數據不是 ClassFile 的結構,則會拋出 ClassFormatError 這句話的意思是 如果 輸入數據 不符合JVM規范就會拋出異常。
2.4、類模型與 Class 實例的位置
類模型的位置
加載的類在 JVM 中創建相應的類結構,類結構會存儲在方法區(JDK 1.8 之 前:永久代;JDK 1.8 之后:元空間)
Class 實例的位置
類將 .class 文件加載至元空間后,會在堆中創建一個 java.lang.Class 對象, 用來封裝類位于方法區內的數據結構,該 Class 對象是在加載類的過程中創建 的,每個類都對應有一個 Class 類型的對象
外部可以通過訪問代表 Order 類的 Class 對象來獲取 Order 的類數據結構
說明
Class 類的構造方法是私有的,只有 JVM 能夠創建 java.lang.Class 實例是訪問類型元數據的接口,也是實現反射的關鍵數據、入口。 通過 Class 類提供的接口,可以獲得目標類所關聯的 .class 文件中具體的數據 結構:方法、字段等信息。
課間休息會
地點:長沙
三、過程(2) :Linking(鏈接)階段
3.1、鏈接階段之 Verification (驗證)
當類加載到系統后,就開始鏈接操作,驗證是鏈接操作的第一步
它的目的是保證加載的字節碼是合法、合理并符合規范的
驗證的步驟比較復雜,實際要驗證的項目也很繁多,
大體上 Java 虛擬機需 要做以下檢查,如圖所示:
- 其中格式驗證會和加載階段一起執行。驗證通過之后,類加載器才會成功將 類的二進制數據信息加載到方法區中
- 格式驗證之外的驗證操作將會在方法區中進行
魔數解釋:魔法值(即魔數)指的是未經預先定義的常量
想要了解這個冷知識的話:傳送門
具體說明:
- 比如:
- 是否所有的類都有父類的存在(在 Java 里,除了 Object 外,其他類都應該 有父類)
- 是否一些被定義為 final 的方法或者類被重寫或繼承了
- 非抽象類是否實現了所有抽象方法或者接口方法
- 是否存在不兼容的方法(比如方法的簽名除了返回值不同,其他都一樣,這種 方法會讓虛擬機無從下手調度;absract 情況下的方法,就不能是 final 的了)
- 比如:
- 在字節碼的執行過程中,是否會跳轉到一條不存在的指令
- 函數的調用是否傳遞了正確類型的參數
- 變量的賦值是不是給了正確的數據類型等
- Class 文件在其常量池會通過字符串記錄 自己將要使用的其他類或者方法。因此,在驗證階段,虛擬機就會檢查這些 類或者方法確實是存在的,并且當前類有權限訪問這些數據,如果一個需要 使用類無法在系統中找到,則會拋出 NoClassDefFoundError,如果一個方法 無法被找到,則會拋出 NoSuchMethdError 此階段在解析環節才會執行
3.2、鏈接階段之 Preparation (準備)
準備階段(Preparation),簡言之,為類的靜態變量分配內存,并將其初始化為 默認值。
當一個類驗證通過時,虛擬機就會進入準備階段。
在這個階段,虛擬機就會 為這個類分配相應的內存空間,并設置默認初始值。
Java 虛擬機為各類型變量 默認的初始值如表所示:
注意1:
Java 并不支持 boolean 類型,對于 boolean 類型,內部實現是 int, 由于 int 的默認值是 0,故對應的,boolean 的默認值就是 false
注意2:
這里不包含基本數據類型的字段用 static final 修飾的情況,因為 final 在編譯的時候就會分配了,準備階段會顯式賦值
注意這里不會為實例變量分配初始化,類變量會分配在方法區中,而實例變量是會隨著對象一起分配到 Java 堆中
在這個階段不會像初始化階段中那樣會有初始化或者代碼被執行
拓展:如果使用字面量的方式定義一個字符串的常量的話,也是在準備環節 直接進行顯式賦值
3.3、鏈接階段之 Resolution (解析)
在準備階段(Resolution),簡言之,將類、接口、字段和方法的符號引用轉為 直接引用
(1) 具體描述:
符號引用就是一些字面量的引用,和虛擬機的內部數據結構和內存分布無關。
比較容理解的就是在 Class 類文件中,通過常量池進行了大量的符號引用。
但是在程序實際運行時,只有符號引用是不夠的,比如當如下 println() 方法被調 用時,系統需要明確知道該方法的位置。
舉例:
輸出操作 System.out.println() 對應的字節碼:
invokevirtual #24<java/io/PrintStream.println>
? 以方法為例,Java 虛擬機為每個類都準備了一張方法表,將其所有的方法都 列在表中,當需要調用一個類的方法的時候,只要知道這個方法在方法表中的偏 移量就可以直接調用該方法。通過解析操作,符號引用就可以轉變為目標方法在 類中方法表中的位置,從而使得方法被成功調用
(2) 小結
所謂解析就是將符號引用轉為直接引用,也就是得到類、字段、方法在內存 中的指針或者偏移量。因此,可以說,如果直接引用存在,那么可以肯定系統中 存在該類、方法或者字段。
再來個課間休息啦。
地點:長沙南站
作者:博主自己
四、過程(2):Initialization(初始化)階段
初始化階段,簡言之,為類的靜態變量賦予正確的初始值。
1)具體描述
類的初始化是類裝載的最后一個階段。如果前面的步驟都沒有問題,那么表 示類可以順利裝載到系統中。此時,類才會開始執行 Java 字節碼。(即:到了初 始化階段,才真正開始執行類中定義的 Java 程序代碼) 初始化階段的重要工作是執行類的初始化方法:() 方法
該方法僅能由 Java 編譯器生成并由 JVM 調用,程序開發者無法自定義一 個同名的方法,更無法直接在 Java 程序中調用該方法,雖然該方法也是由 字節碼指令所組成
它是類靜態成員的賦值語句以及 static 語句塊合并產生的
說明
在加載一個類之前,虛擬機總是會試圖加載該類的父類,因此父類的 總是 在子類 之前被調用,也就是說,父類的 static 塊優先級高于子類
Java 編譯器并不會為所有的類都產生() 初始化方法。哪些類在編譯為字節 碼后,字節碼文件中將不會包含 () 方法?
- 一個類中并沒有聲明任何的類變量,也沒有靜態代碼塊時
- 一個類中聲明類變量,但是沒有明確使用類變量的初始化語句以及靜態代碼 塊來執行初始化操作時
- 一個類中包含 static final 修飾的基本數據類型的字段,這些類字段初始化語 句采用編譯時常量表達式
2)static 與 final 的搭配問題
/** * * 哪些場景下,Java 編譯器就不會生成<clinit>()方法 */ public class InitializationTest1 {//場景 1:對應非靜態的字段,不管是否進行了顯式賦值,都不會生成<clinit>()方法public int num = 1;//場景 2:靜態的字段,沒有顯式的賦值,不會生成<clinit>()方法public static int num1;//場景 3:比如對于聲明為 static final 的基本數據類型的字段,不管是否進行了顯式賦值,都不會生成<clinit>()方法public static final int num2 = 1; } /** * * 說明:使用 static + final 修飾的字段的顯式賦值的操作,到底是在哪個階段 進行的賦值? * 情況 1:在鏈接階段的準備環節賦值 * 情況 2:在初始化階段<clinit>()中賦值 * * 結論: * 在鏈接階段的準備環節賦值的情況: * 1. 對于基本數據類型的字段來說,如果使用 static final 修飾,則顯式賦值(直 接賦值常量,而非調用方法)通常是在鏈接階段的準備環節進行 * 2. 對于 String 來說,如果使用字面量的方式賦值,使用 static final 修飾的 話,則顯式賦值通常是在鏈接階段的準備環節進行 * * 在初始化階段<clinit>()中賦值的情況 * 排除上述的在準備環節賦值的情況之外的情況 * * 最終結論:使用 static + final 修飾,且顯示賦值中不涉及到方法或構造器調 用的基本數據類型或 String 類型的顯式賦值,是在鏈接階段的準備環節進行 */ public class InitializationTest2 {public static int a = 1; //在初始化階段<clinit>()中賦值15public static final int INT_CONSTANT = 10; //在鏈接階段的準備環節賦值public static final Integer INTEGER_CONSTANT1 = Integer.valueOf(100);//在初始化階段<clinit>()中賦值public static Integer INTEGER_CONSTANT2 = Integer.valueOf(1000); //在初始化階段<clinit>()中賦值public static final String s0 = "helloworld0"; //在鏈接階段的準備環節賦值public static final String s1 = new String("helloworld1"); // 在 初 始 化 階 段<clinit>()中賦值 }3)(clinit) 的線程安全性
4) 類的初始化情況:主動使用 vs 被動使用
Java 程序對類的使用分為兩種:主動使用 和 被動使用
一、主動使用
Class 只有在必須要首次使用的時候才會被裝載,Java 虛擬機不會無條件地 裝載 Class 類型。Java 虛擬機規定,一個類或接口在初次使用前,必須要進行 初始化。這里指的"使用",是指主動使用,主動使用只有下列幾種情況:(即: 如果出現如下的情況,則會對類進行初始化操作。而初始化操作之前的加載、驗 證、準備已經完成)
當創建一個類的實例時,比如使用 new 關鍵字,或者通過反射、克隆、反 序列化
當調用類的靜態方法時,即當使用了字節碼 invokestatic 指令
當使用類、接口的靜態字段時(final 修飾特殊考慮),比如,使用 getstatic 或 者 putsttic 指令。(對應訪問變量、賦值變量操作)
當 使 用 java.lang.reflect 包 中 的 方 法 反 射 類 的 方 法時 。 比 如 :Class.forname(“com.atguigu.java.Test”)
當初始化子類時,如果發現其分類還沒有進行過初始化,則需要先觸發其父 類的初始化
如果一個接口定義了 default 方法,那么直接實現或者間接實現該接口的類的初始化,該接口要在其之前被初始化
當虛擬機啟動時,用戶需要指定一個要執行的主類(包含 main() 方法的那個 類),虛擬機會先初始化這個主類
當初次調用 MethodHandle 實例時,初始化該 MethodHandle 指向的方法所 在的類。
(涉及解析 REF_getStatic、REF_putStatic、REF_invokeStatic 方法 17 句柄對應的類)
針對 5,補充說明: 當 Java 虛擬機初始化一個類時,要求它的所有父類都已經被初始化,但是這條規則并不適用于接口
二、被動使用
除了以上的情況屬于主動使用,其他的情況均屬于被動使用。
被動使用不會 引起類的初始化 也就是說:并不是在代碼中出現的類,就一定會被加載或者初始化。如果不符合主動使用的條件,類就不會初始化
- 當通過子類引用父類的靜態變量,不會導致子類初始化
通過數組定義類引用,不會觸發此類的初始化
引用變量不會觸發此類或接口的初始化。因為常量在鏈接階段就已經被顯式 賦值了
調用 ClassLoader 類的 loadClass() 方法加載一個類,并不是對類的主動使 用,不會導致類的初始化 如果針對代碼,設置參數 -XX:+TraceClassLoading,可以追蹤類的加載信息并打印出來
停一會,想一想自己理解了嗎
地點:長沙萬家麗高架上
作者:博主
五、過程四:類的 Using(使用)
任何一個類型在使用之前都必須經歷過完整的加載、鏈接和初始化 3 個類加 載步驟。一旦一個類型成功經歷過這 3 個步驟之后,便“萬事俱備,只欠東風”, 就等著開發者使用了。
開發人員可以在程序中訪問和調用它的靜態類成員信息(比如:靜態字段、靜 態方法),或者使用 new 關鍵字為其創建對象實例。
六、過程五:類的 Unloading(卸載)
1)類的生命周期
當 Sample 類被加載、鏈接和初始化后,它的生命周期就開始了。當代表 Sample 類的 Class 對象不再被引用,即不可觸及時,Class 對象就會結束生命 周期,Sample 類在方法區內的數據也會被卸載,從而結束 Sample 類的生命周期
一個類何時結束生命周期,取決于代表它的 Class 對象何時結束生命周期
? Loader1 變量和 obj 變量間接應用代表 Sample 類的 Class 對象,而 objClass 變量則直接引用它
? 如果程序運行過程中,將上圖左側三個引用變量都置為 null,此時 Sample 對象結束生命周期,MyClassLoader 對象結束生命周期,代表 Sample 類的 Class 對象也結束生命周期,Sample 類在方法區內的二進制數據被卸載。
? 當再次有需要時,會檢查 Sample 類的 Class 對象是否存在,如果存在會直 接使用,不再重新加載;如果不存在 Sample 類會被重新加載,在 Java 虛擬機 的堆區會生成一個新的代表 Sample 類的 Class 實例(可以通過哈希碼查看是否 是同一個實例)
2)類的卸載
啟動類加載器加載的類型在整個運行期間是不可能被卸載的(JVM 和 JSL 規范)
被系統類加載器和擴展類加載器加載的類型在運行期間不太可能被卸載,因 為系統類加載器實例或者擴展類的實例基本上在整個運行期間總能直接或者間接的訪問的到,其達到 unreachable 的可能性極小
被開發者自定義的類加載器實例加載的類型只有在很簡單的上下文環境中 才能被卸載,而且一般還要借助于強制調用虛擬機的垃圾收集功能才可以做到。可以預想,稍微復雜點的應用場景(比如:很多時候用戶在開發自定義類 的加載器實例的時候采用緩存的策略以提高系統性能),被加載的類型在運行 期間也是幾乎不太可能被卸載的(至少卸載的時間是不確定的)
綜合以上三點,一個已經加載的類型被卸載的幾率很小至少被卸載的時間是 不確定的。同時我們可以看的出來,開發者在開發代碼時候,不應該對虛擬機的 類型卸載做任何假設的前提下,來實現系統中的特定功能。
文章來自于 尚硅谷 宋紅康老師 上課PPT
自言自語
越來越明白數據結構、算法重要性。也越來越對于底層的東西感興趣。
我們一直站在巨人的肩旁上,看到的都很遠,但是對于一些基礎的東西卻忘記的越來越多,這是不行的。
共勉。
總結
以上是生活随笔為你收集整理的Java类的加载过程详解 面试高频!!!值得收藏!!!的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Mybatis 源码探究 (4) 将sq
- 下一篇: 小学五年级就已经开始编程啦吗???