牛掰!20張圖解密JVM類加載子系統(tǒng),瞬間豁然開朗
一、內(nèi)存結(jié)構(gòu)概述
如果自己想手寫一個Java虛擬機(jī)的話,主要考慮哪些結(jié)構(gòu)呢?
類加載器 執(zhí)行引擎 
「復(fù)雜版的詳細(xì)圖」
本文針對Class Loader SubSystem這一塊展開講解類加載子系統(tǒng)的工作流程。
二、類加載器和類的加載過程
2.1 類加載子系統(tǒng)作用

「類加載子系統(tǒng)負(fù)責(zé)從文件系統(tǒng)或者網(wǎng)絡(luò)中加載class文件」,class文件在文件開頭有特定的文件標(biāo)識即16進(jìn)制CA FE BA BE;
加載后的Class類信息存放于一塊成為 「方法區(qū)」 的內(nèi)存空間。除了類信息之外,方法區(qū)還會存放運(yùn)行時常量池信息,可能還包括字符串字面量和數(shù)字常量(這部分常量信息是Class文件中常量池部分的內(nèi)存映射)
ClassLoader只負(fù)責(zé)class文件的加載,至于它是否可以運(yùn)行,則由Execution Engine決定
如果調(diào)用構(gòu)造器實(shí)例化對象,則其實(shí)例存放在堆區(qū)
2.2 類加載器ClassLoader角色

class file 存在于本地硬盤上,可以理解為設(shè)計師畫在紙上的模板,而最終這個模板在執(zhí)行的時候要加載到JVM當(dāng)中來根據(jù)這個文件實(shí)例化出n個一模一樣的實(shí)例。 「class file 加載到JVM中,被稱為DNA元數(shù)據(jù)模板,放在方法區(qū)」。 在.class文件 ---> JVM ---> 最終成為元數(shù)據(jù)模板,此過程就要一個運(yùn)輸工具(類裝載器 Class Loader),扮演一個快遞員的角色。

2.3 類的加載過程
看代碼
public?class?HelloLoader?{
????public?static?void?main(String[]?args)?{
????????System.out.println("謝謝ClassLoader加載我....");
????????System.out.println("你的大恩大德,我下輩子再報!");
????}
}
「它的加載過程是怎么樣的呢?」
執(zhí)行 main() 方法(靜態(tài)方法)就需要先加載承載類 HelloLoader 加載成功,則進(jìn)行鏈接、初始化等操作,完成后調(diào)用 HelloLoader 類中的靜態(tài)方法 main 加載失敗則拋出異常

完整的流程圖如下所示:「加載 --> 鏈接(驗證 --> 準(zhǔn)備 --> 解析) --> 初始化」

2.3.1 加載
通過一個類的全限定名獲取定義此類的二進(jìn)制字節(jié)流;
將這個字節(jié)流所代表的的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時數(shù)據(jù);
在內(nèi)存中生成一個代表這個類的java.lang.Class對象,作為方法區(qū)這個類的各種數(shù)據(jù)的訪問入口
?「加載class文件的方式」
?
從本地系統(tǒng)中直接加載 通過網(wǎng)絡(luò)獲取,典型場景:Web Applet 從zip壓縮包中讀取,成為日后jar、war格式的基礎(chǔ) 運(yùn)行時計算生成,使用最多的是:動態(tài)代理技術(shù) 由其他文件生成,典型場景:JSP應(yīng)用從專有數(shù)據(jù)庫中提取.class文件,比較少見 從加密文件中獲取,典型的防Class文件被反編譯的保護(hù)措施
2.3.2 鏈接(即驗證、準(zhǔn)備、解析)
「驗證」
目的在于確保Class文件的字節(jié)流中包含信息符合當(dāng)前虛擬機(jī)要求,保證被加載類的正確性,不會危害虛擬機(jī)自身安全。
主要包括四種驗證,文件格式驗證,元數(shù)據(jù)驗證,字節(jié)碼驗證,符號引用驗證。
「準(zhǔn)備」
為類變量分配內(nèi)存并且設(shè)置該類變量的默認(rèn)初始值,即零值;
這里不包含用final修飾的static,因為final在編譯的時候就會分配了,準(zhǔn)備階段會顯式初始化;
之類不會為實(shí)例變量分配初始化,類變量會分配在方法去中,而實(shí)例變量是會隨著對象一起分配到j(luò)ava堆中。
「解析」
將常量池內(nèi)的符號引用轉(zhuǎn)換為直接引用的過程。
事實(shí)上,解析操作往往會伴隨著jvm在執(zhí)行完初始化之后再執(zhí)行
符號引用就是一組符號來描述所引用的目標(biāo)。符號應(yīng)用的字面量形式明確定義在《java虛擬機(jī)規(guī)范》的class文件格式中。直接引用就是直接指向目標(biāo)的指針、相對偏移量或一個間接定位到目標(biāo)的句柄
解析動作主要針對類或接口、字段、類方法、接口方法、方法類型等。對應(yīng)常量池中的CONSTANT_Class_info/CONSTANT_Fieldref_info、CONSTANT_Methodref_info等。
2.3.3 初始化
clinit()即“class or interface initialization method”,注意他并不是指構(gòu)造器init(),初始化階段就是執(zhí)行類構(gòu)造器方法
的過程() 此方法不需要定義,是javac編譯器自動收集類中的所有類變量的賦值動作和靜態(tài)代碼塊中的語句合并而來。
我們注意到如果沒有靜態(tài)變量c,那么字節(jié)碼文件中就不會有clinit方法

