中yeti不能加载_第二十章_类的加载过程详解
類的加載過程詳解
概述
在 Java 中數(shù)據(jù)類型分為基本數(shù)據(jù)類型和引用數(shù)據(jù)類型?;緮?shù)據(jù)類型由虛擬機預先定義,引用數(shù)據(jù)類型則需要進行類的加載
按照 Java 虛擬機規(guī)范,從 Class 文件到加載到內(nèi)存中的類,到類卸載出內(nèi)存位置,它的整個生命周期包括如下七個階段:
其中,驗證、準備、解析3個部分統(tǒng)稱為鏈接(Linking)
從程序中類的使用過程看:
大廠面試題
螞蟻金服:
描述一下 JVM 加載 Class 文件的原理機制?
一面:類加載過程
百度:
類加載的機制
Java 類加載過程?
簡述 Java 類加載機制?
騰訊:
JVM 中類加載機制,類加載過程?
滴滴:
JVM 類加載機制
美團:
Java 類加載過程
描述一下 JVM 加載 Class 文件的原理機制
過程一:Loading(加載)階段
加載完成的操作
加載的理解
所謂加載,簡而言之就是將 Java 類的字節(jié)碼文件加載到機器內(nèi)存中,并在內(nèi)存中構建出 Java 類的原型——類模板對象。所謂類模板對象,其實就是 Java 類在 JVM 內(nèi)存中的一個快照,JVM 將從字節(jié)碼文件中解析出的常量池、類字段、類方法等信息存儲到模板中,這樣 JVM 在運行期便能通過類模板而獲取 Java 類中的任意信息,能夠?qū)?Java 類的成員變量進行遍歷,也能進行 Java 方法的調(diào)用
反射的機制即基于這一基礎。如果 JVM 沒有將 Java 類的聲明信息存儲起來,則 JVM 在運行期也無法反射
加載完成的操作
加載階段,簡言之,查找并加載類的二進制數(shù)據(jù),生成 Class 的實例
在加載類時,Java 虛擬機必須完成以下3件事情:
- 通過類的全名,獲取類的二進制數(shù)據(jù)流
- 解析類的二進制數(shù)據(jù)流為方法區(qū)內(nèi)的數(shù)據(jù)結構(Java 類模型)
- 創(chuàng)建 java.lang.Class 類的實例,表示該類型。作為方法區(qū)這個類的各種數(shù)據(jù)的訪問入口
二進制流的獲取方式
對于類的二進制數(shù)據(jù)流,虛擬機可以通過多種途徑產(chǎn)生或獲得。(只要所讀取的字節(jié)碼符合 JVM 規(guī)范即可)
- 虛擬機可能通過文件系統(tǒng)讀入一個 Class 后綴的文件(最常見)
- 讀入 jar、zip 等歸檔數(shù)據(jù)包,提取類文件
- 事先存放在數(shù)據(jù)庫中的類的二進制數(shù)據(jù)
- 使用類似于 HTTP 之類的協(xié)議通過網(wǎng)絡進行加載
- 在運行時生成一段 Class 的二進制信息等
在獲取到類的二進制信息后,Java 虛擬機就會處理這些數(shù)據(jù),并最終轉(zhuǎn)為一個 java.lang.Class 的實例
如果輸入數(shù)據(jù)不是 ClassFile 的結構,則會拋出 ClassFormatError
類模型與 Class 實例的位置
加載的類在 JVM 中創(chuàng)建相應的類結構,類結構會存儲在方法區(qū)(JDK 1.8之前:永久代;JDK 1.8之后:元空間)
類將 .class 文件加載至元空間后,會在堆中創(chuàng)建一個 java.lang.Class 對象,用來封裝類位于方法區(qū)內(nèi)的數(shù)據(jù)結構,該 Class 對象是在加載類的過程中創(chuàng)建的,每個類都對應有一個 Class 類型的對象
外部可以通過訪問代表 Order 類的 Class 對象來獲取 Order 的類數(shù)據(jù)結構
Class 類的構造方法是私有的,只有 JVM 能夠創(chuàng)建
java.lang.Class 實例是訪問類型元數(shù)據(jù)的接口,也是實現(xiàn)反射的關鍵數(shù)據(jù)、入口。通過 Class 類提供的接口,可以獲得目標類所關聯(lián)的 .class 文件中具體的數(shù)據(jù)結構:方法、字段等信息
數(shù)組類的加載
創(chuàng)建數(shù)組類的情況稍微有些特殊,因為數(shù)組類本身并不是由類加載器負責創(chuàng)建,而是由 JVM 在運行時根據(jù)需要而直接創(chuàng)建的,但數(shù)組的元素類型仍然需要依靠類加載器去創(chuàng)建。創(chuàng)建數(shù)組類(下述簡稱 A)的過程:
如果數(shù)組的元素類型是引用類型,數(shù)組類的可訪問性就由元素類型的可訪問性決定。否則數(shù)組類的可訪問性將被缺省定義為 public
過程二:Linking(鏈接)階段
環(huán)節(jié)1:鏈接階段之 Verification (驗證)
當類加載到系統(tǒng)后,就開始鏈接操作,驗證是鏈接操作的第一步
它的目的是保證加載的字節(jié)碼是合法、合理并符合規(guī)范的
驗證的步驟比較復雜,實際要驗證的項目也很繁多,大體上 Java 虛擬機需要做以下檢查,如圖所示
整體說明:
驗證的內(nèi)容則涵蓋了類數(shù)據(jù)信息的格式驗證、語義檢查、字節(jié)碼驗證,以及符號引用驗證等
- 其中格式驗證會和加載階段一起執(zhí)行。驗證通過之后,類加載器才會成功將類的二進制數(shù)據(jù)信息加載到方法區(qū)中
- 格式驗證之外的驗證操作將會在方法區(qū)中進行
鏈接階段的驗證雖然拖慢了加載速度,但是它避免了在字節(jié)碼運行時還需要進行各種檢查
具體說明:
棧映射幀(StackMapTable)就是在這個階段,用于檢測在特定的字節(jié)碼處,其局部變量表和操作數(shù)棧是否有著正確的數(shù)據(jù)類型。但遺憾的是,100%準確地判斷一段字節(jié)碼是否可以被安全執(zhí)行是無法實現(xiàn)的,因此,該過程只是盡可能地檢查出可以預知的明顯的問題。如果在這個階段無法通過檢查,虛擬機也不會正確裝載這個類。但是,如果通過了這個階段的檢查,也不能說明這個類是完全沒有問題的
在前面3次檢查中,已經(jīng)排除了文件格式錯誤、語義錯誤以及字節(jié)碼的不正確性。但是依然不能確保類是沒有問題的
此階段在解析環(huán)節(jié)才會執(zhí)行
環(huán)節(jié)2:鏈接階段之 Preparation (準備)
準備階段(Preparation),簡言之,為類的靜態(tài)變量分配內(nèi)存,并將其初始化為默認值
當一個類驗證通過時,虛擬機就會進入準備階段。在這個階段,虛擬機就會為這個類分配相應的內(nèi)存空間,并設置默認初始值。
Java 虛擬機為各類型變量默認的初始值如表所示:
注意:Java 并不支持 boolean 類型,對于 boolean 類型,內(nèi)部實現(xiàn)是 int,由于 int 的默認值是0,故對應的,boolean 的默認值就是 false
注意:
環(huán)節(jié)3:鏈接階段之 Resolution (解析)
在準備階段(Resolution),簡言之,將類、接口、字段和方法的符號引用轉(zhuǎn)為直接引用
符號引用就是一些字面量的引用,和虛擬機的內(nèi)部數(shù)據(jù)結構和內(nèi)存分布無關。比較容理解的就是在 Class 類文件中,通過常量池進行了大量的符號引用。但是在程序?qū)嶋H運行時,只有符號引用是不夠的,比如當如下 println() 方法被調(diào)用時,系統(tǒng)需要明確知道該方法的位置
舉例:輸出操作 System.out.println() 對應的字節(jié)碼:
invokevirtual #24
以方法為例,Java 虛擬機為每個類都準備了一張方法表,將其所有的方法都列在表中,當需要調(diào)用一個類的方法的時候,只要知道這個方法在方法表中的偏移量就可以直接調(diào)用該方法。通過解析操作,符號引用就可以轉(zhuǎn)變?yōu)槟繕朔椒ㄔ陬愔蟹椒ū碇械奈恢?#xff0c;從而使得方法被成功調(diào)用
所謂解析就是將符號引用轉(zhuǎn)為直接引用,也就是得到類、字段、方法在內(nèi)存中的指針或者偏移量。因此,可以說,如果直接引用存在,那么可以肯定系統(tǒng)中存在該類、方法或者字段。但只存在符號引用,不能確定系統(tǒng)中一定存在該結構
不過 Java 虛擬機規(guī)范并沒有明確要求解析階段一定要按照順序執(zhí)行。在 HotSpot VM 中,加載、驗證、準備和初始化會按照順序有條不紊地執(zhí)行,但鏈接階段中的解析操作往往會伴隨著 JVM 在執(zhí)行完初始化之后再執(zhí)行
最后,再來看一下 CONSTANT_String 的解析。由于字符串在程序開發(fā)中有著重要的作用,因此,讀者有必要了解一下 String 在 Java 虛擬機中的處理。當在 Java 代碼中直接使用字符串常量時,就會在類中出現(xiàn) CONSTANT_String,它表示字符串常量,并且會引用一個 CONSTANT_UTF8 的常量項。在 Java 虛擬機內(nèi)部運行中的常量池,會維護一張字符串拘留表(intern),它會保存所有出現(xiàn)過的字符串常量,并且沒有重復項。只要以 CONSTANT_String 形式出現(xiàn)的字符串也都會在這張表中。使用 String.intern() 方法可以得到一個字符串在拘留表中的引用,因為該表中沒有重復項,所以任何字面相同的字符串的 String.intern() 方法返回總是相等的
過程三:Initialization(初始化)階段
初始化階段,簡言之,為類的靜態(tài)變量賦予正確的初始值
類的初始化是類裝載的最后一個階段。如果前面的步驟都沒有問題,那么表示類可以順利裝載到系統(tǒng)中。此時,類才會開始執(zhí)行 Java 字節(jié)碼。(即:到了初始化階段,才真正開始執(zhí)行類中定義的 Java 程序代碼)
初始化階段的重要工作是執(zhí)行類的初始化方法:() 方法
- 該方法僅能由 Java 編譯器生成并由 JVM 調(diào)用,程序開發(fā)者無法自定義一個同名的方法,更無法直接在 Java 程序中調(diào)用該方法,雖然該方法也是由字節(jié)碼指令所組成
- 它是類靜態(tài)成員的賦值語句以及 static 語句塊合并產(chǎn)生的
- 說明
static 與 final 的搭配問題
/**** 哪些場景下,Java 編譯器就不會生成<clinit>()方法*/ public class InitializationTest1 {//場景1:對應非靜態(tài)的字段,不管是否進行了顯式賦值,都不會生成<clinit>()方法public int num = 1;//場景2:靜態(tài)的字段,沒有顯式的賦值,不會生成<clinit>()方法public static int num1;//場景3:比如對于聲明為 static final 的基本數(shù)據(jù)類型的字段,不管是否進行了顯式賦值,都不會生成<clinit>()方法public static final int num2 = 1; } /**** 說明:使用 static + final 修飾的字段的顯式賦值的操作,到底是在哪個階段進行的賦值?* 情況1:在鏈接階段的準備環(huán)節(jié)賦值* 情況2:在初始化階段<clinit>()中賦值** 結論:* 在鏈接階段的準備環(huán)節(jié)賦值的情況:* 1. 對于基本數(shù)據(jù)類型的字段來說,如果使用 static final 修飾,則顯式賦值(直接賦值常量,而非調(diào)用方法)通常是在鏈接階段的準備環(huán)節(jié)進行* 2. 對于 String 來說,如果使用字面量的方式賦值,使用 static final 修飾的話,則顯式賦值通常是在鏈接階段的準備環(huán)節(jié)進行** 在初始化階段<clinit>()中賦值的情況* 排除上述的在準備環(huán)節(jié)賦值的情況之外的情況** 最終結論:使用 static + final 修飾,且顯示賦值中不涉及到方法或構造器調(diào)用的基本數(shù)據(jù)類型或String類型的顯式賦值,是在鏈接階段的準備環(huán)節(jié)進行*/ public class InitializationTest2 {public static int a = 1; //在初始化階段<clinit>()中賦值public static final int INT_CONSTANT = 10; //在鏈接階段的準備環(huán)節(jié)賦值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"; //在鏈接階段的準備環(huán)節(jié)賦值public static final String s1 = new String("helloworld1"); //在初始化階段<clinit>()中賦值}() 的線程安全性
對于 () 方法的調(diào)用,也就是類的初始化,虛擬機會在內(nèi)部確保其多線程環(huán)境中的安全性
虛擬機會保證一個類的 () 方法在多線程環(huán)境中被正確地加鎖、同步,如果多個線程同時去初始化一個類,那么只會有一個線程去執(zhí)行這個類的 () 方法,其他線程都需要阻塞等待,直到活動線程執(zhí)行 () 方法完畢
正是因為函數(shù) () 帶鎖線程安全的,因此,如果一個在類的 () 方法中有耗時很長的操作,就可能造成多個線程阻塞,引發(fā)死鎖。并且這種死鎖是很難發(fā)現(xiàn)的,因為看起來它們并沒有可用的鎖信息
如果之前的線程成功加載了類,則等在隊列中的線程就沒有機會再執(zhí)行 () 方法了。那么,當需要使用這個類時,虛擬機會直接返回給它已經(jīng)準備好的信息
類的初始化情況:主動使用 vs 被動使用
Java 程序?qū)︻惖氖褂梅譃閮煞N:主動使用 和 被動使用
一、主動使用
Class 只有在必須要首次使用的時候才會被裝載,Java 虛擬機不會無條件地裝載 Class 類型。Java 虛擬機規(guī)定,一個類或接口在初次使用前,必須要進行初始化。這里指的"使用",是指主動使用,主動使用只有下列幾種情況:(即:如果出現(xiàn)如下的情況,則會對類進行初始化操作。而初始化操作之前的加載、驗證、準備已經(jīng)完成)
針對5,補充說明:
當 Java 虛擬機初始化一個類時,要求它的所有父類都已經(jīng)被初始化,但是這條規(guī)則并不適用于接口
- 在初始化一個類時,并不會先初始化它所實現(xiàn)的接口
- 在初始化一個接口時,并不會先初始化它的父接口
因此,一個父接口并不會因為它的子接口或者實現(xiàn)類的初始化而初始化,只有當程序首次使用特定接口的靜態(tài)字段時,才會導致該接口的初始化
針對7,說明:
JVM 啟動的時候通過引導類加載器加載一個初始類。這個類在調(diào)用 public static void main(String[]) 方法之前被鏈接和初始化。這個方法的執(zhí)行將依次導致所需的類的加載、鏈接和初始化
二、被動使用
除了以上的情況屬于主動使用,其他的情況均屬于被動使用。被動使用不會引起類的初始化
也就是說:并不是在代碼中出現(xiàn)的類,就一定會被加載或者初始化。如果不符合主動使用的條件,類就不會初始化
如果針對代碼,設置參數(shù) -XX:+TraceClassLoading,可以追蹤類的加載信息并打印出來
過程四:類的Using(使用)
任何一個類型在使用之前都必須經(jīng)歷過完整的加載、鏈接和初始化3個類加載步驟。一旦一個類型成功經(jīng)歷過這3個步驟之后,便“萬事俱備,只欠東風”,就等著開發(fā)者使用了
開發(fā)人員可以在程序中訪問和調(diào)用它的靜態(tài)類成員信息(比如:靜態(tài)字段、靜態(tài)方法),或者使用 new 關鍵字為其創(chuàng)建對象實例
過程五:類的Unloading(卸載)
一、類、類的加載器、類的實例之間的引用關系
在類加載器的內(nèi)部實現(xiàn)中,用一個 Java 集合來存放所加載類的引用。另一方面,一個 Class 對象總是會引用它的類加載器,調(diào)用 Class 對象的 getClassLoader() 方法,就能獲得它的類加載器。由此可見,代表某個類的 Class 實例與其類的加載器之間為雙向關聯(lián)關系
一個類的實例總是引用代表這個類的 Class 對象。在 Object 類中定義了 getClass() 方法,這個方法返回代表對象所屬類的 Class 對象的引用。此外,所有的 Java 類都有一個靜態(tài)屬性 Class,它引用代表這個類的 Class 對象
二、類的生命周期
當 Sample 類被加載、鏈接和初始化后,它的生命周期就開始了。當代表 Sample 類的 Class 對象不再被引用,即不可觸及時,Class 對象就會結束生命周期,Sample 類在方法區(qū)內(nèi)的數(shù)據(jù)也會被卸載,從而結束 Sample 類的生命周期
一個類何時結束生命周期,取決于代表它的 Class 對象何時結束生命周期
三、具體例子
Loader1 變量和 obj 變量間接應用代表 Sample 類的 Class 對象,而 objClass 變量則直接引用它
如果程序運行過程中,將上圖左側(cè)三個引用變量都置為 null,此時 Sample 對象結束生命周期,MyClassLoader 對象結束生命周期,代表 Sample 類的 Class 對象也結束生命周期,Sample 類在方法區(qū)內(nèi)的二進制數(shù)據(jù)被卸載
當再次有需要時,會檢查 Sample 類的 Class 對象是否存在,如果存在會直接使用,不再重新加載;如果不存在 Sample 類會被重新加載,在 Java 虛擬機的堆區(qū)會生成一個新的代表 Sample 類的 Class 實例(可以通過哈希碼查看是否是同一個實例)
四、類的卸載
綜合以上三點,一個已經(jīng)加載的類型被卸載的幾率很小至少被卸載的時間是不確定的。同時我們可以看的出來,開發(fā)者在開發(fā)代碼時候,不應該對虛擬機的類型卸載做任何假設的前提下,來實現(xiàn)系統(tǒng)中的特定功能
回顧:方法區(qū)的垃圾回收
方法區(qū)的垃圾收集主要回收兩部分內(nèi)容:常量池中廢棄的常量和不再使用的類型
HotSpot 虛擬機對常量池的回收策略是很明確的,只要常量池中的常量沒有被任何地方引用,就可以被回收
判定一個常量是否"廢棄"還是相對簡單,而要判定一個類型是否屬于"不再被使用的類"的條件就比較苛刻了。需要同時滿足下面三個條件:
- 該類所有的實例都已經(jīng)被回收。也就是 Java 堆中不存在該類及其任何派生子類的實例
- 加載該類的類加載器已經(jīng)被回收。這個條件除非是經(jīng)過精心設計的可替換類加載器的場景,如 OSGI、JSP 的重加載等,否則通常是很難達成的
- 該類對應的 java.lang.Class 對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法
Java 虛擬機被允許對滿足上述三個條件的無用類進行回收,這里說的僅僅是"被允許",而并不是和對象一樣,沒有引用了就必然會回收
總結
以上是生活随笔為你收集整理的中yeti不能加载_第二十章_类的加载过程详解的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 后端技术:Java 泛型 T,E,K,V
- 下一篇: 音视频开发入门基础及视频会议即时通讯开源