所謂類加載機(jī)制,就是虛擬機(jī)將Class文件加載到內(nèi)存,對數(shù)據(jù)進(jìn)行校驗(yàn)、解析、初始化,然后轉(zhuǎn)化為可被虛擬機(jī)使用的數(shù)據(jù)類型的過程
與靜態(tài)連接的語言不通,Java采用動(dòng)態(tài)連接方式,這種策略在運(yùn)行時(shí)雖然會(huì)增加一些性能開銷,但是卻給程序提供了高度的靈活性。比如我們可以通過自定義類加載器的方式,在運(yùn)行時(shí)通過網(wǎng)絡(luò)進(jìn)行類加載;也可以在運(yùn)行時(shí)為一個(gè)接口指定其實(shí)現(xiàn)類
類加載時(shí)機(jī)
類的整個(gè)生命周期包括:加載、驗(yàn)證、準(zhǔn)備、解析、初始化、使用和卸載。其中驗(yàn)證、準(zhǔn)備、解析這3個(gè)階段稱為連接階段
加載、驗(yàn)證、準(zhǔn)備、初始化和卸載,這5個(gè)階段必須按照順序開始,但并不要求按照這個(gè)順序完成,因?yàn)檫@些階段通常會(huì)在某一個(gè)階段執(zhí)行的過程中被調(diào)用或激活