「構(gòu)造器方法clinit()中指令按語句在源文件中出現(xiàn)的順序執(zhí)行」
「虛擬機(jī)必須保證一個類的clinit()方法在多線程下被同步加鎖。」
即一個類只需被clinit一次,之后該類的內(nèi)部信息就被存儲在方法區(qū)。
可以看到線程2并不會重復(fù)執(zhí)行初始化操作。
三、類加載器分類
JVM支持兩種類型的加載器,分別為 「引導(dǎo)類加載器」 C/C++實(shí)現(xiàn)(BootStrap ClassLoader)和 「自定義類加載器」 由Java實(shí)現(xiàn)
從概念上來講,自定義類加載器一般指的是程序中由開發(fā)人員自定義的一類類加載器,但是java虛擬機(jī)規(guī)范卻沒有這么定義,而是 「將所有派生于抽象類ClassLoader的類加載器都劃分為自定義類加載器」。

「這里的四者之間的關(guān)系是包含關(guān)系。不是上層下層,也不是子父類的繼承關(guān)系。」
按照這樣的加載器的類型劃分,在程序中我們最常見的類加載器是:引導(dǎo)類加載器BootStrapClassLoader、自定義類加載器(Extension Class Loader、System Class Loader、User-Defined ClassLoader)
3.1 自定義類與核心類庫的加載器
對于用戶自定義類來說:將使用系統(tǒng)類System Class Loader加載器中的AppClassLoader進(jìn)行加載
Java核心類庫都是使用引導(dǎo)類加載器BootStrapClassLoader加載的
/**
?*?ClassLoader加載
?*/
public?class?ClassLoaderTest?{
????public?static?void?main(String[]?args)?{
????????//獲取系統(tǒng)類加載器
????????ClassLoader?systemClassLoader?=?ClassLoader.getSystemClassLoader();
????????System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2
????????//獲取其上層??擴(kuò)展類加載器
????????ClassLoader?extClassLoader?=?systemClassLoader.getParent();
????????System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@610455d6
????????//獲取其上層?獲取不到引導(dǎo)類加載器
????????ClassLoader?bootStrapClassLoader?=?extClassLoader.getParent();
????????System.out.println(bootStrapClassLoader);//null
????????//對于用戶自定義類來說:使用系統(tǒng)類加載器進(jìn)行加載
????????ClassLoader?classLoader?=?ClassLoaderTest.class.getClassLoader();
????????System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2
????????//String?類使用引導(dǎo)類加載器進(jìn)行加載的??-->java核心類庫都是使用引導(dǎo)類加載器加載的
????????ClassLoader?classLoader1?=?String.class.getClassLoader();
????????System.out.println(classLoader1);//null獲取不到間接證明了String?類使用引導(dǎo)類加載器進(jìn)行加載的
????}
}
3.2 虛擬機(jī)自帶的加載器
「啟動類加載器(引導(dǎo)類加載器,BootStrap ClassLoader)」
這個類加載使用C/C++語言實(shí)現(xiàn)的,嵌套在JVM內(nèi)部
它用來加載java的核心庫(JAVA_HOME/jre/lib/rt.jar/resources.jar或sun.boot.class.path路徑下的內(nèi)容),用于提供JVM自身需要的類
并不繼承自java.lang.ClassLoader,沒有父加載器
「加載拓展類和應(yīng)用程序類加載器,并指定為他們的父加載器,即ClassLoade」r
出于安全考慮,BootStrap啟動類加載器只加載包名為java、javax、sun等開頭的類
「拓展類加載器(Extension ClassLoader)」
java語言編寫 ,由sun.misc.Launcher$ExtClassLoader實(shí)現(xiàn)。
「派生于ClassLoader類」
從java.ext.dirs系統(tǒng)屬性所指定的目錄中加載類庫,或從JDK的安裝目錄的jre/lib/ext子目錄(擴(kuò)展目錄)下加載類庫。「如果用戶創(chuàng)建的JAR放在此目錄下,也會由拓展類加載器自動加載?!?/strong>
「應(yīng)用程序類加載器(系統(tǒng)類加載器,AppClassLoader)」
java語言編寫, 由sun.misc.Launcher$AppClassLoader實(shí)現(xiàn)。
「派生于ClassLoader類」
它負(fù)責(zé)加載環(huán)境變量classpath或系統(tǒng)屬性 java.class.path指定路徑下的類庫
「該類加載器是程序中默認(rèn)的類加載器」,一般來說,java應(yīng)用的類都是由它來完成加載
通過ClassLoader#getSystemClassLoader()方法可以獲取到該類加載器
/**
?*?虛擬機(jī)自帶加載器
?*/
public?class?ClassLoaderTest1?{
????public?static?void?main(String[]?args)?{
????????System.out.println("********啟動類加載器*********");
????????URL[]?urls?=?sun.misc.Launcher.getBootstrapClassPath().getURLs();
????????//獲取BootStrapClassLoader能夠加載的api路徑
????????for?(URL?e:urls){
????????????System.out.println(e.toExternalForm());
????????}
????????//從上面的路徑中隨意選擇一個類?看看他的類加載器是什么
????????//Provider位于?/jdk1.8.0_171.jdk/Contents/Home/jre/lib/jsse.jar?下,引導(dǎo)類加載器加載它
????????ClassLoader?classLoader?=?Provider.class.getClassLoader();
????????System.out.println(classLoader);//null
????????System.out.println("********拓展類加載器********");
????????String?extDirs?=?System.getProperty("java.ext.dirs");
????????for?(String?path?:?extDirs.split(";")){
????????????System.out.println(path);
????????}
????????//從上面的路徑中隨意選擇一個類?看看他的類加載器是什么:拓展類加載器
????????ClassLoader?classLoader1?=?CurveDB.class.getClassLoader();
????????System.out.println(classLoader1);//sun.misc.Launcher$ExtClassLoader@4dc63996
????}
}
「知識擴(kuò)展:啟動類加載器BootStrapClassLoader能夠加載的api路徑有」

