【死磕JVM】五年 該知道JVM加載機制了!
本文轉(zhuǎn)載自微信公眾號「牧小碼農(nóng)」,作者牧小農(nóng)。轉(zhuǎn)載本文請聯(lián)系牧小碼農(nóng)公眾號。
類加載
Java虛擬機類加載過程是把Class類文件加載到內(nèi)存,并對Class文件中的數(shù)據(jù)進行校驗、轉(zhuǎn)換解析和初始化,最終形成可以被虛擬機直接使用的java類型的過程
和那些編譯時需要連接工作的語言不同,在Java語言里,類型的加載,連接和初始化過程都是在程序 運行期間完成的,這種策略雖然會令類加載時稍微增加一些性能開銷,但是會為java應(yīng)用程序提供比較高的靈活性。
當(dāng)我們使用到某個類的時候,如果這個類還未從磁盤上加載到內(nèi)存中,JVM就會通過三步走策略(加載、連接、初始化)來對這個類進行初始化,JVM完成這三個步驟的名稱,就叫做類加載或者類初始化
類加載的時機
什么情況下需要開始類加載的第一個階段——加載 ,在Java虛擬機規(guī)范中沒有進行強制約束,而是交給虛擬機的具體實現(xiàn)來進行把握,但是對于初始化階段,虛擬機規(guī)范嚴(yán)格規(guī)定了 “有且只有” 五種情況必須立即對類進行初始化(而加載、驗證、準(zhǔn)備自然需要在此之前開始),具體情況如下所示:
class文件的加載時機:
序號 | 內(nèi)容 |
---|---|
1 | 遇到 new、getstatic、putstatic、或invokestatic這四條字節(jié)碼指令 |
2 | 使用 java.lang.reflect 包的方法對類進行反射調(diào)用的時候 |
3 | 初始化類時,父類沒有被初始化,先初始化父類 |
4 | 虛擬機啟動時,用戶指定的主類(包含main()的那個類) |
5 | 當(dāng)使用JDK1.7動態(tài)語言支持的時,如果一個java.lang.invoke.MethodHandle 實例最后解析的結(jié)果REFgetStatic、REFputStatic、REF_invokeStatic的方法句柄,并且這個方法句柄鎖對應(yīng)的類沒有進行過初始化時 |
關(guān)于序號1的詳細(xì)解釋:
- 使用 new 關(guān)鍵字實例化對象時
- 讀取類的靜態(tài)變量時(被 final修飾,已在編譯期把結(jié)果放入常量池的靜態(tài)字段除外)
- 設(shè)置類的靜態(tài)變量時
- 調(diào)用一個類的靜態(tài)方法時
注意: newarray指令觸發(fā)的只是數(shù)組類型本身的初始化,而不會導(dǎo)致其相關(guān)類型的初始化,比如, newString[]只會直接觸發(fā) String[]類的初始化,也就是觸發(fā)對類 [Ljava.lang.String的初始化,而直接不會觸發(fā) String類的初始化。
生成這四條指令最常見的Java代碼場景是:
對于這5種會觸發(fā)類進行初始化的場景,虛擬機規(guī)范中使用了一個很強烈的限定語:“有且只有”,這5種場景中的行為稱為對一個類進行主動引用。除此之外,所有引用類的方式都不會觸發(fā)初始化,稱為 被動引用。
需要特別指出的是,類的實例化和類的初始化是兩個完全不同的概念:
- 類的實例化是指創(chuàng)建一個類的實例(對象)的過程;
- 類的初始化是指為類各個成員賦初始值的過程,是類生命周期中的一個階段;
被動引用的三個場景:
通過子類引用父類的靜態(tài)字段,不會導(dǎo)致子類初始化
- /**
- * @program: jvm
- * @ClassName Test1
- * @Description:通過子類引用父類的靜態(tài)字段,不會導(dǎo)致子類初始化
- * @author: 牧小農(nóng)
- * @create: 2021-02-27 11:42
- * @Version 1.0
- **/
- public class Test1 {
- static {
- System.out.println("Init Superclass!!!");
- }
- public static void main(String[] args) {
- int x = Son.count;
- }
- }
- class Father extends Test1{
- static int count = 1;
- static {
- System.out.println("Init father!!!");
- }
- }
- class Son extends Father{
- static {
- System.out.println("Init son!!!");
- }
- }
輸出:
- Init Superclass!!!
- Init father!!!
對于靜態(tài)字段,只有直接定義這個字段的類才會被初始化,因此通過其子類來引用父類中定義的靜態(tài)字段,只會觸發(fā)父類的初始化而不會觸發(fā)子類的初始化。至于是否要觸發(fā)子類的加載和驗證,在虛擬機中并未明確規(guī)定,這點取決于虛擬機的具體實現(xiàn)。對于Sun HotSpot虛擬機來說,可通過-XX:+TraceClassLoading參數(shù)觀察到此操作會導(dǎo)致子類的加載。
上面的案例中,由于count字段是在Father類中定義的,因此該類會被初始化,此外,在初始化類Father的時候,虛擬機發(fā)現(xiàn)其父類Test1 還沒被初始化,因此虛擬機將先初始化其父類Test1 ,然后初始化子類Father,而Son始終不會被初始化;
通過數(shù)組定義來引用類,不會觸發(fā)此類的初始化
- /**
- * @program: jvm
- * @ClassName Test2
- * @description:
- * @author: muxiaonong
- * @create: 2021-02-27 12:03
- * @Version 1.0
- **/
- public class Test2 {
- public static void main(String[] args) {
- M[] m = new M[8];
- }
- }
- class M{
- static {
- System.out.println("Init M!!!");
- }
- }
運行之后我們會發(fā)現(xiàn)沒有輸出 "Init M!!!",說明沒有觸發(fā)類的初始化階段
常量在編譯階段會存入調(diào)用類的常量池中,本質(zhì)上并沒有直接引用到定義常量的類,因此不會觸發(fā)定義常量的類的初始化
- /**
- * @program: jvm
- * @ClassName Test3
- * @description:
- * @author: muxiaonong
- * @create: 2021-02-27 12:05
- * @Version 1.0
- **/
- public class Test3 {
- public static void main(String[] args) {
- System.out.println(ConstClass.COUNT);
- }
- }
- class ConstClass{
- static final int COUNT = 1;
- static{
- System.out.println("Init ConstClass!!!");
- }
- }
上面代碼運行后也沒有輸出 InitConstClass!!!,這是因為雖然在Java源碼中引用了ConstClass 類中的常量COUNT ,但其實在編譯階段通過常量傳播優(yōu)化,已經(jīng)將常量的值 "1"存儲到Test3 常量池中了,對常量ConstClass.COUNT的引用實際都被轉(zhuǎn)化為Test3 類對自身常量池的引用了,也就是說,實際上Test3 的Class文件之中并沒有ConstClass類的符號引用入口,這兩個類在編譯為Class文件之后就不存在關(guān)系
類加載過程
有一個名叫Class文件,它靜靜的躺在了硬盤上,吃香的喝辣的,他究竟需要一個怎么樣的過程經(jīng)歷了什么,才能夠從舒服的硬盤中到內(nèi)存中呢?class進入內(nèi)存總共有三大步。
- 加載(Loading)
- 連接(Linking)
- 初始化(Initlalizing)
1、加載
加載 是 類加載(Class Loading) 過程的一個階段,加載 是 類加載(Class Loading) 過程的一個階段,加載是指將當(dāng)前類的class文件讀入內(nèi)存中,并且創(chuàng)建一個 java.lang.Class的對象,也就是說,當(dāng)程序中使用任何類的時候,系統(tǒng)都會創(chuàng)建一個叫 java.lang.Class對象
在加載階段,虛擬機需要完成以下三個事情:
通過一個類的全限定名類獲取定義此類的二進制字節(jié)流(沒有指明只能從一個Class文件中獲取,可以從其他渠道,如:網(wǎng)絡(luò)、動態(tài)生成、數(shù)據(jù)庫等)
將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu)
在內(nèi)存中生成一個代表這個類的 java.lang.Class對象,作為方法區(qū)這個類的各種數(shù)據(jù)的訪問入口
類加載器通常無須等到“首次使用”該類時才加載該類,Java虛擬機規(guī)范允許系統(tǒng)預(yù)先加載某些類。加載階段與連接階段的部分內(nèi)容是交叉進行的,加載階段尚未完成,連接階段可能已經(jīng)開始,但這些夾在夾在階段之中進行的動作,仍然屬于連接階段的內(nèi)容,這兩個階段的開始時間仍然保持著固定的先后順序。
2、連接
當(dāng)類被加載之后,系統(tǒng)會生成一個對應(yīng)的Class對象,就會進入 連接階段,連接階段負(fù)責(zé)把類的二進制數(shù)據(jù)合并到JRE中,連接階段又分為三個小階段
1.1 驗證
驗證是連接階段的第一步,這一階段的主要目的是為了確保Class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機的要求,并且不會危害虛擬機自身的安全。Java語言相對于 C/C++ 來說本身是相對安全的語言,驗證階段是非常重要的,這個階段是否嚴(yán)謹(jǐn),決定了Java虛擬機能不能承受惡意代碼的攻擊,當(dāng)驗證輸入的字節(jié)流不符合Class文件格式的約束時,虛擬機會拋出一個 java.lang.VerifyError異?;蛘咦宇惍惓?,從大體來說驗證主要分為四個校驗動作:文件格式驗證、元數(shù)據(jù)驗證、字節(jié)碼驗證、符號引用驗證
文件格式驗證: 主要驗證字節(jié)流是否符合Class文件格式的規(guī)范,并且能被當(dāng)前版本的虛擬機處理。主要包含以下幾個方面:
- 文件格式是否以 CAFEBABE開頭
- 主次版本是否在虛擬機處理的范圍內(nèi)
- 常量池的常量是否有不被支持的常量類型
- 指向常量的各種索引值是否有指向不存在的常量或者不符合類型的常量
- CONSTANTUtf8info 型的常量是否有不符合UTF8編碼的數(shù)據(jù)
- Class文件中各個部分及文件本身是否有被刪除的活附件的信息
元數(shù)據(jù)驗證: 主要是對字節(jié)碼描述的信息進行語義分析,主要目的是對類的元數(shù)據(jù)進行語義校驗,分析是否符合Java的 語言語法的規(guī)范,保證不存在不符合Java語言的規(guī)范的元數(shù)據(jù)的信息,該階段主要驗證的方面包含以下幾個方面:
- 這個類是否有父類(除java.lang.Object)
- 這個類的父類是否繼承了不允許被繼承的類(被final 修飾的類)
- 如果這個類不是抽象類,是否實現(xiàn)了父類或接口之中要求的所有方法
- 類中的字段、方法是否和父類產(chǎn)生矛盾
字節(jié)碼驗證: 最重要也是最復(fù)雜的校驗環(huán)節(jié),通過數(shù)據(jù)流和控制流分析程序語義是否合法、符合邏輯的。主要針對類的方法體進行校驗分析,保證被校驗的類在運行時不會危害虛擬機安全的事情
保證任何時候操作數(shù)棧的數(shù)據(jù)類型和指令代碼序列都能配合工作(例如在操作棧上有一個int類型的數(shù)據(jù),保證不會在使用的時候按照long類型來加載到本地變量表中)
跳轉(zhuǎn)指令不會條狀到方法體以外的字節(jié)碼指令上
保證方法體中的數(shù)據(jù)轉(zhuǎn)換是有效的,例如可以把一個子類對象賦值給父類數(shù)據(jù)類型,但是不能把父類賦值給子類數(shù)據(jù)類型
符號引用驗證: 針對符號引用轉(zhuǎn)換直接引用的時候,這個裝換工作會在第三階段(字節(jié)碼驗證)解析階段中發(fā)生。主要是保證引用一定會被訪問到,不會出現(xiàn)類無法訪問的問題。
1.2 準(zhǔn)備
為類變量 分配內(nèi)存并設(shè)置類變量初始值的階段,這些變量所使用的內(nèi)存都會在方法區(qū)進行分配,在準(zhǔn)備階段是把class文件靜態(tài)變量賦默認(rèn)值,注意:不是賦初始值,比如我們 publicstaticinti=8,在這個步驟 并不是把 i 賦值成8 ,而是先賦值為0
基本類型的默認(rèn)值:
數(shù)據(jù)類型 | 默認(rèn)值 |
---|---|
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的初始值變?yōu)?,
1.3 解析
把class文件常量池里面用到的符號引用轉(zhuǎn)換為直接內(nèi)存地址,直接可以訪問到的內(nèi)容 符號引用:以一組符號來描述所引用的目標(biāo),符號可以是任何字面形式的字面量,只要不會出現(xiàn)沖突能夠定位到就可以 直接引用:可以是直接指向目標(biāo)的指針、相對偏移量或是一個能間接定位到目標(biāo)的句柄,如果有了直接引用,那引用的目標(biāo)必定已經(jīng)在內(nèi)存中存在了
3、初始化
初始化是給類的靜態(tài)變量賦正確的初始值,剛才我們有講到準(zhǔn)備階段是復(fù)制默認(rèn)值,而初始化是給靜態(tài)變量賦值初始值,看下面的語句:
- public static int i = 8
首先字節(jié)碼文件被加載到內(nèi)存后,先進行連接驗證,通過準(zhǔn)備階段,給i分配內(nèi)存,因為是static,所以這個時候i 等于int類型的默認(rèn)初始值是0,所以i 現(xiàn)在是 0,到了初始化的時候,才會真正把i 賦值為8
類加載器
類加載器負(fù)責(zé)加載所有的類,并且為載入內(nèi)存中的類生成一個 java.lang.Class實例對象,如果一個類被加載到JVM中后,同一個類不會再次被載入,就像對象有一個唯一的標(biāo)識,同樣載入的JVM的類也有一個唯一的標(biāo)識。JVM本身有一個類加載器的層次,這個類加載器本身就是一個普通的Class,所有的Class都是被類加載器加載到內(nèi)存中,我們可以稱之為ClassLoader,一個頂級的父類,也是一個abstract抽象類。
Bootstrap: 類加載器的加載過程,分成不同的層次來進行加載,不同的類加載器加載不同的Class,作為最頂層的Bootstrap,它加載lib里JDK最核心的內(nèi)容,比如說rt.jar charset.jar等核心類,當(dāng)我們調(diào)用getClassLoader()拿到這個加載器結(jié)果是一個Null的時候,代表我們已經(jīng)達到了最頂層的加載器
Extension: Extension加載器擴展類,加載擴展包里的各種各樣的文件,這些擴展包在JDK安裝目錄 jre/lib/ext下的jar
App: 就是我們平時用到的application ,用來加載classpath指定的內(nèi)容
Custom ClassLoader: 自定義ClassLoader,加載自己自定義的加載器 Custom ClassLoader 的父類加載器是 application 的父類加載器是 Extension的父類加載器是Bootstrap
注意:他們不是繼承關(guān)系,而是委托關(guān)系
- public class ClassLoaderTest {
- public static void main(String[] args) {
- // 查看是誰Load到內(nèi)存的,執(zhí)行結(jié)果是null,因為Bootstrap使用C++實現(xiàn)的
- // 在Java里面沒有class和它對應(yīng)
- System.out.println(String.class.getClassLoader());
- //這個是核心類庫某個包里的類執(zhí)行,執(zhí)行結(jié)果是Null,因為該類也是被Bootstrap加載的
- System.out.println(sun.awt.HKSCS.class.getClassLoader());
- //這個類是位于ext目錄下某個jar文件里面,當(dāng)我們調(diào)用他執(zhí)行結(jié)果就是sun.misc.Launcher$ExtClassLoader@a09ee92
- System.out.println(sun.net.spi.nameservice.dns.DNSNameService.class.getClassLoader());
- // 這個是我們自己寫的ClassLoad加載器,由sun.misc.Launcher$AppClassLoader@18b4aac2加載
- System.out.println(ClassLoaderTest.class.getClassLoader());
- // 是Exe的ClassLoader 調(diào)用它的getclass(),它本身也是一個class,調(diào)用它的getClassLoader,他的ClassLoader的ClassLoader就是我們的Bootstrap所以結(jié)果為Null
- System.out.println(sun.net.spi.nameservice.dns.DNSNameService.class.getClassLoader().getClass().getClassLoader());
- }
- }
類加載器繼承關(guān)系
這個圖講的是ClassLoader從語法上是從誰繼承的,這個圖只是單純的一個語法關(guān)系,不是繼承關(guān)系,大家可以記住,和上面的類加載沒有一點關(guān)系,過分的大家其實可以忽略這個圖
雙親委派
父加載器: 父加載器不是"類加載器的加載器",也不是"類加載器的父類加載器" 雙親委派是一個孩子向父親的方向,然后父親向孩子方向的雙親委派過程
當(dāng)一個類加載器收到了類加載請求時候,他會先嘗試從自定義里面去找,同時它內(nèi)部還維護了緩存,如果在緩存中找到了就直接返回結(jié)果,如果沒有找到,就向父類進行委托,父類再去緩存中找,一直到最頂級的父類,如果這個時候還沒有從緩存中獲取到我們想要的結(jié)果,這個時候父親就說我你這個事情,我辦不了,你要自己動,然后兒子就自己去查詢對應(yīng)的class類并加載,如果到了最小的一個兒子還是沒有找到對應(yīng)的類,就會拋出異常 Class Not Found Exception
為什么要弄雙親委派?
這個是類加載器必問的一個面試題。
主要為了安全,如果任何一個Class都可以把他load到內(nèi)存中的話,那么我寫一個 java.lang.String,如果我寫入了有危險的代碼,是不是就會發(fā)生安全問題,并且可以保證Java核心api中定義的類型不會被隨意替換,可以防止API內(nèi)庫被隨意更改,其次是效率問題,如果有緩存在,直接從緩存里面拿,就不用一遍一遍的去遍歷查詢我們的父類或者子類了。