類加載過程
1.加載
在加載階段,虛擬機(jī)需要完成3件事:
(1)通過類的全限定名來獲取該類的二進(jìn)制字節(jié)流
(2)將二進(jìn)制字節(jié)流轉(zhuǎn)化為運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)
(3)在內(nèi)存中生成java.lang.Class對象供后續(xù)使用
對于加載這個(gè)階段,非數(shù)組類和數(shù)組類的情況有所不同:
#對于非數(shù)組類,加載階段既可以使用系統(tǒng)提供的引導(dǎo)類加載器完成,也可以使用自定義的類加載器完成
#對于數(shù)組類,它本身并不通過類加載器加載,而是由虛擬機(jī)直接加載。但是數(shù)組類的元素類型依然需要靠類加載器加載
2.驗(yàn)證
驗(yàn)證是連接階段的第一步,目的是確保Class字節(jié)流符合當(dāng)前虛擬機(jī)的要求。雖然編譯器本身可以編譯出合法的字節(jié)碼,但是字節(jié)碼的來源并不一定是編譯器(比如直接通過十六進(jìn)制編輯器編輯),因此虛擬機(jī)并不會(huì)完全信任字節(jié)碼,需要對其進(jìn)行驗(yàn)證
驗(yàn)證過程大致可以分為4個(gè)階段:文件格式驗(yàn)證、元數(shù)據(jù)驗(yàn)證、字節(jié)碼驗(yàn)證、符號引用驗(yàn)證
#文件格式驗(yàn)證
這一階段要驗(yàn)證字節(jié)流是否符合類文件格式的規(guī)范(關(guān)于類文件格式可以參考上一篇系列文章:類文件結(jié)構(gòu)),并且是否能夠被當(dāng)前版本的虛擬機(jī)所處理。驗(yàn)證點(diǎn)例如:
(1)魔數(shù)是否是0xCAFEBABE
(2)主次版本號能否被當(dāng)前虛擬機(jī)兼容
(3)常量池中是否存在不被支持的常量類型(檢查常量tag標(biāo)志)
(4)指向常量池的索引值是否指向不存在的或者不符合類型的常量
...
實(shí)際上,這一階段的驗(yàn)證還遠(yuǎn)不止上面列出這些。通過了這一階段的驗(yàn)證后,字節(jié)流會(huì)被建立成內(nèi)存中的數(shù)據(jù)結(jié)構(gòu),供后續(xù)3個(gè)驗(yàn)證階段使用
#元數(shù)據(jù)驗(yàn)證
這個(gè)階段主要是對字節(jié)碼描述的信息進(jìn)行語義分析及校驗(yàn),保證其符合Java語言的規(guī)范。驗(yàn)證點(diǎn)例如:
(1)該類是否有父類(除java.lang.Object之外,所有類都應(yīng)該有父類)
(2)該類是否繼承了不允許被繼承的類(被final修飾對類)
(3)如果該類不是抽象類,是否實(shí)現(xiàn)了其父類或者接口中要求被實(shí)現(xiàn)的所有方法
...
#字節(jié)碼驗(yàn)證
該階段將對類的方法體進(jìn)行數(shù)據(jù)流和控制流分析,確保程序語義是合法、符合邏輯并且不會(huì)對虛擬機(jī)造成危害的。驗(yàn)證點(diǎn)例如:
(1)保證任意時(shí)刻操作數(shù)棧與指令序列都能配合工作(數(shù)據(jù)類型與操作指令匹配)
(2)保證跳轉(zhuǎn)指令不會(huì)跳轉(zhuǎn)到方法體以外的字節(jié)碼指令上
(3)保證方法體中的類型轉(zhuǎn)換是有效的(比如把一個(gè)List對象賦值給一個(gè)ArrayList類型的引用是不合法的)
...
#符號引用驗(yàn)證
這個(gè)階段的校驗(yàn)是對類自身以外(常量池中各種符號引用)的信息進(jìn)行匹配性校驗(yàn)。目的是確保“解析”階段的正常運(yùn)行。如果無法通過驗(yàn)證,將會(huì)拋出java.lang.IncompatibleChangeError異常的子類,如java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchFieldErrorMethod等。驗(yàn)證點(diǎn)例如:
(1)符號引用中通過字符串描述的全限定名是否能找到對應(yīng)的類
(2)指定類中的字段和方法是否存在
(3)符號引用中的類、字段、方法的可訪問性驗(yàn)證
...
3.準(zhǔn)備
準(zhǔn)備階段是正式為類變量(不是實(shí)例變量)分配內(nèi)存并設(shè)置初始值的階段。這里說的初始值,通常情況下是對應(yīng)數(shù)據(jù)類型的零值,比如定義一個(gè)類變量:
public static int abc = 123;
那么abc在準(zhǔn)備階段后的初始值是0,而給abc賦值的putstatic指令存在于類的構(gòu)造器<cinit>()方法之中,所以給abc賦值為123的動(dòng)作將在初始化階段執(zhí)行
但是如果abc的定義改為:
public static final int abc = 123;
那么abc的值在準(zhǔn)備階段就會(huì)被賦值為123。也就是說如果類變量的字段屬性表存在ConstantValue屬性,那么在準(zhǔn)備階段虛擬機(jī)將會(huì)根據(jù)ConstantValue的設(shè)置給該字段賦值
4.解析
解析階段是虛擬機(jī)將符號引用替換成直接引用的過程
#符號引用
符號引用是以一組符號來描述引用的目標(biāo),它與虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局無關(guān),引用的目標(biāo)不一定已經(jīng)加載到內(nèi)存當(dāng)中。在Class文件中以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等類型常量出現(xiàn)
#直接引用
直接引用可以是直接指向目標(biāo)的指針、相對偏移量或者是能間接定位到目標(biāo)的句柄。它與虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局息息相關(guān)。引用的目標(biāo)一定已經(jīng)加載到內(nèi)存當(dāng)中
下面分別對其中比較重要和基本的幾種解析做下簡單介紹:
#類和接口的解析
假設(shè)當(dāng)前代碼所處類為A,將要把一個(gè)符號引用B解析成一個(gè)類或者接口C的直接引用,那么虛擬機(jī)需要完成下面3個(gè)步驟:
(1)如果C不是數(shù)組類型,那么虛擬機(jī)會(huì)把代表B的全限定名傳遞給A的類加載器去加載C。在此過程中還有可能會(huì)觸發(fā)其他相關(guān)類的加載動(dòng)作,比如加載C的父類或者實(shí)現(xiàn)的接口。一旦加載過程中出現(xiàn)任何異常,解析過程就會(huì)失敗
(2)如果C是數(shù)組類型,并且數(shù)組的元素類型為對象,那么會(huì)按照(1)中介紹的規(guī)則加載數(shù)組的元素類型,之后再生成一個(gè)代表此元素類型的數(shù)組對象
(3)如果上述步驟沒有出現(xiàn)異常,那么C在虛擬機(jī)中實(shí)際上已經(jīng)是一個(gè)有效的類或者接口了。但是依然需要對A是否具備對C的訪問權(quán)限進(jìn)行驗(yàn)證,如果驗(yàn)證失敗,將會(huì)拋出java.lang.IllegalAccessError異常
#字段解析
要對一個(gè)字段的符號引用進(jìn)行解析,首先需要對該字段所屬的類或者接口的符號引用進(jìn)行解析。如果在此過程中出現(xiàn)了任何異常,那么字段的解析都是失敗的。如果該過程成功,那么將按照如下規(guī)則對該字段進(jìn)行搜索(假設(shè)該字段所屬的類或者接口是A):
(1)如果A本身就包含了簡單名稱和字段描述符都與目標(biāo)相匹配的字段,則直接返回這個(gè)字段的直接引用,查找結(jié)束
(2)否則,如果A實(shí)現(xiàn)了接口,那么將會(huì)按照繼承關(guān)系自下而上遞歸的搜索各接口,如果某接口包含了簡單名稱和字段描述符都與目標(biāo)相匹配的字段,那么就返回這個(gè)字段的直接引用,查找結(jié)束
(3)否則,如果A不是java.lang.Object,則會(huì)按照繼承關(guān)系自下而上遞歸的搜索其父類,如果某父類包含了簡單名稱和字段描述符都與目標(biāo)相匹配的字段,那么就返回這個(gè)字段的直接引用,查找結(jié)束
(4)否則,查找失敗,拋出java.lang.NoSuchFieldError異常
如果查找過程成功,將會(huì)對這個(gè)字段進(jìn)行權(quán)限驗(yàn)證,如果不具備訪問權(quán)限,將會(huì)拋出java.lang.IllegalAccessError異常
#類方法解析
類方法解析的第一個(gè)步驟與字段解析相同,都是要對所屬的類或者接口的符號引用進(jìn)行解析。如果該過程成功,那么將按照如下規(guī)則對該方法進(jìn)行搜索(假設(shè)該字段所屬的類或者接口是A):
(1)類方法(CONSTANT_Methodref_info)和接口方法(CONSTANT_InterfaceMethodref_info)的符號引用定義是分開的,如果在類方法表中發(fā)現(xiàn)class_index中索引的A是接口,那么將拋出java.lang.IncompatibleChangeError異常
(2)通過第一步后,如果A本身就包含了簡單名稱和描述符都與目標(biāo)相匹配的方法,則直接返回這個(gè)方法的直接引用,查找結(jié)束
(3)否則,按照繼承關(guān)系自下而上遞歸的搜索其父類,如果某父類包含了簡單名稱和描述符都與目標(biāo)相匹配的方法,那么就返回這個(gè)方法的直接引用,查找結(jié)束
(4)否則,將會(huì)按照繼承關(guān)系自下而上遞歸的搜索其實(shí)現(xiàn)的接口,如果某接口包含了簡單名稱和描述符都與目標(biāo)相匹配的方法,說明A是一個(gè)抽象類,查找結(jié)束,拋出java.lang.AbstractMethodError異常
(5)否則,查找失敗,拋出java.lang.NoSuchMethodError異常
如果查找過程成功,將會(huì)對這個(gè)方法進(jìn)行權(quán)限驗(yàn)證,如果不具備訪問權(quán)限,將會(huì)拋出java.lang.IllegalAccessError異常
#接口方法解析
接口方法也需要先對所屬的類或者接口的符號引用進(jìn)行解析。如果該過程成功,那么將按照如下規(guī)則對該接口方法進(jìn)行搜索(假設(shè)該字段所屬的接口是A):
(1)如果在接口方法表中發(fā)現(xiàn)class_index中索引的A是類而不是接口,那么將拋出java.lang.IncompatibleChangeError異常
(2)通過第一步后,如果A本身就包含了簡單名稱和描述符都與目標(biāo)相匹配的方法,則直接返回這個(gè)方法的直接引用,查找結(jié)束
(3)否則,按照繼承關(guān)系自下而上遞歸的搜索其父類,如果某父類包含了簡單名稱和描述符都與目標(biāo)相匹配的方法,那么就返回這個(gè)方法的直接引用,查找結(jié)束
(4)否則,查找失敗,拋出java.lang.NoSuchMethodError異常
由于接口中方法默認(rèn)都是public的,因此不存在訪問權(quán)限問題,也就不會(huì)拋出java.lang.IllegalAccessError異常
5.初始化
對于“加載”,Java虛擬機(jī)規(guī)范中并沒有進(jìn)行強(qiáng)制約束。但是對于“初始化”,則嚴(yán)格規(guī)定了有且僅有以下五種情況必須對類進(jìn)行初始化
(1)在遇到new、getstatic、putstatic、invokestatic這四條字節(jié)碼指令時(shí),如果類還沒有進(jìn)行初始化,則必須先進(jìn)行初始化。這四條字節(jié)碼指令對應(yīng)的Java代碼場景分別是:使用new關(guān)鍵字實(shí)例化對象時(shí)、讀取或設(shè)置靜態(tài)變量時(shí)(非final變量)、調(diào)用類的靜態(tài)方法時(shí)
(2)通過反射對類進(jìn)行調(diào)用的時(shí)候,如果類還沒有進(jìn)行初始化,則必須先進(jìn)行初始化
(3)當(dāng)初始化一個(gè)類的時(shí)候,如果發(fā)現(xiàn)其父類還沒有初始化,則必須先對其父類進(jìn)行初始化
(4)程序運(yùn)行的主類(包含main方法的類),虛擬機(jī)需要先對其進(jìn)行初始化
(5)如果一個(gè)java.lang.invoke.MethodHandle實(shí)例最后的解析結(jié)果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且這個(gè)方法句柄對應(yīng)的類沒有進(jìn)行初始化,則必須先進(jìn)行初始化
初始化是類加載的最后一個(gè)階段。在前面的加載過程中,只有在加載階段,用戶可通過自定義的類加載器進(jìn)行參與,后續(xù)幾個(gè)階段都是虛擬機(jī)自動(dòng)控制完成的。直到初始化階段,才真正開始執(zhí)行類中編寫的程序代碼
在準(zhǔn)備階段,類變量已經(jīng)被賦過一次零值。初始化階段,類變量才會(huì)根據(jù)代碼邏輯重新賦值。這個(gè)動(dòng)作就是在類構(gòu)造器<clinit>()中執(zhí)行的。我們來看一下類構(gòu)造器執(zhí)行過程中可能會(huì)影響程序行為的特點(diǎn)和細(xì)節(jié):
(1)類構(gòu)造器是由編譯器自動(dòng)收集類變量賦值動(dòng)作和靜態(tài)代碼塊(static{}代碼塊)生成的。收集順序是按照源代碼中聲明的順序決定的。靜態(tài)代碼塊只能訪問到定義在它之前的類變量,定義在它之后的類變量,只能賦值,不能訪問