最近看java.util.concurrent包的內(nèi)容,發(fā)現(xiàn)java.time.、java.util.、java.nio.、java.lang.、java.text.、java.sql.、java.math.*等等都在rt.jar包下。
3.3 用戶自定義加載器
「在Java的日常應(yīng)用程序開發(fā)中,類的加載幾乎是由上述三種類加載器相互配合執(zhí)行的,在必要的時候,我們還可以自定義加載器,來定制類的加載方式?!?/strong>
為什么要使用用戶自定義類加載器
隔離加載類
修改類加載的方式
拓展加載源
防止源碼泄漏
「用戶自定義加載器實(shí)現(xiàn)步驟:」
開發(fā)人員可以通過繼承抽象類 java.lang.ClassLoader 類的方式,實(shí)現(xiàn)自己的類加載器,以滿足一些特殊的需求 在JDK1.2之前,在自定義類加載器時,總會去繼承ClassLoader類并重寫loadClass()方法,從而實(shí)現(xiàn)自定義的類加載類,但是在JDK1.2之后已不再建議用戶去覆蓋loadClass()方法,而是建議把自定義的類加載邏輯寫在findClass()方法中 在編寫自定義類加載器時,如果沒有太過于復(fù)雜的需求,可以直接繼承URLClassLoader類,這樣就可以避免自己去編寫findClass()方法及其獲取字節(jié)碼流的方式,使自定義類加載器編寫更加簡潔。
四、ClassLoader的常用方法及獲取方法
ClassLoader類,它是一個抽象類,其后所有的類加載器都繼承自ClassLoader(不包括啟動類加載器)

