本文導(dǎo)讀:
1弊添、前奏动猬,舉個(gè)生活中的小栗子
2、為何Java類型加載表箭、連接在程序運(yùn)行期完成?
3、一個(gè)類在什么情況下才會被加載到JVM中免钻?
什么是主動使用彼水、被動使用?代碼示例助你透徹理解類初始化的時(shí)機(jī)极舔。
4凤覆、類的加載(Loading)內(nèi)幕透徹剖析
類加載做的那些事兒、雙親委派模型工作過程拆魏、ClassLoader源碼解析
5盯桦、Tomcat如何打破雙親委派模型的
6、上下文類加載器深入淺出剖析
7渤刃、最后總結(jié)
1拥峦、前奏,舉個(gè)生活中的小栗子
春節(jié)馬上要到了卖子,大家是不是都在迫不及待的等著回家團(tuán)圓了呢略号?
大春運(yùn)早已啟動,回家的過程其實(shí)是個(gè)「辛苦活」洋闽,有的同學(xué)還沒有買到票呢玄柠,蒙眼狂奔終于搶到了,發(fā)現(xiàn)竟然是個(gè)站票~诫舅,退了羽利,連站票的機(jī)會都沒了吧?
昨天還聽一位同學(xué)說:『嘿嘿刊懈,去年我提前就買到票了这弧,但是... 但是... 去錯(cuò)火車站了。俏讹。当宴。尼瑪,當(dāng)時(shí)那是啥心情啊~ 幸運(yùn)的是后來又刷到票了泽疆,不然就真回不去了户矢!』
回家大部分朋友都要乘坐交通工具,不管你乘坐什么樣的交通工具出行殉疼,對于「交通管理」內(nèi)部來說梯浪,最最重要的任務(wù)就是保障大家得出行安全。
那么如何保障大家的出行安全呢瓢娜?
乘坐地鐵挂洛、飛機(jī)等這些公共交通工具,必不可少的最重要的環(huán)節(jié)就是『安檢』眠砾,不是什么東西都可以隨便讓你帶的虏劲,都是有明文規(guī)定的,比如易燃易爆、酒類等都是有限制的柒巫。
交通出行的大體過程励堡,有點(diǎn)類似類文件加載到Java虛擬機(jī)(簡稱 JVM)的過程,程序中運(yùn)行的各種類文件(比如Java堡掏、Kotlin)应结,也是要必須經(jīng)過『安檢』的,才能允許進(jìn)入到JVM中的,一切都是為了安全。
當(dāng)然慎王,安檢的標(biāo)準(zhǔn)是不同的盐股。
接下來,我們進(jìn)入正題,一起來看看類文件是如何被加載到JVM當(dāng)中的。
上圖的對比只是為了方便理解 ,抽象出來一層『安全檢查』肛炮,其實(shí)就是『類加載』的過程。
這個(gè)過程JVM當(dāng)中約束了規(guī)范和標(biāo)準(zhǔn)宝踪,都會經(jīng)過加載侨糟、驗(yàn)證、準(zhǔn)備瘩燥、解析秕重、初始化五個(gè)階段。
這里一定要說一個(gè)概念厉膀,個(gè)人認(rèn)為對于理解類加載過程挺重要的溶耘。
更準(zhǔn)確的說法,應(yīng)該是類型
的加載過程服鹅,在Java代碼中凳兵,類型的加載、連接企软、初始化都是在程序運(yùn)行時(shí)完成的庐扫。
這里的類型,是指你在開發(fā)代碼時(shí)常見的class仗哨、interface形庭、enum這些關(guān)鍵字的定義,并不是指具體的class對象厌漂。
舉個(gè)??:
Object obj = new Object();
new出來的obj是Object類型嗎萨醒?當(dāng)然不是,obj只是通過new創(chuàng)建出來的Object對象苇倡,而類型實(shí)際是Object類本身
富纸。而要想創(chuàng)建Object對象的前提囤踩,必須要有類型的信息,才能在Java堆中創(chuàng)建出來晓褪。所以高职,這里要明確區(qū)分開。
絕大多數(shù)情況下辞州,類型是提前編寫好的,比如Object類是由JDK已經(jīng)提供的寥粹。另外一些情況是可以在運(yùn)行期間動態(tài)的生成出來变过,比如動態(tài)代理(程序運(yùn)行期完成的)。
2涝涤、為何Java類型加載媚狰、連接在程序運(yùn)行期完成?
其實(shí)阔拳,運(yùn)行區(qū)間能做這件事崭孤,就為一些有創(chuàng)意的開發(fā)人員提供了很多的可能性。一切的文件都已經(jīng)存在糊肠,程序運(yùn)行的過程中可以采取一些特殊的處理方式把這些之前已經(jīng)存在或者運(yùn)行期生成出來的這些類型有機(jī)的裝配在一起辨宠。
Java本身是一門靜態(tài)的語言,而他的很多特性又具有動態(tài)語言才能擁有的特質(zhì)货裹,也因此類型的加載嗤形、連接和初始化在運(yùn)行期間完成起到了很大的幫助作用。
類型的加載:查找并加載類的二進(jìn)制數(shù)據(jù)(字節(jié)碼文件)弧圆,最常見的赋兵,是將類的Class文件從磁盤加載到內(nèi)存中。
類型的連接:將類與類的關(guān)系確定好搔预,對于字節(jié)碼相關(guān)的處理霹期、驗(yàn)證、校驗(yàn)在加載連接階段去完成的拯田。字節(jié)碼本身可以被人為操縱的历造,也因此可能有惡意的可能性,所以需要校驗(yàn)勿锅。
驗(yàn)證:確保被加載類的正確性帕膜,就是要按照J(rèn)VM規(guī)范定義的。
準(zhǔn)備:為類的靜態(tài)變量分配內(nèi)存溢十,并將其初始化為默認(rèn)值
class Test {
public static int num = 1;
}
上述代碼示例中的中間過程垮刹,在將類型加載到內(nèi)存過程中,num分配內(nèi)存张弛,首先設(shè)置為0荒典,1是在后續(xù)的初始化階段賦值給num變量酪劫。
- 解析:把類中的符號引用轉(zhuǎn)換為直接引用
符號引用: 間接的引用方式,通過一個(gè)符號的表示一個(gè)類引用了另外的類寺董。 直接引用:直接引用到目標(biāo)對象中的內(nèi)存的位置
初始化階段:為類的靜態(tài)變量賦予正確的初始值覆糟。
類型的初始化:比如一些靜態(tài)的變量的賦值是在初始化階段完成的。
3遮咖、一個(gè)類在什么情況下才會被加載到JVM中滩字?
Java程序?qū)︻惖氖褂梅绞娇煞譃閮煞N:
主動使用
被動使用
特別的重要:
所有的Java虛擬機(jī)實(shí)現(xiàn)必須在每個(gè)類或接口被java程序首次主動使用時(shí)才初始化他們。
主動使用(八種情況
):
1)創(chuàng)建類的實(shí)例御吞,比如new一個(gè)對象
2)訪問某一個(gè)類或接口的靜態(tài)變量麦箍,或者對該靜態(tài)變量賦值 (訪問類的靜態(tài)變量的助記符getstatic,賦值是putstatic)陶珠。
3)調(diào)用類的靜態(tài)方法 (應(yīng)用invokestatic助記符)挟裂。
4)使用java.lang.reflect包的方法對類型進(jìn)行反射調(diào)用,比如:Class.forName(“com.test.Test") 通過反射的方式獲取類的Class對象揍诽。
5)初始化一個(gè)類的子類诀蓉,比如有class Parent{}、子類class Child extends Parent{}暑脆,當(dāng)初始化Child類時(shí)也表示對Parent類的主動使用渠啤,Parent類也要全部初始化。
6)Java虛擬機(jī)啟動時(shí)被標(biāo)注為啟動類的類饵筑,即有main方法的類埃篓。
7)JDK1.7開始提供的動態(tài)語言支持:java.lang.invoke.MethodHandle實(shí)例的解析結(jié)果REF_getStatic, REF_putStatic, REF_invokeStatic句柄對應(yīng)的類沒有初始化,則初始化根资。
8)當(dāng)一個(gè)接口中定義了JDK 8新加入的默認(rèn)方法(被default關(guān)鍵字修飾的接口方法)時(shí)架专,如果有這個(gè)接口的實(shí)現(xiàn)類發(fā)生了初始化,那該接口要在其之前被初始化玄帕。
除了上述所講的八種情況部脚,其他使用Java類的方式都被看作是類的被動使用,都不會導(dǎo)致類的初始化裤纹。
另外委刘,要特別說明的一點(diǎn)
:
接口的加載過程與類加載過程會有所不同,接口不能使用 「static{}」語句塊鹰椒,但是編譯器會為接口生成對應(yīng)的 <clinit>()類構(gòu)造器锡移,用于初始化接口中所定義的成員變量。
主動使用的第5種:當(dāng)子類初始化時(shí)漆际,要求其父類也要全部初始化完成淆珊。但是,對于一個(gè)接口的初始化時(shí)奸汇,并不要求其父接口要全部初始化完成施符,只有在真正使用到父接口時(shí)(比如引用接口中定義的常量)時(shí)才會去初始化往声,有點(diǎn)延遲加載的意思。
被動使用示例:
1)通過子類引用父類的靜態(tài)字段戳吝,不會導(dǎo)致子類的初始化
public class Parent {
static {
System.out.println("Parent init....");
}
public static int a = 123;
}
public class Child extends Parent {
static {
System.out.println("Child init...");
}
}
// Test類打印浩销,子類直接調(diào)用父類的靜態(tài)字段
public static void main(String[] args) {
System.out.println(Child.a);
}
輸出結(jié)果:
Parent init....
123
根據(jù)輸出結(jié)果看到,不會輸出 Child init...听哭,通過其子類來引用父類中定義的靜態(tài)字段慢洋,只會觸發(fā)父類的初始化而不會觸發(fā)子類的初始化,對于靜態(tài)字段陆盘,只有直接定義這個(gè)字段的類才會被初始化且警。
2) 創(chuàng)建數(shù)組類對象,并不會導(dǎo)致引用的類初始化
public class Child extends Parent {
static {
System.out.println("Child init...");
}
}
// 使用 Child 引用創(chuàng)建個(gè)數(shù)組
public static void main(String[] args) {
Child[] child = new Child[1];
System.out.println(child);
}
輸出結(jié)果:
[Lcom.dskj.jvm.beidong.Child;@7852e922
并沒有輸出Child init...證明并沒有初始化com.dskj.jvm.beidong.Child類礁遣,根據(jù)輸出結(jié)果看到了[Lcom.dskj.jvm.beidong.Child
,帶了[L
說明觸發(fā)了數(shù)組類的初始化階段肩刃,它是由JVM自動生成的祟霍,繼承自java.lang.Object類,由于anewarray
助記符觸發(fā)創(chuàng)建動作的盈包。
對于數(shù)組來說沸呐,JavaDoc通常將其所構(gòu)成的元素稱作為Component,實(shí)際上就是將數(shù)組降低一個(gè)維度的類型呢燥。
助記符:
anewarray:表示創(chuàng)建一個(gè)引用類型的(如類崭添、接口、數(shù)組)數(shù)組叛氨,并將其引用值壓入棧頂呼渣。
newarray:表示創(chuàng)建一個(gè)指定的原始類型的(如int、float寞埠、char屁置、short、double仁连、boolean蓝角、byte)的數(shù)組,并將其引用值壓入棧頂饭冬。
對應(yīng)字節(jié)碼內(nèi)容:
3)調(diào)用ClassLoader的loadClass()方法使鹅,不會導(dǎo)致類的初始化。
代碼如下:
public class LoadClassTest {
public static void main(String[] args) {
try {
ClassLoader.getSystemClassLoader().loadClass("com.dskj.jvm.passivemode.LoadClass");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
class LoadClass {
public static final String STR = "Hello World";
static {
System.out.println("LoadClass init...");
}
}
沒有輸出 LoadClass init...昌抠,證明了調(diào)用系統(tǒng)類加載器的loadClass()方法患朱,并不會初始化LoadClass類,因?yàn)镃lassLoader#loadClass()方法內(nèi)部傳入的resolve參數(shù)為false扰魂,表示Class不會進(jìn)入到連接
階段麦乞,也就不會導(dǎo)致類的初始化蕴茴。
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
...
if (resolve) {
//** Links the specified class**
resolveClass(c);
}
}
4)final修飾的常量,編譯時(shí)會存入調(diào)用類常量池中姐直,本質(zhì)上沒有引用到定義常量的類倦淀,不會導(dǎo)致類的初始化動作。
看下面代碼:
public class ConstClassTest {
public static void main(String[] args) {
System.out.println(ConstClass.STR);
}
}
class ConstClass {
static {
System.out.println("ConstClass init...");
}
public static final String STR = "Hello World";
}
輸出結(jié)果:
Hello World
結(jié)果只會輸出 Hello World声畏,不會輸出ConstClass init...撞叽,ConstClassTest類對常量ConstClass.STR的引用,實(shí)際被轉(zhuǎn)化為ConstClassTest類對自身常量池的引用了插龄。也就是說愿棋,實(shí)際上ConstClassTest的Class文件之中并沒有ConstClass類的符號引用入口。
編譯完成均牢,兩個(gè)ConstClassTest和ConstClass就沒有任何關(guān)系了糠雨。這句話如何能證明一下?
你可以先運(yùn)行一次徘跪,然后將編譯后的ConstClass.class文件從磁盤上刪除掉甘邀,再次運(yùn)行跟上面輸出結(jié)果是一樣的。
還不信垮庐?如下圖所示Idea中的運(yùn)行結(jié)果:
在IDEA下測試時(shí)松邪,如果你使用的Gradle來構(gòu)建,模擬上面的刪除class文件過程哨查,要使用 xxx/out/production/ 目錄下生成編譯后的class文件逗抑,當(dāng)類沒有發(fā)生變化時(shí)不會重新生成class文件。如果使用默認(rèn)的 xxx/build/xx寒亥,每次運(yùn)行都會重新生成新的class文件邮府。
如果有問題,可以在 Project Settings -> Modules -> 項(xiàng)目的 Paths 中調(diào)整編譯輸出目錄溉奕。
我們繼續(xù)在這個(gè)示例基礎(chǔ)上做修改:
public class ConstClassTest {
public static void main(String[] args) {
System.out.println(ConstClass.STR);
}
}
class ConstClass {
// STR 定義的常量通過UUID生成一個(gè)隨機(jī)串
public static final String STR = "Hello World" + UUID.randomUUID();
static {
System.out.println("ConstClass init...");
}
}
注意挟纱,這里 STR 常量通過UUID生成一個(gè)隨機(jī)串,編譯是通過的腐宋。
直接運(yùn)行紊服,輸出結(jié)果:
ConstClass init...
Hello World:d26d7f1d-2d46-41cb-b5dc-2b7b3fe61e74
看到了ConstClass init...,說明ConstClass類被初始化了胸竞。
將ConstClass.class文件刪除后欺嗤,再次運(yùn)行:
Exception in thread "main" java.lang.NoClassDefFoundError: com/dskj/jvm/passivemode/ConstClass
at com.dskj.jvm.passivemode.ConstClassTest.main(ConstClassTest.java:7)
Caused by: java.lang.ClassNotFoundException: com.dskj.jvm.passivemode.ConstClass
at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
... 1 more
大家看到了嗎?ConstClass.class文件被刪除后卫枝,再次運(yùn)行就發(fā)生了 java.lang.NoClassDefFoundError
異常了煎饼,為什么?正是因?yàn)?ConstClass 類里定義的STR常量并不是編譯器能夠確定的值校赤,那么其值就不會被放到調(diào)用類的常量池中吆玖。
這個(gè)示例可以好好理解下筒溃,同時(shí)印證了該類的初始化時(shí)機(jī)中,主動使用和被動使用的場景沾乘。
大家記住一個(gè)類的8種主動使用情況怜奖,都是在開發(fā)過程中常見的使用方式。另外翅阵,注意下被動使用的幾種情況歪玲,結(jié)合上面的列舉的代碼示例透徹理解。
類加載全過程的每一個(gè)階段掷匠,結(jié)合前文給出的圖示滥崩,詳細(xì)展開。
4讹语、類的加載(Loading)內(nèi)幕透徹剖析
前面提到的類文件钙皮,就是后綴文件為.class
的二進(jìn)制文件。
JVM在加載階段主要完成如下三件事
1)通過一個(gè)類的全限定名顽决,即包名+類名
來獲取定義此類的二進(jìn)制字節(jié)流株灸。
2)將這個(gè)字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)。
3)JVM內(nèi)存中生成一個(gè)代表該類的java.lang.Class對象
擎值,作為方法區(qū)這個(gè)類的各種數(shù)據(jù)的訪問入口。
對于第一點(diǎn)來說逐抑,并沒有要求這個(gè)二進(jìn)制字節(jié)流鸠儿,具體以什么樣的方式從Class文件中讀取。
通過下面一張圖來匯總一下:
解釋下比較常見的Class文件讀取方式:
1)從ZIP包中讀取Class文件厕氨,流行的SpringBoot/SpringCoud框架基本都打成Jar包形式进每,內(nèi)嵌了Tomcat,俗稱Fat Jar
命斧,通過java -jar可以直接啟動田晚,非常方便。
另外国葬,還有一些項(xiàng)目仍然是使用War包形式贤徒,并且使用單獨(dú)使用Tomcat這類應(yīng)用容器來部署的。
2)運(yùn)行時(shí)生成的Class文件汇四,應(yīng)用最多的就是動態(tài)代理技術(shù)了接奈,比如CGLIB、JDK動態(tài)代理通孽。
雙親委派模型
思考個(gè)問題序宦,這些Class文件是由誰來加載的呢?
實(shí)現(xiàn)這個(gè)動作的代碼正是類加載器來完成的背苦,類加載器在類層次劃分互捌、OSGi潘明、程序熱部署、代碼加密等領(lǐng)域大放異彩秕噪,成為Java技術(shù)體系中一塊重要的基石钳降。
對于任意一個(gè)類,如何確定在JVM當(dāng)中的唯一性巢价?必須是由加載該類的類加載器和該類本身一起共同確立在JVM中的唯一性牲阁。
每一個(gè)類加載器,都擁有一個(gè)獨(dú)立的類名稱空間壤躲。通俗理解:比較兩個(gè)類是否『相等』城菊,這兩個(gè)類只有在同一個(gè)類加載器加載的前提下才有意義。否則碉克,即使這兩個(gè)類來源于同一個(gè)Class文件凌唬,被同一個(gè)JVM加載,只要加載它們的類加載器不同漏麦,那這兩個(gè)類就必定不相等客税。
類加載器之間是什么關(guān)系?
如下圖所示撕贞,三種加載器之間的層次關(guān)系被稱為類加載器的 『雙親委派模型(Parents Delegation Model)』更耻。
雙親委派模型要求除了頂層的啟動類加載器外,其余的類加載器都應(yīng)有自己的父類加載器捏膨。不過這里類加載器之間的父子關(guān)系一般不是以繼承(Inheritance)的關(guān)系來實(shí)現(xiàn)的秧均,而是通常使用組合(Composition)關(guān)系來復(fù)用父加載器的代碼。圖:
這里說個(gè)有意思的問題号涯,不止一次在某些文章留言中看到糾結(jié):『為什么叫做雙親目胡?』國外文章寫的 parent delegation model,這里的parent不是單親嗎链快?誉己?應(yīng)該翻譯為單親委派模型才對,全互聯(lián)網(wǎng)都跟著錯(cuò)誤走域蜗。巨双。。其實(shí)parent這個(gè)英文單詞翻譯過來也有雙親的意思霉祸,不需要做個(gè)『杠精』炉峰,沒啥意義哈。
雙親委派模型工作過程
結(jié)合類加載器的自底向上的委托關(guān)系總結(jié):
假設(shè)一個(gè)類處于ClassPath下脉执,版本是JDK8疼阔,默認(rèn)使用應(yīng)用類加載器進(jìn)行加載。
1)當(dāng)應(yīng)用類加載器收到了類加載的請求,會把這個(gè)請求委派給它的父類(擴(kuò)展類)加載器去完成婆廊。
2)擴(kuò)展類加載器收到類加載的請求迅细,會把這個(gè)請求委派給它的父類(引導(dǎo)類)加載器去完成。
3)引導(dǎo)類加載器收到類加載的請求淘邻,查找下自己的特定庫是否能加載該類茵典,即在rt.jar、tools.jar...包中的類宾舅。發(fā)現(xiàn)不能呀统阿!返回給擴(kuò)展類加載器結(jié)果。
4)擴(kuò)展類加載器收到返回結(jié)果筹我,查找下自己的擴(kuò)展目錄下是否能加載該類扶平,發(fā)現(xiàn)不能啊蔬蕊!返回給應(yīng)用類加載器結(jié)果结澄。
5)應(yīng)用類加載器收到結(jié)果,額岸夯!都沒有加載成功麻献,那只能自己加載這個(gè)類了,發(fā)現(xiàn)在ClassPath中找到了猜扮,加載成功勉吻。
你對并發(fā)很感興趣,自己創(chuàng)建了個(gè)跟JDK一樣的全限定名類LongAdder旅赢, java.util.concurrent.atomic.LongAdder
齿桃,然后程序啟動交給類加載器去加載,能成功嗎鲜漩?
當(dāng)然不能!這個(gè)LongAdder是 Doug Lea 大神寫的集惋,貢獻(xiàn)到JDK并發(fā)包下的孕似,并且被安排在rt.jar包中了,因此是由 Bootstrap ClassLoader 類加載器優(yōu)先加載的刮刑,別人誰寫同樣的類喉祭,那就是故意跟JDK作對,是絕對不容許的雷绢。
即使你寫了同樣的類泛烙,編譯可以通過,但是永遠(yuǎn)不會被加載運(yùn)行翘紊,被JDK直接忽略掉蔽氨。
ClassLoader源碼分析
雙親委派模型在JDK中內(nèi)部是如何實(shí)現(xiàn)的?
JDK中提供了一個(gè)抽象的類加載器 ClassLoader,其中提供了三個(gè)非常核心的方法鹉究。
public abstract class ClassLoader {
//每個(gè)類加載器都有個(gè)父加載器
private final ClassLoader parent;
public Class<?> loadClass(String name) {
//查找一下這個(gè)類是不是已經(jīng)加載過了
Class<?> c = findLoadedClass(name);
//如果沒有加載過
if( c == null ){
//先委托給父加載器去加載宇立,注意這是個(gè)遞歸調(diào)用
if (parent != null) {
c = parent.loadClass(name);
}else {
// 如果父加載器為空,查找Bootstrap加載器是不是加載過了
c = findBootstrapClassOrNull(name);
}
}
// 如果父加載器沒加載成功自赔,調(diào)用自己的findClass去加載
if (c == null) {
c = findClass(name);
}
return c妈嘹;
}
protected Class<?> findClass(String name){
//1. 根據(jù)傳入的類名name,到在特定目錄下去尋找類文件绍妨,把.class文件讀入內(nèi)存
...
//2. 調(diào)用defineClass將字節(jié)數(shù)組轉(zhuǎn)成Class對象
return defineClass(buf, off, len)润脸;
}
// 將字節(jié)碼數(shù)組解析成一個(gè)Class對象,用native方法實(shí)現(xiàn)
protected final Class<?> defineClass(byte[] b, int off, int len){
...
}
}
參見ClassLoader核心代碼注釋他去,提取和印證幾個(gè)關(guān)鍵信息:
1)JVM 的類加載器是分層次的毙驯,它們有父子關(guān)系,每個(gè)類加載器都有個(gè)父加載器孤页,是parent字段尔苦。
2)loadClass() 方法是 public 修飾的,說明它才是對外提供服務(wù)的接口行施。根據(jù)源碼可看出這是一個(gè)遞歸調(diào)用允坚,父子關(guān)系是一種組合關(guān)系,子加載器持有父加載器的引用蛾号,當(dāng)一個(gè)類加載器需要加載一個(gè) Java 類時(shí)稠项,會先委托父加載器去加載,然后父加載器在自己的加載路徑中搜索 Java 類鲜结,當(dāng)父加載器在自己的加載范圍內(nèi)找不到時(shí)展运,才會交還給子加載器加載,這就是所謂的『雙親委托模型』精刷。
3)**findClass() **方法的主要職責(zé)就是找到 .class 文件拗胜,可能來自磁盤或者網(wǎng)絡(luò),找到后把.class文件讀到內(nèi)存得到byte[]字節(jié)碼數(shù)組怒允,然后調(diào)用 defineClass() 方法得到 Class 對象埂软。
4)defineClass() 是個(gè)工具方法,它的職責(zé)是調(diào)用 native 方法把 Java 類的字節(jié)碼解析成一個(gè) Class 對象纫事,所謂的 native 方法就是由 C 語言實(shí)現(xiàn)的方法勘畔,Java 通過 JNI 機(jī)制調(diào)用。
雙親委派模型在JDK不同版本中有哪些變化丽惶?
JDK8中的三層類加載器:
JDK8以及之前的JDK版本都是如下三層類加載器實(shí)現(xiàn)方式炫七。
1)啟動類加載器(Bootstrap ClassLoader),這個(gè)類加載器是由C++實(shí)現(xiàn)的钾唬,負(fù)載加載$JAVA_HOME/jre/lib目錄下的jar文件万哪,比如 rt.jar侠驯、tools.jar,或者-Xbootclasspath系統(tǒng)環(huán)境變量指定目錄下的路徑壤圃。它是個(gè)超級公民陵霉,即使開啟了Security Manager的時(shí)候,它也能擁有加載程序的所有權(quán)限伍绳,使用null作為擴(kuò)展類加載器的父類踊挠。
同時(shí),啟動類加載器在JVM啟動后也用于加載擴(kuò)展類加載器和系統(tǒng)類加載器冲杀。
2)擴(kuò)展類加載器(Extension ClassLoader)效床,這個(gè)類加載器由sun.misc.Launcher$ExtClassLoader
來實(shí)現(xiàn),負(fù)責(zé)加載$JAVA_HOME/jre/lib/ext目錄中权谁,或者java.ext.dirs系統(tǒng)變量指定路徑中所有的類庫剩檀,允許用戶將具備通用性的類庫可以放到ext目錄下,擴(kuò)展Java SE功能旺芽。在JDK 9之后沪猴,這種擴(kuò)展機(jī)制被模塊化帶來的天然的擴(kuò)展能力所取代。
3)應(yīng)用類加載器(App/System ClassLoader)采章,也稱作為系統(tǒng)類加載器运嗜,這個(gè)類加載器由sun.misc.Launcher$AppClassLoader
來實(shí)現(xiàn)。 它負(fù)責(zé)加載用戶應(yīng)用類路徑(ClassPath)上所有的類庫悯舟,開發(fā)者同樣可以直接在代碼中使用這個(gè)類加載器担租。如果應(yīng)用程序中沒有自定義過自己的類加載器,一般情況下這個(gè)就是程序中默認(rèn)的類加載器抵怎。
JDK9中的類加載器有哪些變化奋救?
1)擴(kuò)展類加載器被重命名為平臺類加載器(Platform ClassLoader),部分不需要 AllPermission 的 Java 基礎(chǔ)模塊反惕,被降級到平臺類加載器中尝艘,相應(yīng)的權(quán)限也被更精細(xì)粒度地限制起來。
2) 擴(kuò)展類加載器機(jī)制被移除姿染。這會帶來什么影響呢背亥?就是說如果我們指定 java.ext.dirs 環(huán)境變量,或者 $JAVA_HOME/jre/lib/ext目錄存在盔粹,JVM會返回錯(cuò)誤隘梨。 建議解決辦法就是將其放入 classpath 里程癌。部分不需要 AllPermission 的 Java 基礎(chǔ)模塊舷嗡,被降級到平臺類加載器中,相應(yīng)的權(quán)限也被更精細(xì)粒度地限制起來嵌莉。
3)在$JAVA_HOME/jre/lib路徑下的 rt.jar 和 tools.jar 同樣是被移除了进萄。JDK 的核心類庫以及相關(guān)資源,被存儲在 jimage 文件中,并通過新的 JRT 文件系統(tǒng)訪問中鼠,而不是原有的 JAR 文件系統(tǒng)可婶。
4)增加了 Layer 的抽象, JVM 啟動默認(rèn)創(chuàng)建 BootLayer援雇,開發(fā)者也可以自己去定義和實(shí)例化 Layer矛渴,可以更加方便的實(shí)現(xiàn)類似容器一般的邏輯抽象。
新增的Layer的抽象惫搏,去內(nèi)部的BootLayer作為內(nèi)建類加載器具温,包括了 BootStrap Loader、Platform Loader筐赔、Application Loader铣猩,其他 Layer 內(nèi)部有自定義的類加載器,不同版本模塊可以同時(shí)工作在不同的 Layer茴丰。
結(jié)合了 Layer达皿,目前最新的 JVM 內(nèi)部結(jié)構(gòu)如下圖所示:
5、Tomcat如何打破雙親委派模型的
因?yàn)镴DK里的類加載器ClassLoader是抽象類贿肩,如果你自定義類加載器可以重寫 findClass() 方法峦椰,重寫 findClass() 方法還是會按照既定的雙親委派機(jī)制運(yùn)作的。
而我們發(fā)現(xiàn)loadClass()方法也是public修飾的尸曼,說明也是允許重寫的们何,重寫loadClass()方法就可以『為所欲為』了,不按照既定套路出牌了控轿,不遵循雙親委派模型冤竹。
典型的就是Tomcat應(yīng)用容器,就是自定義WebAppClassLoader類加載器茬射,打破了雙親委派模型鹦蠕。
WebAppClassLoader 類加載器具體實(shí)現(xiàn)是重寫了 ClassLoader 的兩個(gè)方法:loadClass() 和 findClass()。其大致工作過程:首先類加載器自己嘗試去加載某個(gè)類在抛,如果找不到再委托代理給父類加載器钟病,其目的是優(yōu)先加載 Web 應(yīng)用自己定義的類。
這也正是一個(gè)Tomcat能夠部署多個(gè)應(yīng)用實(shí)例的根本原因刚梭。
接下來肠阱,我們分析下源碼實(shí)現(xiàn):
loadClass() 重寫方法的源碼實(shí)現(xiàn),僅保留最核心的代碼便于理解:
// 重寫了 loadClass() 方法
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 使用了synchronized同步鎖
synchronized (getClassLoadingLock(name)) {
Class<?> clazz = null;
//1)先在本地緩存中朴读,查找該類是否已經(jīng)加載過
clazz = findLoadedClass0(name);
if (clazz != null) {
if (resolve)
// 本地緩存找到屹徘,連接該類
resolveClass(clazz);
return clazz;
}
//2) 從系統(tǒng)類加載器的緩存中,查找該類是否已經(jīng)加載過
clazz = findLoadedClass(name);
if (clazz != null) {
if (resolve)
// 從系統(tǒng)類加載器緩存找到衅金,連接該類
resolveClass(clazz);
return clazz;
}
// 3)嘗試用ExtClassLoader類加載器類加載
ClassLoader javaseLoader = getJavaseClassLoader();
try {
clazz = javaseLoader.loadClass(name);
if (clazz != null) {
if (resolve)
// 從擴(kuò)展類加載器中找到噪伊,連接該類
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
// 4)嘗試在本地目錄查找加載該類
try {
clazz = findClass(name);
if (clazz != null) {
if (resolve)
// 從本地目錄找到簿煌,連接該類
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
// 5) 嘗試用系統(tǒng)類加載器來加載
try {
clazz = Class.forName(name, false, parent);
if (clazz != null) {
if (resolve)
// 從系統(tǒng)類加載器中找到,連接該類
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
//6. 上述過程都加載失敗鉴吹,拋出異常
throw new ClassNotFoundException(name);
}
loadClass() 重寫的方法實(shí)現(xiàn)上會復(fù)雜些姨伟,畢竟打破雙親委派機(jī)制就在這里實(shí)現(xiàn)的。
主要有如下幾個(gè)步驟:
1)先在本地緩存 Cache 查找該類是否已經(jīng)加載過豆励,即 Tomcat 自定義類加載器 WebAppClassLoader 是否已加載過夺荒。
2)如果 Tomcat 類加載器沒有加載過這個(gè)類,再看看系統(tǒng)類加載器是否加載過良蒸。
3)如果系統(tǒng)類加載器也沒有加載過般堆,此時(shí),會讓 ExtClassLoader 擴(kuò)展類加載器去加載诚啃,很關(guān)鍵淮摔,其目的防止 Web 應(yīng)用自己的類覆蓋 JRE 的核心類渣蜗。
因?yàn)?Tomcat 需要打破雙親委托機(jī)制烁设,假如 Web 應(yīng)用里有類似上面舉的例子自定義了 Object 類,如果先加載這些JDK中已有的類舟误,會導(dǎo)致覆蓋掉JDK里面的那個(gè) Object 類造垛。
這就是為什么 Tomcat 的類加載器會優(yōu)先嘗試用 ExtClassLoader 去加載魔招,因?yàn)?ExtClassLoader 會委托給 BootstrapClassLoader 去加載,JRE里的類由BootstrapClassLoader安全加載五辽,然后返回給 Tomcat 的類加載器办斑。
這樣 Tomcat 的類加載器就不會去加載 Web 應(yīng)用下的 Object 類了,也就避免了覆蓋 JRE 核心類的問題杆逗。
4)如果 ExtClassLoader 加載器加載失敗乡翅,也就是說 JRE 核心類中沒有這類,那么就在本地 Web 應(yīng)用目錄下查找并加載罪郊。
5)如果本地目錄下沒有這個(gè)類蠕蚜,說明不是 Web 應(yīng)用自己定義的類,那么由系統(tǒng)類加載器去加載悔橄。這里請你注意:Web 應(yīng)用是通過Class.forName調(diào)用交給系統(tǒng)類加載器的靶累,因?yàn)镃lass.forName的默認(rèn)加載器就是系統(tǒng)類加載器。
6)如果上述加載過程全部失敗癣疟,拋出 ClassNotFoundException 異常挣柬。
findClass() 重寫方法的源碼實(shí)現(xiàn),僅展示最核心代碼便于理解:
// 重寫了 findClass 方法
public Class<?> findClass(String name) throws ClassNotFoundException {
...
Class<?> clazz = null;
try {
//1) 優(yōu)先在自己Web應(yīng)用目錄下查找類
clazz = findClassInternal(name);
} catch (RuntimeException e) {
throw e;
}
if (clazz == null) {
try {
//2) 如果在本地目錄沒有找到當(dāng)前類睛挚,則委托代理給父加載器去查找
clazz = super.findClass(name);
} catch (RuntimeException e) {
throw e;
}
//3) 如果父類加載器也沒找到邪蛔,則拋出ClassNotFoundException
if (clazz == null) {
throw new ClassNotFoundException(name);
}
return clazz;
}
在 findClass() 重寫的方法里,主要有三個(gè)步驟:
1)先在 Web 應(yīng)用本地目錄下查找要加載的類竞川。
2)如果沒有找到店溢,交給父加載器去查找,它的父加載器就是上面提到的系統(tǒng)類加載器 AppClassLoader委乌。
3)如何父加載器也沒找到這個(gè)類床牧,拋出 ClassNotFoundException 異常。
6遭贸、上下文類加載器深入淺出剖析
我們都知道Jdbc是一個(gè)標(biāo)準(zhǔn)戈咳,那么具體數(shù)據(jù)庫廠商會根據(jù)Jdbc標(biāo)準(zhǔn)提供自己的數(shù)據(jù)庫實(shí)現(xiàn),既然Jdbc是一個(gè)標(biāo)準(zhǔn)壕吹,這些類原生的會存在JDK中了著蛙,比如Connection、Statement耳贬,而且是位于rt.jar包中的踏堡,他們在啟動的時(shí)候是由BootstrapClassLoader加載的。
那么怎么具體加載廠商的實(shí)現(xiàn)呢咒劲?
肯定是通過廠商提供相應(yīng)的jar包顷蟆,然后放到我們應(yīng)用的ClassPath下,這樣的話腐魂,廠商所提供的jar中的肯定不是由啟動類加載器去加載的帐偎。
所以,廠商的具體驅(qū)動的實(shí)現(xiàn)是由應(yīng)用類加載器進(jìn)行加載的 蛔屹。
Connection是一個(gè)接口削樊,它是由啟動類加載器加載的,而它具體的實(shí)現(xiàn)啟動類加載器無法加載兔毒,由系統(tǒng)類加載器加載的漫贞。這樣會存在什么樣的問題?
根據(jù)**類加載原則: **
- 父類加載器的加載類或接口是看不到子類加載器加載的類或接口的育叁。
- 子加載器所加載的類或接口是能看到父加載器加載的類或接口的绕辖。
SPI(Service Provider Interface)
父ClassLoader可以使用當(dāng)前線程Thread.currentThread().getContextClassLoader()所指定的classloader加載的類。
這就改變了父ClassLoader不能使用子ClassLoader或是其他沒有直接父子關(guān)系的ClassLoader所加載類的情況擂红,即改變了雙親委托模型仪际。
線程上下文類加載器就是當(dāng)前線程的Current Classloader。
在雙親委托模型下昵骤,類加載器是由下而上树碱,即下層的類加載器會委托上層進(jìn)行加載。但是對于SPI來說变秦,有些接口是Java核心庫所提供的成榜,而Java核心庫是由啟動類加載器來加載的,而這些接口的實(shí)現(xiàn)卻來自于不同jar包(廠商提供)蹦玫,Java的啟動類加載器是不會加載其他來源的jar包赎婚,這樣傳統(tǒng)的雙親委托模型就無法滿足SPI的要求刘绣。
而通過給當(dāng)前線程設(shè)置上下文類加載器,就可以由設(shè)置的上下文類加載器來實(shí)現(xiàn)對于接口實(shí)現(xiàn)類的加載挣输。
線程上下文類加載器的一般使用模式:
獲取 ---> 使用 --> 還原
ClassLoader classloader = Thread.currentThread().getContextClassLoader();
try {
// 將目標(biāo)類加載器設(shè)置到上下文類加載器
Thread.currentThread().setContextClassLoader(targetTccl);
// 在該方法中使用設(shè)置的上下文類加載器加載所需的類
doSomethingUsingContextClassLoader();
} finally {
// 將原來的classloader設(shè)置到上下文類加載器
Thread.currentThread().setContextClassLoader(classloader);
}
doSomethingUsingContextClassLoader()方法中則調(diào)用了 Thread.currentThread().getContextClassLoader() ,獲取當(dāng)前線程的上下文類加載器做某些事情纬凤。
如果一個(gè)類由類加載器A加載,那么這個(gè)類的依賴類也是由相同的類加載器加載的(如果該依賴類之前沒有被加載過的話)撩嚼。
在SPI的接口代碼當(dāng)中停士,就可以通過上下文類加載器成功的加載到SPI的實(shí)現(xiàn)類。因此完丽,上下文類加載器在很多的SPI的實(shí)現(xiàn)中都會得到大量的應(yīng)用恋技。
當(dāng)高層提供了統(tǒng)一的接口讓低層(比如Jdbc各個(gè)廠商提供的具體實(shí)現(xiàn)類)去實(shí)現(xiàn),同時(shí)又要在高層加載(或?qū)嵗┑蛯拥念悤r(shí)逻族,就必須要通過線程上下文類加載器來幫助高層的類加載器并加載該類(本質(zhì)上蜻底,高層的類加載器與低層的類加載器是不一樣的)
一般情況下,我們沒有修改過線程上下文類加載器聘鳞,默認(rèn)的就是系統(tǒng)類加載器朱躺。由于是運(yùn)行期間是設(shè)置的上下文類加載器,所以搁痛,不管當(dāng)前程序在什么地方长搀,在啟動類的加載器的范圍內(nèi)還是擴(kuò)展類加載器的范圍內(nèi),那么我們在任何有需要的時(shí)候都是可以通過Thread.currentThread().getContextClassLoader()獲取設(shè)置的上下文類加載器來完成操作鸡典。
這個(gè)也有點(diǎn)像ThreadLocal的類源请,如果借助于ThreadLocal的話就沒有必要同步,因?yàn)槊恳粋€(gè)線程都有相應(yīng)的數(shù)據(jù)副本彻况,這些數(shù)據(jù)副本之間是互不干擾的谁尸,他們只能被當(dāng)前的線程所使用和訪問,既然每個(gè)線程都有數(shù)據(jù)副本纽甘,每個(gè)線程當(dāng)然操作的是副本良蛮,所以線程之間就不需要同步、鎖就可以處理并發(fā)悍赢。ThreadLocal本質(zhì)上是用空間換時(shí)間的概念决瞳,因?yàn)槲覀儗?shù)據(jù)拷貝多份會占用一定的內(nèi)存空間,每個(gè)線程中去使用左权。
7皮胡、最后的總結(jié)
限于篇幅,本文主要對類的初始化時(shí)機(jī)赏迟,類的加載過程中最重要的類加載器機(jī)制進(jìn)行了分析屡贺,對其中的雙親委派模型,以及Tomcat是如何打破雙親委派模型的,結(jié)合源代碼進(jìn)行了深入剖析甩栈,對上下文類加載器是如何改變雙親委派模型進(jìn)行了分析泻仙。
總結(jié)一下:
一個(gè)類都是通過主動使用
的方式加載到JVM當(dāng)中的,到目前為止一共總結(jié)了八種情況量没,除此之外的都屬于被動使用
玉转,被動使用的列舉了代碼示例,結(jié)合示例可以更為清晰的理解允蜈。
詳細(xì)介紹了雙親委派模型的工作過程,JDK8和JDK9版本中類加載器層次關(guān)系蒿柳,類加載器的結(jié)果本質(zhì)上并不是一種樹形結(jié)構(gòu)饶套,而是一種包含關(guān)系。
同時(shí)垒探,也介紹了Tomcat是如何打破雙親委派機(jī)制的妓蛮,通過源碼透視打破規(guī)則的全過程。
最后圾叼,對上下文類加載器根據(jù)Jdbc的例子蛤克,進(jìn)一步分析了使用模式,如何改變雙親委派機(jī)制做到父類加載器夷蚊,可以加載和使用各個(gè)廠商提供的實(shí)現(xiàn)類的构挤。
另外,回到最初的圖示惕鼓,一個(gè)類要想順利進(jìn)入到JVM內(nèi)存結(jié)構(gòu)中筋现,除了類的加載階段外,還有驗(yàn)證箱歧、準(zhǔn)備矾飞、解析、初始化四個(gè)階段完成后呀邢,才算真正完成類的初始化操作洒沦。
在JVM中某個(gè)類的Class對象不再被引用,即不可觸及价淌,Class對象就會結(jié)束生命周期申眼,該類在方法區(qū)內(nèi)的數(shù)據(jù)會被卸載,從而技術(shù)該類的整個(gè)生命周期蝉衣。
一個(gè)類何時(shí)結(jié)束生命周期豺型,取決于代表它的Class對象何時(shí)結(jié)束生命周期。
但是买乃,JVM自帶的類加載器所加載的類姻氨,在虛擬機(jī)的生命周期中,始終不會被卸載剪验。前面已經(jīng)介紹過肴焊,JVM自帶的類加載器包括引導(dǎo)類加載器前联、擴(kuò)展類加載器和系統(tǒng)類加載器(應(yīng)用類加載器)。Java虛擬機(jī)本身會始終引用這些類加載器娶眷,而這些類加載器會始終引用它們所加載的類的Class對象似嗤,因此這些Class對象是始終可觸及的。
在如下情況下届宠,JVM將結(jié)束生命周期烁落。
執(zhí)行了System.exit()
程序正常執(zhí)行結(jié)束
程序在執(zhí)行過程中遇到了異常或者錯(cuò)誤而異常終止
由于操作系統(tǒng)出現(xiàn)錯(cuò)誤而導(dǎo)致Java虛擬機(jī)進(jìn)程終止
大家如何覺得本文有收獲關(guān)個(gè)注唄豌注,碼字不易伤塌,文章不妥之處,歡迎留言斧正轧铁。本號不定期會發(fā)布精彩原創(chuàng)文章每聪。
參考資料:
深入理解Java虛擬機(jī)
極客時(shí)間課程