(2)與實(shí)例構(gòu)造器<init>()不同,類構(gòu)造器不需要顯式調(diào)用父類的類構(gòu)造器。虛擬機(jī)會(huì)保證父類的類構(gòu)造器先于子類的類構(gòu)造器執(zhí)行。因此,父類的靜態(tài)代碼塊要優(yōu)先于子類的靜態(tài)代碼塊執(zhí)行
(3)類構(gòu)造器并不是必須的,當(dāng)類中沒有靜態(tài)代碼塊,也沒有對類變量賦值的操作,那么編譯器不會(huì)生成類構(gòu)造器
(4)接口中沒有靜態(tài)代碼塊,但是可能會(huì)有類變量的賦值操作,因此也有可能會(huì)有類構(gòu)造器。但是與類的類構(gòu)造器不同的是,執(zhí)行接口的類構(gòu)造器不需要先執(zhí)行父接口的類構(gòu)造器。只有當(dāng)父接口中定義的類變量被使用時(shí),父接口的類構(gòu)造器才會(huì)執(zhí)行。同樣,接口的實(shí)現(xiàn)類在初始化時(shí)也不會(huì)執(zhí)行接口的類構(gòu)造器
(5)虛擬機(jī)會(huì)保證一個(gè)類的類構(gòu)造器在多線程環(huán)境下只執(zhí)行一次。因此當(dāng)類構(gòu)造器內(nèi)代碼耗時(shí)很長的時(shí)候,會(huì)造成其他線程阻塞
類加載器
類加載器,它的作用就是通過一個(gè)類的全限定名來獲取對應(yīng)類的二進(jìn)制字節(jié)流。這個(gè)動(dòng)作沒有完全固化在虛擬機(jī)中,而是暴露給外部程序,目的就是為了提供類加載過程的靈活性
1.類與類加載器
如果兩個(gè)類擁有完全相同的全限定類名,那么是否可以判定這兩個(gè)類“相等”?答案是只有在這兩個(gè)類是由同一個(gè)類加載器加載的前提下,才可以判定這兩個(gè)類“相等”
這里的“相等”,包含Class對象的equals()方法、isAssignableFrom()方法、isInstance()方法、instanceof關(guān)鍵字
2.雙親委派模型
從虛擬機(jī)的角度來看,只存在兩種類加載器:一種是啟動(dòng)類加載器(Bootstrap ClassLoader),它內(nèi)置在虛擬機(jī)中,是虛擬機(jī)的一部分;另外一種是其他所有類加載器,這類加載器由Java語言實(shí)現(xiàn),不屬于虛擬機(jī),全部繼承自java.lang.ClassLoader
從開發(fā)人員角度,類加載器可以再細(xì)分為:啟動(dòng)類加載器(Bootstrap ClassLoader)、擴(kuò)展類加載器(Extension ClassLoader)、應(yīng)用程序類加載器(Application ClassLoader)
#啟動(dòng)類加載器,負(fù)責(zé)將JAVA_HOME/jre/lib/目錄下(我本機(jī)使用的是java 8)虛擬機(jī)識別的類庫(按照文件名識別,比如rt.jar)加載到虛擬機(jī)內(nèi)存中
#擴(kuò)展類加載器,負(fù)責(zé)將JAVA_HOME/jre/lib/ext/目錄下的類庫加載到虛擬機(jī)內(nèi)存中
#應(yīng)用程序類加載器,負(fù)責(zé)加載CLASS_PATH下指定的類庫,如果應(yīng)用程序中沒有自定義類加載器,一般情況下默認(rèn)會(huì)使用這個(gè)類加載器??赏ㄟ^ClassLoader的getSystemClassLoader()方法獲取
一般情況下,程序使用系統(tǒng)提供的這三種類加載器,如果有必要,還可以自定義類加載器。類加載器通過稱為“雙親委派模型(Parents Delegation Model)”的方式配合工作(并非強(qiáng)制約束,而是推薦方式),它們之間使用組合(而非繼承)的方式來委派類加載行為