4.1 ClassLoader繼承關(guān)系
「sun.misc.Launcher它是一個java虛擬機(jī)的入口應(yīng)用」

4.2 代碼示例
public?class?ClassLoaderTest2?{
????public?static?void?main(String[]?args)?{
????????try?{
????????????
????????????//1.Class.forName().getClassLoader()
????????????ClassLoader?classLoader?=?Class.forName("java.lang.String").getClassLoader();
????????????System.out.println(classLoader);?//?String?類由啟動類加載器加載,我們無法獲取
????????????//2.Thread.currentThread().getContextClassLoader()
????????????ClassLoader?classLoader1?=?Thread.currentThread().getContextClassLoader();
????????????System.out.println(classLoader1);
????????????//3.ClassLoader.getSystemClassLoader().getParent()
????????????ClassLoader?classLoader2?=?ClassLoader.getSystemClassLoader();
????????????System.out.println(classLoader2);
????????}?catch?(ClassNotFoundException?e)?{
????????????e.printStackTrace();
????????}
????}
}
//?輸出
null
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$AppClassLoader@18b4aac2
五、雙親委派機(jī)制
Java虛擬機(jī)對class文件采用的是按需加載的方式,也就是說當(dāng)需要使用該類時才會將她的class文件加載到內(nèi)存生成的class對象。而且加載某個類的class文件時,java虛擬機(jī)采用的是雙親委派模式,即把請求交由父類處理,它是一種任務(wù)委派模式。
5.1 雙親委派機(jī)制工作原理
如果一個類加載器收到了類加載請求,它并不會自己先去加載,而是把這個請求委托給父類的加載器去執(zhí)行; 如果父類加載器還存在其父類加載器,則進(jìn)一步向上委托,依次遞歸,請求最終將到達(dá)頂層的啟動類加載器; 如果父類加載器可以完成類加載任務(wù),就成功返回,倘若父類加載器無法完成此加載任務(wù),子加載器才會嘗試自己去加載,這就是雙親委派模式。

5.2 代碼示例
如圖,雖然我們自定義了一個java.lang包下的String嘗試覆蓋核心類庫中的String,但是由于雙親委派機(jī)制,啟動加載器會加載java核心類庫的String類(BootStrap啟動類加載器只加載包名為java、javax、sun等開頭的類),而核心類庫中的String并沒有main方法。

5.3 雙親委派機(jī)制的優(yōu)勢
「避免類的重復(fù)加載,如上」
「保護(hù)程序安全,防止核心API被隨意修改」
啟動類加載器可以搶在標(biāo)準(zhǔn)擴(kuò)展類加載器之前去加載類,而標(biāo)準(zhǔn)擴(kuò)展類裝載器可以搶在類路徑加載器之前去裝載那個類,類路徑裝載 器又可以搶在自定義類加載器之前去加載它。所以Java虛擬機(jī)先從最可信的Java核心API查找類型,這是為了防止不可靠的類扮演被信任的類,試想一 下,網(wǎng)絡(luò)上有個名叫java.lang.Integer的類,它是某個黑客為了想混進(jìn)java.lang包所起的名字,實(shí)際上里面含有惡意代碼,但是這種伎倆在雙親模式加載體系結(jié)構(gòu)下是行不通的,因為網(wǎng)絡(luò)類加載器在加載它的時候,它首先調(diào)用雙親類加載器,這樣一直向上委托,直到啟動類加載器,而啟動類加載器在核心Java API里發(fā)現(xiàn)了這個名字的類,所以它就直接加載Java核心API的java.lang.Integer類,然后將這個類返回,所以自始自終網(wǎng)絡(luò)上的 java.lang.Integer的類是不會被加載的。
「保證核心API包的訪問權(quán)限」
但是如果這個移動代碼不是去試圖替換一個被信任的類(就是前面說的那種情況),而是想在一個被信任的包中插入一個全新的類型,情況會怎樣呢?比如一個名為 java.lang.Virus的類,經(jīng)過雙親委托模式,最終類裝載器試圖從網(wǎng)絡(luò)上下載這個類,因為網(wǎng)絡(luò)類裝載器的雙親們都沒有這個類(當(dāng)然沒有了,因為 是病毒嘛)。假設(shè)成功下載了這個類,那你肯定會想,Virus和lang下的其他類痛在java.lang包下,暗示這個類是Java API的一部分,那么是不是也擁有修改Java.lang包中數(shù)據(jù)的權(quán)限呢?答案當(dāng)然不是,因為要取得訪問和修改java.lang包中的權(quán) 限,java.lang.Virus和java.lang下其他類必須是屬于同一個運(yùn)行時包的,什么是運(yùn)行時包?運(yùn)行時包是指由同一個類裝載器裝載的、屬 于同一個包的、多個類型的集合。考慮一下,java.lang.Virus和java.lang其他類是同一個類裝載器裝載的嗎?不是 的!java.lang.Virus是由網(wǎng)絡(luò)類裝載器裝載的!
自定義類:java.lang.MeDsh(java.lang包需要訪問權(quán)限,阻止我們用包名自定義類)

