上篇文章中觅闽,我們介紹了 .class 文件的結(jié)構(gòu)巷查,.class 文件只是一個靜態(tài)的文件疯坤,那 JVM 是加載 .class 文件是什么樣的一個過程呢编曼?這就涉及到 JVM 的類加載機制了,是本篇文章將要講解的內(nèi)容
所謂的 JVM 的類加載機制是指 JVM 把描述類的數(shù)據(jù)從 .class 文件加載到內(nèi)存畜侦,并對數(shù)據(jù)進行校驗元扔、轉(zhuǎn)換解析和初始化,最終形成可以被虛擬機直接使用的 Java 類型夏伊,這就是 JVM 的類加載機制
Java 語言中的加載摇展、連接、初始化都是在運行期完成的溺忧,這樣雖然對性能會有影響咏连,但是卻十分靈活。Java 語言的動態(tài)擴展性很強鲁森,其原因就是依賴于 Java 運行期動態(tài)加載和動態(tài)連接的特性祟滴,動態(tài)加載是指我們可以通過預定義的類加載器和自定義的類加載器在運行期從本地、網(wǎng)絡(luò)或其他地方加載 .class 文件歌溉;動態(tài)連接是指在面向接口編程中垄懂,在運行時才會指定其實際的實現(xiàn)類
一. 類加載的時機
首先講一下類的生命周期。
類的生命周期總共分為7個階段:加載痛垛、驗證草慧、準備、解析匙头、初始化漫谷、使用和卸載,如下圖所示蹂析,其中驗證舔示、準備、解析三個步驟又可統(tǒng)稱為連接电抚。
加載惕稻、驗證、準備蝙叛、初始化和卸載五個步驟的順序都是確定的俺祠,解析階段在某些情況下有可能發(fā)生在初始化之后,這是為了支持 Java 語言的運行期綁定的特性甥温。
在 JVM 虛擬機規(guī)范中并沒有規(guī)定加載的時機锻煌,但是卻規(guī)定了初始化的時機,而加載姻蚓、驗證、準備三個步驟是在初始化之前匣沼。有以下五種情況需要必須立即對類進行初始化
- 遇到 new狰挡、getstatic、putstatic 或 invokestatic 這 4 條字節(jié)碼指令時,如果類沒有進行過初始化加叁,則需要先觸發(fā)其初始化倦沧。生成這 4 條指令最常見的 Java 代碼場景是:使用 new 關(guān)鍵字實例化對象、讀取或設(shè)置一個類的靜態(tài)字段(被 final 修飾它匕、已在編譯期把結(jié)果放入到常量池的靜態(tài)字段除外)以及調(diào)用一個類的靜態(tài)方法的時候
- 使用 java.lang.reflect 包的方法對類進行反射調(diào)用的時候
- 當初始化一個類的時候展融,如果發(fā)現(xiàn)其父類還沒有被初始化過,則需要先觸發(fā)其父類的初始化
- 當虛擬機啟動時豫柬,用戶需要指定一個要執(zhí)行的主類(包含 main() 方法的類)告希,虛擬機會先初始化這個主類
- 當使用 JDK 1.7 的動態(tài)語言支持時,如果一個 java.lang.invoke.MethodHandle 實例最后的解析結(jié)果 REF_getStatic烧给、REF_putStatic燕偶、REF_invodeStatic 的方法句柄,并且這個方法句柄所對應的類沒有進行過初始化础嫡,則需要先觸發(fā)其初始化(說實話指么,這句話我也沒理解。榴鼎。)
主動引用:上面這五種行為稱為對一個類的的主動引用伯诬,會觸發(fā)類的初始化
被動引用:除上面五種主動引用之外,其他引用類的方式都不會觸發(fā)類的初始化巫财,稱為類的被動引用
下面舉幾個被動引用的例子:
1.1 被動引用示例一
首先看段代碼盗似,如下所示:
package com.lijiankun24.classpractice;
public class SuperClass {
static {
System.out.println("SuperClass init!");
}
public static int value = 24;
}
public class SubClass extends SuperClass {
static {
System.out.println("SubClass init!");
}
}
public class Demo {
public static void main(String[] args){
System.out.println("The value is " + Subclass.value);
}
}
上面代碼運行之后輸出結(jié)果如下所示
SuperClass init!
The value is 24
對于靜態(tài)字段,只有直接定義這個字段的類會被初始化翁涤,如果是通過子類引用父類的字段桥言,父類會被初始化,子類不一定會被初始化葵礼,子類會不會被初始化 JVM 虛擬機規(guī)范并沒有明確規(guī)定号阿,取決于虛擬機的具體實現(xiàn)
1.2 被動引用示例二
有如下代碼
public class SubClass {
static {
System.out.println("SubClass init!");
}
}
public class Demo {
public static void main(String[] args){
SubClass[] subClassArray = new SubClass[10];
}
}
上面代碼運行之后,并不會輸出 "SubClass init!"鸳粉,因為在上面 Demo#main() 方法中扔涧,并沒有初始化 SubClass 類届谈,而是初始化了一個 SubClass[] 數(shù)組類枯夜,SubClass[] 數(shù)組類代表了一個元素類型為 SubClass 的一維數(shù)組,繼承自 Object 類艰山,由 newarray 字節(jié)碼創(chuàng)建湖雹。
1.3 被動引用示例三
public class Constant {
static {
System.out.println("Constant init!");
}
public static final String VALUE = "Hello World!";
}
public class Demo {
public static void main(String[] args){
System.out.println(Constant.VALUE);
}
}
上面代碼運行之后也并不會輸出 "Constant init!",因為這涉及到一個概念 ---- “常量傳播優(yōu)化”曙搬。雖然在代碼中摔吏,Demo 類引用了 Constant 類中的常量 VALUE鸽嫂,但是在編譯階段,會將 VALUE 的實際值 "Hello World!" 放到 Demo 類中的常量池中征讲,Demo 類每次使用 "Hello World!" 常量的時候都會從自己的常量池中去找据某。Demo 類不會持有 Constant 類的符號引用,所以 Constant 類也并不會被初始化诗箍。
二. 類加載的過程
2.1. 加載
在加載階段有三個步驟:
- 通過一個類的全限定名獲取定義此類的二進制字節(jié)流
- 將二進制字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)換為方法區(qū)中的運行時數(shù)據(jù)結(jié)構(gòu)
- 在內(nèi)存中生成一個代表此類的 java.lang.Class 的對象癣籽,作為方法區(qū)中這個類的訪問入口
在這個階段,有兩點需要注意:
- 并沒有規(guī)定從哪里獲取二進制字節(jié)流滤祖。我們可以從 .class 靜態(tài)存儲文件中獲取筷狼,也可以從 zip、jar 等包中讀取氨距,可以從數(shù)據(jù)庫中讀取桑逝,也可以從網(wǎng)絡(luò)中獲取,甚至我們自己可以在運行時自動生成俏让。
- 在內(nèi)存中實例化一個代表此類的 java.lang.Class 對象之后楞遏,并沒有規(guī)定此 Class 對象是方法 Java 堆中的,有些虛擬機就會將 Class 對象放到方法區(qū)中首昔,比如 HotSpot寡喝。
2.2 驗證
驗證是連接階段的第一個步驟,驗證的目的是為了確保 .class 文件中的字節(jié)流所包含的信息是符合當前虛擬機的要求勒奇,并且不會危害到虛擬機自身的安全的预鬓。
驗證主要包括四個方面的驗證:文件格式驗證、元數(shù)據(jù)驗證赊颠、字節(jié)碼驗證和符號引用驗證格二。
- 文件格式驗證:主要驗證二進制字節(jié)流數(shù)據(jù)是否符合 .class 文件的規(guī)范,并且該 .class 文件是否在本虛擬機的處理范圍之內(nèi)(版本號驗證)竣蹦。只有通過了文件格式的驗證之后顶猜,二進制的字節(jié)流才會進入到內(nèi)存中的方法區(qū)進行存儲。而且只有通過了文件格式驗證之后痘括,才會進行后面三個驗證长窄,后面三個驗證都是基于方法區(qū)中的存儲結(jié)構(gòu)進行的
- 元數(shù)據(jù)驗證:主要是對類的元數(shù)據(jù)信息進行語義檢查,保證不存在不符合 Java 語義規(guī)范的元數(shù)據(jù)信息
- 字節(jié)碼驗證:字節(jié)碼驗證是整個驗證中最復雜的一個過程纲菌,在元數(shù)據(jù)驗證中挠日,驗證了元數(shù)據(jù)信息中的數(shù)據(jù)類型做完校驗后,字節(jié)碼驗證主要對類的方法體進行校驗分析翰舌,保證被校驗的類的方法不會做出危害虛擬機的行為
- 符號引用驗證:符號引用驗證發(fā)生在連接的第三個階段解析階段中嚣潜,主要是保證解析過程可以正確地執(zhí)行。符號引用驗證是類本身引用的其他類的驗證椅贱,包括:通過一個類的全限定名是否可以找到對應的類郑原,訪問的其他類中的字段和方法是否存在唉韭,并且訪問性是否合適等
2.3 準備
在準備階段所做的工作就是夜涕,在方法區(qū)中為類 Class 對象的類變量分配內(nèi)存并初始化類變量犯犁,有三點需要注意:
- 在方法區(qū)中分配內(nèi)存的只有類變量(被 static 修飾的變量),而不包括實例變量女器,實例變量將會跟隨著對象在 Java 堆中為其分配內(nèi)存
- 初始化類變量的時候酸役,是將類變量初始化為其類型對應的 0 值,比如有如下類變量驾胆,在準備階段完成之后涣澡,val 的值是 0 而不是 123,為 val 復制為 123丧诺,是在后面要講的初始化階段之后
public static int val = 123;
- 對于常量入桂,其對應的值會在編譯階段就存儲在字段表的 ConstantValue 屬性當中,所以在準備階段結(jié)束之后驳阎,常量的值就是 ConstantValue 所指定的值了抗愁,比如如下,在準備階段結(jié)束之后镜廉,val 的值就是 123 了吧凉。
public static final int val = 123;
2.4 解析
解析是將符號引用解析為直接引用的過程赞咙,符號引用是指在 .class 常量池中存儲的 CONSTANT_Class_info、CONSTANT_Fieldref_info 等常量撮珠,直接引用則是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄金矛,如果有了直接引用芯急,那引用的目標必定已經(jīng)在內(nèi)存中了。對于解析有以下 3 點需要注意:
- 虛擬機規(guī)范中并未規(guī)定解析階段發(fā)生的具體時間驶俊,只規(guī)定了在執(zhí)行newarray娶耍、new、putfidle废睦、putstatic伺绽、getfield、getstatic 等 16 個指令之前嗜湃,對它們所使用的符號引用進行解析奈应。所以虛擬機可以在類被加載器加載之后就進行解析,也可以在執(zhí)行這幾個指令之前才進行解析
- 對同一個符號引用進行多次解析是很常見的事购披,除 invokedynamic 指令以外杖挣,虛擬機實現(xiàn)可以對第一次解析的結(jié)果進行緩存,以后解析相同的符號引用時刚陡,只要取緩存的結(jié)果就可以了
- 解析動作主要對類或接口惩妇、字段株汉、類方法、接口方法歌殃、方法類型乔妈、方法句柄和調(diào)用點限定符 7 類符號引用進行解析
2.5 初始化
類的初始化階段才是真正開始執(zhí)行類中定義的 Java 程序代碼。初始化說白了就是調(diào)用類構(gòu)造器 <clinit>() 的過程氓皱,在類的構(gòu)造器中會為類變量初始化定義的值路召,會執(zhí)行靜態(tài)代碼塊中的內(nèi)容。下面將介紹幾點和開發(fā)者關(guān)系較為緊密的注意點
- 類構(gòu)造器 <clinit>() 是由編譯器自動收集類中出現(xiàn)的類變量波材、靜態(tài)代碼塊中的語句合并產(chǎn)生的股淡,收集的順序是在源文件中出現(xiàn)的順序決定的,靜態(tài)代碼塊可以訪問出現(xiàn)在靜態(tài)代碼塊之前的類變量廷区,出現(xiàn)的靜態(tài)代碼塊之后的類變量唯灵,只可以賦值,但是不能訪問隙轻,比如如下代碼
public class Demo { private static String before = "before"; static { after = "after"; // 賦值合法 System.out.println(before); // 訪問合法埠帕,因為出現(xiàn)在 static{} 之前 System.out.println(after); // 訪問不合法,因為出現(xiàn)在 static{} 之后 } private static String after; }
- <clinit>() 類構(gòu)造器和<init>()實例構(gòu)造器不同大脉,類構(gòu)造器不需要顯示的父類的類構(gòu)造搞监,在子類的類構(gòu)造器調(diào)用之前,會自動的調(diào)用父類的類構(gòu)造器镰矿。因此虛擬機中第一個被調(diào)用的 <clinit>() 方法是 java.lang.Object 的類構(gòu)造器
- 由于父類的類構(gòu)造器優(yōu)先于子類的類構(gòu)造器執(zhí)行琐驴,所以父類中的 static{} 代碼塊也優(yōu)先于子類的 static{} 執(zhí)行
- 類構(gòu)造器<clinit>() 對于類來說并不是必需的,如果一個類中沒有類變量秤标,也沒有 static{}绝淡,那這個類不會有類構(gòu)造器 <clinit>()
- 接口中不能有 static{},但是接口中也可以有類變量苍姜,所以接口中也可以有類構(gòu)造器 <clinit>{}牢酵,但是接口的類構(gòu)造器和類的類構(gòu)造器有所不同,接口在調(diào)用類構(gòu)造器的時候衙猪,如果不需要馍乙,不用調(diào)用父接口的類構(gòu)造器,除非用到了父接口中的類變量垫释,接口的實現(xiàn)類在初始化的時候也不會調(diào)用接口的類構(gòu)造器
- 虛擬機會保證一個類的 <clinit>() 方法在多線程環(huán)境中被正確地加鎖丝格、同步,如果多個線程同時去初始化一個類棵譬,那么只有一個線程去執(zhí)行這個類的類構(gòu)造器 <clinit>()显蝌,其他線程會被阻塞,直到活動線程執(zhí)行完類構(gòu)造器 <clinit>() 方法
三. 類加載器
關(guān)于類加載器订咸,我們帶著幾個問題去學習曼尊,什么是類加載器酬诀,類加載器分為哪幾類,提到 JVM 類加載器就會提到雙親委派模型骆撇,雙親委派模型有什么好處呢瞒御?我們就一一來解答這幾個問題
3.1 類加載器
類加載器是完成"通過一個類的全限定名獲取這個類的二進制字節(jié)流"的工作,類加載器是獨立于虛擬機之外存在的艾船。
對于每一個類葵腹,都需要加載這個類的類加載器和類本身來確認這個類在 JVM 虛擬機中的唯一性,每個類加載器都有獨立的類名稱空間屿岂。換句話說,比較兩個類是否“相等”鲸匿,必須是在這兩個類是由同一個類加載器加載的前提下比較爷怀,如果兩個類是由不同的類加載器加載的,即使它們兩個來自于同一個 .class 文件带欢,那這兩個類也是不同的运授。
3.2 類加載器的分類
從不同的角度看,加載器可以有不同的分類方式乔煞。
- 從 Java 虛擬機角度來看呢吁朦,存在兩種不同的類加載器
- 一種是啟動類加載器(Bootstrap ClassLoader),這個類是由 C++ 語言實現(xiàn)的渡贾, 是虛擬機本身的一部分
- 另一種是除了啟動類加載器之外逗宜,所有的其他類加載器,是由 Java 語言實現(xiàn)的空骚,是獨立于 Java 虛擬機之外的纺讲,并且全部繼承自抽象類 java.lang.ClassLoader
- 從 Java 開發(fā)人員的角度來看的,可以分為以下 3 種類加載器
- 啟動類加載器(Bootstrap ClassLoader):加載 <JAVA_HOME>\lib 目錄下和 -Xbootclasspath 參數(shù)所指定的可以被虛擬機識別的類庫到內(nèi)存中
- 擴展類加載器(Extension ClassLoader):加載 <JAVA_HOME>\lib\ext 目錄中的和 java.ext.dirs 系統(tǒng)變量所指定的路徑中的類庫加載到內(nèi)存中
- 應用程序類加載器(Application ClassLoader):加載用戶類路徑上所指定的類庫囤屹,開發(fā)者可以直接使用這個類加載器熬甚,是默認的類加載器
我們的應用程序就是由上面三種類加載器相互配合被加載進來的,如果有必要可以自定義類加載器
3.3 雙親委派模型
對于上面提到的 3 中類加載器肋坚,他們有如下圖所示的關(guān)系
對于上圖所示的這種關(guān)系呢乡括,我們就稱之類類加載器的雙親委派模型。在雙親委派模型中智厌,除了頂層的 Bootstrap ClassLoader 之外诲泌,其他的類加載器都有自己的父加載器。
雙親委派模型的工作流程是這樣的峦剔,如果一個類加載器收到了一個加載類的請求档礁,會首先把這個請求委派給自己的父加載器去加載,這樣的所有的類加載請求都會向上傳遞到 Bootstrap ClassLoader 中去吝沫,只有當父類加載器無法完成這個類加載請求時呻澜,才會讓子類加載器去處理這個請求递礼。
使用雙親委派模型的好處就是,被加載到虛擬機中的類會隨著加載他們的類加載器有一種優(yōu)先級的層次關(guān)系羹幸。比如脊髓,開發(fā)者自定義了一個 java.lang.Object 的類,但是你會發(fā)現(xiàn)栅受,自定義的 java.lang.Object 永遠無法被調(diào)用将硝,因為在使用自定義的類加載器去加載這個類的時候,自定義的類加載器會將加載請求傳遞到 Bootstrap ClassLoader 中去屏镊,在 Bootstrap ClassLoader 中會從 rt.jar 中加載 Java 本身自帶的 Java.lang.Object依疼,這個時候加載請求已經(jīng)完成,找到了這個類而芥,就不需要自定義的 ClassLoader 去加載用戶路徑下的 java.lang.Object 這個類了律罢。
雙親委派模型對于 Java 程序的穩(wěn)定運行十分重要,實現(xiàn)卻非常簡單
- 首先會判斷是否已經(jīng)加載過此類了棍丐,如果已經(jīng)加載過就不用再加載了
- 如果沒有加載過误辑,則調(diào)用父類加載器去加載
- 若父類加載器為空,則默認使用啟動類加載器作為父類加載器加載
- 若父類加載器加載未能成功會拋出 ClassNotFoundException 的異常
- 再調(diào)用自己的 findClass() 方法進行加載
如下代碼所示:
protected synchronized Class<?> loadClass(String name, boolean resolve) throw ClassNotFoundException {
Class c = findLoadedClass(name);
if(c == null){
try{
if(parent != null){
c = parent.loadClass(name, resolve);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e){
}
if(c == null){
c = findClass(name);
}
}
if(resolve){
resolveClass(c);
}
return c;
}
寫在最后歌逢,在這篇文章中巾钉,我們對類的生命周期中的各個階段進行了詳細的介紹,并且對類的加載器及雙親委派模型進行了詳細的介紹秘案。上篇文章介紹了 .class 文件中的內(nèi)容砰苍,這篇文章介紹了 Java 虛擬機怎么加載 .class 文件,并且介紹了類的生命周期踏烙,下篇文章將會介紹在 Java 虛擬機中字節(jié)碼是怎么執(zhí)行的师骗。