#工作過程
雙親委派模型的工作過程是:如果一個(gè)類加載器收到了類加載的請求,它首先不會(huì)自己直接嘗試加載這個(gè)類,而是將請求委派給它的父類加載器,每個(gè)層次的類加載器都是如此,因此所有的類加載請求最終都會(huì)落到頂層的啟動(dòng)類加載器。如果上層的類加載器反饋?zhàn)约簾o法處理加載該類的請求(上面講過每個(gè)類加載器都有自己負(fù)責(zé)的類加載路徑),那么下層的類加載器才會(huì)去嘗試自己加載
#為什么這么做
其中一個(gè)顯而易見的好處就是,通過這種方式能夠形成一種帶有優(yōu)先級的層次結(jié)構(gòu)。例如java.lang.Object類,它在JAVA_HOME/jre/lib/rt.jar中,無論哪個(gè)類加載器要加載這個(gè)類,最終都會(huì)委派給啟動(dòng)類加載器進(jìn)行加載,因此能夠保證在同一個(gè)虛擬機(jī)環(huán)境下Object類在程序的各種類加載器環(huán)境中都是同一個(gè)類。想象一個(gè),如果不使用雙親委派模型,會(huì)出現(xiàn)怎樣的混亂場景
#實(shí)現(xiàn)
雙親委派模型對于保證Java程序的穩(wěn)定運(yùn)作至關(guān)重要,但是它的實(shí)現(xiàn)卻非常簡單(詳見java.lang.ClassLoader的loadClass()方法),核心邏輯是:首先檢查該類是否已經(jīng)被加載過,如果沒有加載過則調(diào)用父類的loadClass()方法,如果父類是null則直接調(diào)用啟動(dòng)類加載器進(jìn)行加載,如果仍然加載失敗則拋出ClassNotFoundException異常后,再調(diào)用自己的findClass()方法進(jìn)行加載

思維導(dǎo)圖:

筆記4結(jié)束