5.4 雙親委派機(jī)制在SPI中的應(yīng)用
某個應(yīng)用程序由雙親委派機(jī)制找到引導(dǎo)類加載器,首先調(diào)用rt.jar包中的SPI核心,但由于SPI核心當(dāng)中有各種各樣的接口需要被實(shí)現(xiàn)(這里指具體的服務(wù)提供商),這里我們已JDBC.jar為例,jdbc.jar可以為我們提供具體的實(shí)現(xiàn)。
那么這時我們需要反向委托,找到線程上下文類加載器去加載jdbc.jar
線程上下文類加載器屬于系統(tǒng)類加載器

5.5 沙箱安全機(jī)制
自定義String類,但是在加載自定義String類的時候會率先使用引導(dǎo)類加載器加載,而引導(dǎo)類加載器在加載的過程中會先加載JDK自帶的文件(rt.jar包中java\lang\String.class),報錯信息說沒有main方法,就是因為加載的rt.jar包中的String類。這樣可以何保證對java核心源代碼的保護(hù),這就是沙箱安全機(jī)制。
六、其他
6.1 JVM中表示兩個class對象是否為同一個類
在JVM中表示兩個class對象是否為同一個類存在的兩個必要條件
類的完整類名必須一致,包括包名
即使類的完整類名一致,同時要求加載這個類的ClassLoader(指ClassLoader實(shí)例對象)必須相同;是引導(dǎo)類加載器、還是定義類加載器
換句話說,在JVM中,即使這兩個類對象(class對象)來源同一個Class文件,被同一個虛擬機(jī)所加載,但只要加載它們的ClassLoader實(shí)例對象不同,那么這兩個類對象也是不相等的。
6.2 對類加載器的引用
JVM必須知道一個類型是由啟動類加載器加載的還是由用戶類加載器加載的。如果一個類型由用戶類加載器加載的,那么JVM會 「將這個類加載器的一個引用作為類型信息的一部分保存在方法區(qū)中」。當(dāng)解析一個類型到另一個類型的引用的時候,JVM需要保證兩個類型的加載器是相同的。
6.3 類的主動使用和被動使用
Java程序?qū)︻惖氖褂梅绞椒譃椋褐鲃邮褂煤捅粍邮褂茫?strong>「即是否調(diào)用了clinit()方法」。
主動使用在類加載系統(tǒng)中的第三階段initialization即初始化階段調(diào)用了clinit()方法。
而被動使用不會去調(diào)用,主動使用,分為七種情況:
創(chuàng)建類的實(shí)例
訪問某各類或接口的靜態(tài)變量,或者對靜態(tài)變量賦值
調(diào)用類的靜態(tài)方法
反射 比如Class.forName(com.dsh.jvm.xxx)
初始化一個類的子類
java虛擬機(jī)啟動時被標(biāo)明為啟動類的類
JDK 7 開始提供的動態(tài)語言支持:java.lang.invoke.MethodHandle實(shí)例的解析結(jié)果REF_getStatic、REF_putStatic、REF_invokeStatic句柄對應(yīng)的類沒有初始化,則初始化
除了以上七種情況,其他使用java類的方式都被看作是對「類的被動使用」,都「不會導(dǎo)致類的初始化」。
[JVM從入門到放棄]第二篇?,嗯...?今天也...學(xué)習(xí)了( ?? ω ?? )??
—?【 THE END 】— 本公眾號全部博文已整理成一個目錄,請在公眾號里回復(fù)「m」獲取! 3T技術(shù)資源大放送!包括但不限于:Java、C/C++,Linux,Python,大數(shù)據(jù),人工智能等等。在公眾號內(nèi)回復(fù)「1024」,即可免費(fèi)獲?。?!
