詳解Java類加載器與雙親委派機(jī)制
引子
大家想必都有過(guò)平時(shí)開發(fā)springboot 項(xiàng)目的時(shí)候稍微改動(dòng)一點(diǎn)代碼,就得重啟,就很煩
網(wǎng)上一般介紹 2種方式 spring-boot-devtools
,或者通過(guò)JRebel
插件 來(lái)實(shí)現(xiàn)"熱部署"
熱部署就是當(dāng)應(yīng)用正在運(yùn)行時(shí),修改應(yīng)用不需要重啟應(yīng)用。
其中 spring-boot-devtools
其實(shí)是自動(dòng)重啟,主要是節(jié)省了我們手動(dòng)點(diǎn)擊重啟的時(shí)間,不算真正意義上的熱部署。JRebel插件啥都好,就是需要收費(fèi)
但如果平時(shí)我們?cè)?strong>調(diào)試debug的情況下,只是在方法塊內(nèi)代碼修改了一下,我們還得重啟項(xiàng)目,就很浪費(fèi)時(shí)間。這個(gè)時(shí)候我們其實(shí)可以直接build ,不重啟項(xiàng)目,即可 實(shí)現(xiàn)熱部署。
我們先來(lái)寫一個(gè)例子演示一下:
@RestController public?class?TestController?{ ????@RequestMapping(value?=?"/test",method?=?{RequestMethod.GET,?RequestMethod.POST}) ????public?void?testclass()?{ ????????String?name?=?"zj"; ????????int?weight?=?100; ????????System.out.println("name:"+?name); ????????System.out.println("weight:?"+weight); ????} }
結(jié)果:
name:zj weight: 100
修改代碼,然后直接build項(xiàng)目,不重啟項(xiàng)目,我們?cè)僬?qǐng)求這個(gè)測(cè)試接口:
String?name?=?"ming"; int?weight?=?300;
神奇的一幕出現(xiàn)了,結(jié)果為:
name:ming weight: 300
當(dāng)我們修改.java文件,只需重新生成對(duì)應(yīng)的.class文件,就能影響到程序運(yùn)行結(jié)果, 無(wú)需重啟,Why? 背后JVM的操作原理且看本文娓娓道來(lái)。
了解.class文件
首先我們得先了解一下 什么是.class文件
舉個(gè)簡(jiǎn)單的例子,創(chuàng)建一個(gè)Person類:
public?class?Person?{ ????/** ?????*?狀態(tài)?or?屬性 ?????*/ ????String?name;//姓名 ????String?sex;//性別 ????int?height;//身高 ????int?weight;//體重 ???? ????/** ?????*?行為 ?????*/ ????public?void?sleep(){ ?????System.out.println(this.name+"--"+?"睡覺(jué)"); ?} ????public?void?eat(){ ????????System.out.println("吃飯"); ????} ????public?void?Dance(){ ????????System.out.println("跳舞"); ????} }
我們執(zhí)行javac命令,生成Person.class文件
然后我們通過(guò)vim 16進(jìn)制
打開它
#打開file文件 vim?Person.class? #在命令模式下輸入..?以16進(jìn)制顯示 ?:%!xxd ? #在命令模式下輸入..?切換回默認(rèn)顯示 :%!xxd?-r
不同的操作系統(tǒng),不同的 CPU 具有不同的指令集,JAVA能做到平臺(tái)無(wú)關(guān)性,依靠的就是 Java 虛擬機(jī)。.java源碼是給人類讀的,而.class字節(jié)碼是給JVM虛擬機(jī)讀的,計(jì)算機(jī)只能識(shí)別 0 和 1組成的二進(jìn)制文件,所以虛擬機(jī)就是我們編寫的代碼和計(jì)算機(jī)之間的橋梁。
虛擬機(jī)將我們編寫的 .java 源程序文件編譯為 字節(jié)碼 格式的 .class 文件,字節(jié)碼是各種虛擬機(jī)與所有平臺(tái)統(tǒng)一使用的程序存儲(chǔ)格式,class文件主要用于解決平臺(tái)無(wú)關(guān)性的中間文件
類加載的過(guò)程
在之前的一篇文章談?wù)凧AVA中對(duì)象和類、this、super和static關(guān)鍵字中,我們知曉 Java 是如何創(chuàng)建對(duì)象的
?Person?zhang?=?new?Person();
雖然我們寫的時(shí)候是簡(jiǎn)單的一句,但是JVM內(nèi)部的實(shí)現(xiàn)過(guò)程卻是復(fù)雜的:
- 將硬盤上指定位置的Person.class文件加載進(jìn)內(nèi)存
- 執(zhí)行main方法時(shí),在棧內(nèi)存中開辟了main方法的空間(壓棧-進(jìn)棧),然后在main方法的棧區(qū)分配了一個(gè)變量zhang。
- 執(zhí)行new,在堆內(nèi)存中開辟一個(gè) 實(shí)體類的 空間,分配了一個(gè)內(nèi)存首地址值
- 調(diào)用該實(shí)體類對(duì)應(yīng)的構(gòu)造函數(shù),進(jìn)行初始化(如果沒(méi)有構(gòu)造函數(shù),Java會(huì)補(bǔ)上一個(gè)默認(rèn)構(gòu)造函數(shù))。
- 將實(shí)體類的 首地址賦值給zhang,變量zhang就引用了該實(shí)體。(指向了該對(duì)象)
類加載過(guò)程
其中 上圖步驟1 Classloader(類加載器) 將class文件加載到內(nèi)存中具體分為3個(gè)步驟:加載、連接、初始化
類的生命周期一般有如下圖有7個(gè)階段,其中階段1-5為類加載過(guò)程,驗(yàn)證、準(zhǔn)備、解析統(tǒng)稱為連接
類的生命周期
1.加載
加載階段:指的是將類對(duì)應(yīng)的.class文件中的二進(jìn)制字節(jié)流讀入到內(nèi)存中,將這個(gè)字節(jié)流轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu),然后在堆區(qū)創(chuàng)建一個(gè)java.lang.Class 對(duì)象,作為對(duì)方法區(qū)中這些數(shù)據(jù)的訪問(wèn)入口
相對(duì)于類加載的其他階段而言,加載階段(準(zhǔn)確地說(shuō),是加載階段獲取類的二進(jìn)制字節(jié)流的動(dòng)作)是我們最可以控制的階段,因?yàn)殚_發(fā)人員既可以使用系統(tǒng)提供的類加載器來(lái)完成加載,也可以自定義類加載器來(lái)完成加載。這個(gè)我們文章后面再詳細(xì)講
2.驗(yàn)證
驗(yàn)證階段:校驗(yàn)字節(jié)碼文件正確性。這一階段的目的是為了確保Class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求,并且不會(huì)危害虛擬機(jī)自身的安全。
這部分對(duì)開發(fā)者而言是無(wú)法干預(yù)的,以下內(nèi)容了解即可
驗(yàn)證階段大致會(huì)完成4個(gè)階段的檢驗(yàn)動(dòng)作:
文件格式驗(yàn)證:驗(yàn)證字節(jié)流是否符合Class文件格式的規(guī)范;例如:是否以0xCAFEBABE
開頭、主次版本號(hào)是否在當(dāng)前虛擬機(jī)的處理范圍之內(nèi)、常量池中的常量是否有不被支持的類型。
元數(shù)據(jù)驗(yàn)證:對(duì)字節(jié)碼描述的信息進(jìn)行語(yǔ)義分析(注意:對(duì)比javac編譯階段的語(yǔ)義分析),以保證其描述的信息符合Java語(yǔ)言規(guī)范的要求;例如:這個(gè)類是否有父類,除了java.lang.Object之外。
字節(jié)碼驗(yàn)證:通過(guò)數(shù)據(jù)流和控制流分析,確定程序語(yǔ)義是合法的、符合邏輯的。
符號(hào)引用驗(yàn)證:確保解析動(dòng)作能正確執(zhí)行。
驗(yàn)證階段是非常重要的,但不是必須的,它對(duì)程序運(yùn)行期沒(méi)有影響,如果所引用的類經(jīng)過(guò)反復(fù)驗(yàn)證,那么可以考慮采用-Xverifynone
參數(shù)來(lái)關(guān)閉大部分的類驗(yàn)證措施,以縮短虛擬機(jī)類加載的時(shí)間。
3.準(zhǔn)備
準(zhǔn)備階段:為類變量(static 修飾的變量)分配內(nèi)存,并將其初始化為默認(rèn)值
注意此階段僅僅是為類變量 即靜態(tài)變量分配內(nèi)存,并將其初始化為默認(rèn)值
舉個(gè)例子,在這個(gè)準(zhǔn)備階段:
static int value = 3;//類變量?初始化,設(shè)為默認(rèn)值?0,不是 3哦??。?! int num = 4;//類成員變量,在這個(gè)階段不初始化;在 new類,調(diào)用對(duì)應(yīng)類的構(gòu)造函數(shù)才進(jìn)行初始化 final static valFin = 5;//這個(gè)比較特殊,在這個(gè)階段也不會(huì)分配內(nèi)存?。?!
注意: valFin
是被final static修飾的常量
在 **編譯 **的時(shí)候已分配好了,所以在準(zhǔn)備階段 此時(shí)的值為5,所以在這個(gè)階段也不會(huì)初始化!
4.解析
解析階段:是虛擬機(jī)將常量池內(nèi)的符號(hào)引用
替換為直接引用
的過(guò)程,解析動(dòng)作主要針對(duì)類或接口、字段、類方法、接口方法、方法類型、方法句柄和調(diào)用點(diǎn)限定符7類符號(hào)引用進(jìn)行。
符號(hào)引用就是一組符號(hào)來(lái)描述目標(biāo),可以是任何字面量。
直接引用就是直接指向目標(biāo)的指針、相對(duì)偏移量或一個(gè)間接定位到目標(biāo)的句柄。
這個(gè)階段了解一下即可
5.初始化
直到初始化階段,Java虛擬機(jī)才真正開始執(zhí)行類中編寫的Java程序代碼,將主導(dǎo)權(quán)移交給應(yīng)用程序。
初始化階段 是類加載過(guò)程的最后一個(gè)步驟,之前介紹的幾個(gè)類加載的動(dòng)作里,除了在加載階段
用戶應(yīng)用程序可以通過(guò)自定義類加載器
的方式局部參與外,其余動(dòng)作都完全由Java虛擬機(jī)來(lái)主導(dǎo)控 制。
Java程序?qū)︻惖氖褂梅绞娇煞譃閮煞N:主動(dòng)使用
與被動(dòng)使用
。一般來(lái)說(shuō)只有當(dāng)對(duì)類的首次主動(dòng)使用的時(shí)候才會(huì)導(dǎo)致類的初始化,所以主動(dòng)使用又叫做類加載過(guò)程中“初始化”開始的時(shí)機(jī)。
類實(shí)例初始化方式,主要是以下幾種:
1、創(chuàng)建類的實(shí)例,也就是new的方式
2、訪問(wèn)某個(gè)類或接口的靜態(tài)變量,或者對(duì)該靜態(tài)變量賦值
3、調(diào)用類的靜態(tài)方法
4、反射(如Class.forName("com.test.Person")
)
5、初始化某個(gè)類的子類,則其父類也會(huì)被初始化
6、Java虛擬機(jī)啟動(dòng)時(shí)被標(biāo)明為啟動(dòng)類的類(JavaTest),還有就是Main方法的類會(huì) 首先被初始化
這邊就不展開說(shuō)了,大家記住即可
6.使用
當(dāng)JVM完成初始化階段之后,JVM便開始從入口方法開始執(zhí)行用戶的程序代碼
7.卸載
當(dāng)用戶程序代碼執(zhí)行完畢后,JVM便開始銷毀創(chuàng)建的Class對(duì)象,最后負(fù)責(zé)運(yùn)行的JVM也退出內(nèi)存
在如下幾種情況下,Java虛擬機(jī)將結(jié)束生命周期
執(zhí)行了System.exit()方法
程序正常執(zhí)行結(jié)束
程序在執(zhí)行過(guò)程中遇到了異?;蝈e(cuò)誤而異常終止
由于操作系統(tǒng)出現(xiàn)錯(cuò)誤而導(dǎo)致Java虛擬機(jī)進(jìn)程終止
類加載器 與 雙親委派機(jī)制
上文類加載過(guò)程中,是需要類加載器的參與,類加載器在Java中非常重要,它使得 Java 類可以被動(dòng)態(tài)加載到 Java 虛擬機(jī)中并執(zhí)行
那什么是類加載器?通過(guò)一個(gè)類的全限定名來(lái)獲取描述此類的二進(jìn)制字節(jié)流到JVM中,然后轉(zhuǎn)換為一個(gè)與目標(biāo)類對(duì)應(yīng)的java.lang.Class對(duì)象實(shí)例
Java虛擬機(jī)支持類加載器的種類:主要包括3中:引導(dǎo)類加載器(Bootstrap ClassLoader)、擴(kuò)展類加載器(Extension ClassLoader)、應(yīng)用類加載器(系統(tǒng)類加載器,AppClassLoader),另外我們還可以自定義加載器-用戶自定義類加載器
- 引導(dǎo)類加載器(Bootstrap ClassLoader):
BootStrapClassLoader
是由c++實(shí)現(xiàn)的。引導(dǎo)類加載器加載java運(yùn)行過(guò)程中的核心類庫(kù)JRE\lib\rt.jar,sunrsasign.jar, charsets.jar, jce.jar, jsse.jar, plugin.jar
以及存放 在JRE\classes
里的類,也就是JDK提供的類等常見(jiàn)的比如:Object、Stirng、List
等 - 擴(kuò)展類加載器(Extension ClassLoader):它用來(lái)加載
/jre/lib/ext
目錄以及java.ext.dirs
系統(tǒng)變量指定的類路徑下的類。 - 應(yīng)用類加載器(AppClassLoader):它主要加載應(yīng)用程序ClassPath下的類(包含jar包中的類)。它是java應(yīng)用程序默認(rèn)的類加載器。其實(shí)就是加載我們一般開發(fā)使用的類
- 用戶自定義類加載器:用戶根據(jù)自定義需求,自由的定制加載的邏輯,只需繼承應(yīng)用類加載器AppClassLoader,負(fù)責(zé)加載用戶自定義路徑下的class字節(jié)碼文件
- 線程上下文類加載器:除了以上列舉的三種類加載器,其實(shí)還有一種比較特殊的類型就是
線程上下文類加載器
。ThreadContextClassLoader可以是上述類加載器的任意一種,這個(gè)我們下文再細(xì)說(shuō)
我們來(lái)看一個(gè)例子:
public?class?TestClassLoader?{ ????public?static?void?main(String[]?args)?throws?ClassNotFoundException?{ ????????ClassLoader?classLoader?=?TestClassLoader.class.getClassLoader(); ????????System.out.println(classLoader); ????????System.out.println(classLoader.getParent());//獲取其父類加載器 ????????System.out.println(classLoader.getParent().getParent());//獲取父類的父類加載器 ????} }
結(jié)果:
sun.misc.Launcher
ExtClassLoader@5caf905d null
結(jié)果顯示分別打印應(yīng)用類加載器、擴(kuò)展類加載器和引導(dǎo)類加載器
由于 引導(dǎo)類加載器 是由c++實(shí)現(xiàn)的,所以并不存在一個(gè)Java的類,因此會(huì)打印出null
我們還可以看到結(jié)果里面打印了 sun.misc.Launcher
,這個(gè)是什么東東?
其實(shí)Launcher是JRE中用于啟動(dòng)程序入口main()的類,我們看下Launcher的源碼:
public?class?Launcher?{ ????private?static?Launcher?launcher?=?new?Launcher(); ????private?static?String?bootClassPath?= ????????System.getProperty("sun.boot.class.path"); ????public?static?Launcher?getLauncher()?{ ????????return?launcher; ????} ????private?ClassLoader?loader; ????public?Launcher()?{ ????????//?Create?the?extension?class?loader ????????ClassLoader?extcl; ????????try?{ ????????????extcl?=?ExtClassLoader.getExtClassLoader();?//加載擴(kuò)展類類加載器 ????????}?catch?(IOException?e)?{ ????????????throw?new?InternalError( ????????????????"Could?not?create?extension?class?loader",?e); ????????} ????????//?Now?create?the?class?loader?to?use?to?launch?the?application ????????try?{ ????????????loader?=?AppClassLoader.getAppClassLoader(extcl);//加載應(yīng)用程序類加載器,并設(shè)置parent為extClassLoader ????????}?catch?(IOException?e)?{ ????????????throw?new?InternalError( ????????????????"Could?not?create?application?class?loader",?e); ????????} ????????Thread.currentThread().setContextClassLoader(loader);?//設(shè)置AppClassLoader為線程上下文類加載器 ????} ????/* ?????*?Returns?the?class?loader?used?to?launch?the?main?application. ?????*/ ????public?ClassLoader?getClassLoader()?{ ????????return?loader; ????} ????/* ?????*?The?class?loader?used?for?loading?installed?extensions. ?????*/ ????static?class?ExtClassLoader?extends?URLClassLoader?{} /** ?????*?The?class?loader?used?for?loading?from?java.class.path. ?????*?runs?in?a?restricted?security?context. ?????*/ ????static?class?AppClassLoader?extends?URLClassLoader?{}
其中loader = AppClassLoader.getAppClassLoader(extcl);
的核心方法源碼如下:
private?ClassLoader(Void?unused,?ClassLoader?parent)?{ ????????this.parent?=?parent;//設(shè)置parent ????????if?(ParallelLoaders.isRegistered(this.getClass()))?{ ????????????parallelLockMap?=?new?ConcurrentHashMap<>(); ????????????package2certs?=?new?ConcurrentHashMap<>(); ????????????assertionLock?=?new?Object(); ????????}?else?{ ????????????//?no?finer-grained?lock;?lock?on?the?classloader?instance ????????????parallelLockMap?=?null; ????????????package2certs?=?new?Hashtable<>(); ????????????assertionLock?=?this; ????????} ????}
通過(guò)以上源碼我們可以知曉:
- Launcher的
ClassLoader
是BootstrapClassLoader
,在Launcher創(chuàng)建的同時(shí),還會(huì)同時(shí)創(chuàng)建ExtClassLoader,AppClassLoader(并設(shè)置其parent為extClassLoader)。其中代碼中 "sun.boot.class.path"是BootstrapClassLoader
加載的jar包路徑。 - 這幾種類加載器 都遵循 雙親委派機(jī)制
雙親委派機(jī)制說(shuō)的其實(shí)就是,當(dāng)一個(gè)類加載器收到一個(gè)類加載請(qǐng)求時(shí),會(huì)去判斷有沒(méi)有加載過(guò),如果加載過(guò)直接返回,否則該類加載器會(huì)把請(qǐng)求先委派給父類加載器。每個(gè)類加載器都是如此,只有在父類加載器在自己的搜索范圍內(nèi)找不到指定類時(shí),子類加載器才會(huì)嘗試自己去加載。
雙親委派模式優(yōu)勢(shì):
- 避免類的重復(fù)加載, 當(dāng)父親已經(jīng)加載了該類時(shí),就沒(méi)有必要子ClassLoader再加載一次, 這樣保證了每個(gè)類只被加載一次。
- 保護(hù)程序安全,防止核心API被隨意篡改,比如 java核心api中定義類型不會(huì)被隨意替換
我們這里看一個(gè)例子:
我們新建一個(gè)自己的類“String”放在src/java/lang目錄下
public?class?String?{ ????static?{ ????????System.out.println("自定義?String類"); ????} }
新建StringTest類:
public?class?StringTest?{ ????public?static?void?main(String[]?args)?{ ????????String?str=new?java.lang.String(); ????????System.out.println("start?test-------"); ????} }
結(jié)果:
start test-------
可以看出,程序并沒(méi)有運(yùn)行我們自定義的“String”類,而是直接返回了String.class。像String,Integer等類 是JAVA中的核心類,是不允許隨意篡改的!
ClassLoader
ClassLoader
是一個(gè)抽象類,負(fù)責(zé)加載類,像 ExtClassLoader,AppClassLoader
都是由該類派生出來(lái),實(shí)現(xiàn)不同的類裝載機(jī)制。這塊的源碼太多了,就不貼了
我們來(lái)看下 它的核心方法loadClass()
,傳入需要加載的類名,它會(huì)幫你加載:
protected?Class<?>?loadClass(String?name,?boolean?resolve)?throws?ClassNotFoundException?{ ????synchronized?(getClassLoadingLock(name))?{ ????????//?一開始先?檢查是否已經(jīng)加載該類 ????????Class<?>?c?=?findLoadedClass(name); ????????if?(c?==?null)?{ ????????????long?t0?=?System.nanoTime(); ????????????try?{ ????????????????//?如果未加載過(guò)類,則遵循?雙親委派機(jī)制,來(lái)加載類 ????????????????if?(parent?!=?null)?{ ????????????????????c?=?parent.loadClass(name,?false); ????????????????}?else?{ ????????????????//如果父類是null就是BootstrapClassLoader,使用?啟動(dòng)類類加載器 ????????????????????c?=?findBootstrapClassOrNull(name); ????????????????} ????????????}?catch?(ClassNotFoundException?e)?{ ????????????????//?ClassNotFoundException?thrown?if?class?not?found ????????????????//?from?the?non-null?parent?class?loader ????????????} ????????????if?(c?==?null)?{ ????????????????long?t1?=?System.nanoTime(); ????????????????//?如果還是沒(méi)有加載成功,調(diào)用findClass(),讓當(dāng)前類加載器加載 ????????????????c?=?findClass(name); ????????????????//?this?is?the?defining?class?loader;?record?the?stats ????????????????sun.misc.PerfCounter.getParentDelegationTime().addTime(t1?-?t0); ????????????????sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); ????????????????sun.misc.PerfCounter.getFindClasses().increment(); ????????????} ????????} ????????if?(resolve)?{ ????????????resolveClass(c); ????????} ????????return?c; ????} } //?繼承的子類得重寫該方法 protected?Class<?>?findClass(String?name)?throws?ClassNotFoundException?{ ????throw?new?ClassNotFoundException(name); }
loadClass()源碼 展示了,一般加載.class文件大致流程:
- 先去緩存中 檢查是否已經(jīng)加載該類,有就直接返回,避免重復(fù)加載;沒(méi)有就下一步
- 遵循 雙親委派機(jī)制,來(lái)加載.class文件
- 上面兩步都失敗了,調(diào)用findClass()方法,讓當(dāng)前類加載器加載
注意:由于ClassLoader
類是抽象類,而抽象類是無(wú)法通過(guò)new創(chuàng)建對(duì)象的,所以它最核心的findClass()
方法,沒(méi)有具體實(shí)現(xiàn),只拋了一個(gè)異常,而且是protected的,這是應(yīng)用了模板方法模式
,具體的findClass()方法丟給子類實(shí)現(xiàn), 所以繼承的子類得重寫該方法。
自定義類加載器
編寫一個(gè)自定義的類加載器
那我們仿照 ExtClassLoader,AppClassLoader
來(lái)實(shí)現(xiàn)一個(gè)自定義的類加載器,我們同樣是繼承ClassLoader
類
編寫一個(gè)測(cè)試類TestPerson
public?class?TestPerson?{ ????String?name?=?"xiao?ming"; ????public?void?print(){ ????????System.out.println("hello?my?name?is:?"+?name); ????} }
接著 編寫一個(gè)自定義類加載器MyTestClassLoader:
public?class?MyTestClassLoader?extends?ClassLoader??{ ????final?String?classNameSpecify??=?"TestPerson"; ????public?MyTestClassLoader()?{ ????} ????public?MyTestClassLoader(ClassLoader?parent) ????{ ????????super(parent); ????} ????protected?Class<?>?findClass(String?name)?throws?ClassNotFoundException ????{ ????????File?file?=?getClassFile(name); ????????try ????????{ ????????????byte[]?bytes?=?getClassBytes(file); ????????????Class<?>?c?=?this.defineClass(name,?bytes,?0,?bytes.length); ????????????return?c; ????????} ????????catch?(Exception?e) ????????{ ????????????e.printStackTrace(); ????????} ????????return?super.findClass(name); ????} ????private?File?getClassFile(String?name) ????{ ????????File?file?=?new?File("D:\\ideaProjects\\src\\main\\java\\com\\zj\\ideaprojects\\test2\\"+?classNameSpecify+?".class"); ????????return?file; ????} ????private?byte[]?getClassBytes(File?file)?throws?Exception ????{ ????????//?這里要讀入.class的字節(jié),因此要使用字節(jié)流 ????????FileInputStream?fis?=?new?FileInputStream(file); ????????FileChannel?fc?=?fis.getChannel(); ????????ByteArrayOutputStream?baos?=?new?ByteArrayOutputStream(); ????????WritableByteChannel?wbc?=?Channels.newChannel(baos); ????????ByteBuffer?by?=?ByteBuffer.allocate(1024); ????????while?(true) ????????{ ????????????int?i?=?fc.read(by); ????????????if?(i?==?0?||?i?==?-1) ????????????????break; ????????????by.flip(); ????????????wbc.write(by); ????????????by.clear(); ????????} ????????fis.close(); ????????return?baos.toByteArray(); ????} ????//我們這邊要打破雙親委派模型,重寫整個(gè)loadClass方法 ????@Override ????public?Class<?>?loadClass(String?name)?throws?ClassNotFoundException?{ ????????Class<?>?c?=?findLoadedClass(name); ????????if?(c?==?null?&&?name.contains(classNameSpecify)){//指定的類,不走雙親委派機(jī)制,自定義加載 ????????????c?=?findClass(name); ????????????if?(c?!=?null){ ????????????????return?c; ????????????} ????????} ????????return?super.loadClass(name); ????} }
最后在編寫一個(gè)測(cè)試controller:
@RestController public?class?TestClassController?{ ????@RequestMapping(value?=?"testClass",method?=?{RequestMethod.GET,?RequestMethod.POST}) ????public?void?testClassLoader()?throws?ClassNotFoundException,?InstantiationException,?IllegalAccessException,?NoSuchMethodException,?InvocationTargetException?{ ????????MyTestClassLoader?myTestClassLoader?=?new?MyTestClassLoader(); ????????Class<?>?c1?=?Class.forName("com.zj.ideaprojects.test2.TestPerson",?true,?myTestClassLoader); ????????Object?obj?=?c1.newInstance(); ????????System.out.println("當(dāng)前類加載器:"+obj.getClass().getClassLoader()); ????????obj.getClass().getMethod("print").invoke(obj); ????} }
先找到TestPerson所在的目錄, 執(zhí)行命令:javac TestPerson
,生成TestPerson.class
這里沒(méi)有使用idea的build,是因?yàn)槲覀兇a的class讀取路徑 是寫死了的,不走默認(rèn)CLASSPATH
D:\ideaProjects\src\main\java\com\zj\ideaprojects\test2\TestPerson.class
我們?nèi)缓笥胮ostman調(diào)用testClassLoader()測(cè)試接口
結(jié)果:
當(dāng)前類加載器:com.zj.ideaprojects.test2.MyTestClassLoader@1d75e392
hello my name is: xiao ming
然后修改TestPerson,將name 改為 “xiao niu”
public?class?TestPerson?{ ????String?name?=?"xiao?niu"; ????public?void?print(){ ????????System.out.println("hello?my?name?is:?"+?name); ????} }
然后在當(dāng)前目錄 重新編譯, 執(zhí)行命令:javac TestPerson
,會(huì)在當(dāng)前目錄重新生成TestPerson.class 不重啟項(xiàng)目,直接用postman 直接調(diào)這個(gè)測(cè)試接口 結(jié)果:
當(dāng)前類加載器:com.zj.ideaprojects.test2.MyTestClassLoader@7091bd27
hello my name is: xiao niu
這樣就實(shí)現(xiàn)了“熱部署”?。?!
為什么我們這邊要打破雙親委派機(jī)制
如果不打破的話,結(jié)果 當(dāng)前類加載器會(huì)顯示"sun.misc.Launcher$AppClassLoader",原因是由于idea啟動(dòng)項(xiàng)目的時(shí)候會(huì)自動(dòng)幫我們編譯,將class放到 CLASSPATH路徑下。其實(shí)可以把默認(rèn)路徑下的.class刪除也行。這里也是為了展示如何打破雙親委派機(jī)制,才如此實(shí)現(xiàn)的。
官方推薦我們自定義類加載器時(shí),遵循雙親委派機(jī)制。但是凡事得看實(shí)際需求嘛
自定義類加載器時(shí),如何打破雙親委派機(jī)制
通過(guò)上面的例子我們可以看出:
1、如果不想打破雙親委派機(jī)制,我們自定義類加載器,那么只需要重寫findClass方法即可
2、如果想打破雙親委派機(jī)制,我們自定義類加載器,那么還得重寫整個(gè)loadClass方法
SPI機(jī)制 與 線程上下文類加載器
如果你閱讀到這里,你會(huì)發(fā)現(xiàn)雙親委派機(jī)制的各種好處,但萬(wàn)物都不是絕對(duì)正確的,我們需要一分為二地看待問(wèn)題。
在某些場(chǎng)景下雙親委派制過(guò)于局限,所以有時(shí)候必須打破雙親委派機(jī)制來(lái)達(dá)到目的。比如 :SPI機(jī)制、線程上下文類加載器
1.SPI(Service Provider Interface)服務(wù)提供接口。它是jdk內(nèi)置的一種服務(wù)發(fā)現(xiàn)機(jī)制,將裝配的控制權(quán)移到程序之外,在模塊化設(shè)計(jì)中這個(gè)機(jī)制尤其重要,其核心思想就是 讓服務(wù)定義與實(shí)現(xiàn)分離、解耦。
SPI機(jī)制圖
2.線程上下文類加載器(context class loader)是可以破壞Java類加載委托機(jī)制,使程序可以逆向使用類加載器,使得java類加載體系顯得更靈活。
Java 應(yīng)用運(yùn)行的初始線程的上下文類加載器是應(yīng)用類加載器,在線程中運(yùn)行的代碼可以通過(guò)此類加載器來(lái)加載類和資源。Java.lang.Thread中的方法getContextClassLoader()和 setContextClassLoader(ClassLoader cl)
用來(lái)獲取和設(shè)置線程的上下文類加載器。如果沒(méi)有通過(guò) setContextClassLoader(ClassLoader cl)
方法進(jìn)行設(shè)置的話,線程將繼承其父線程的上下文類加載器。
SPI機(jī)制在框架的設(shè)計(jì)上應(yīng)用廣泛,下面舉幾個(gè)常用的例子:
JDBC
平時(shí)獲取jdbc,我們可以這樣:Connection connection =DriverManager.getConnection("jdbc://localhost:3306");
我們讀DriverManager
的源碼發(fā)現(xiàn):其實(shí)就是查詢classPath下,所有META-INF下給定Class名的文件,并將其內(nèi)容返回,使用迭代器遍歷,這里遍歷的內(nèi)部使用Class.forName
加載了類。
其中有一處非常重要 ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
我們看下它的實(shí)現(xiàn):
????public?static?<S>?ServiceLoader<S>?load(Class<S>?service)?{ ????????ClassLoader?cl?=?Thread.currentThread().getContextClassLoader();//important?! ????????return?ServiceLoader.load(service,?cl); ????}
我們可以看出JDBC,DriverManager
類和ServiceLoader
類都是屬于核心庫(kù) rt.jar
的,它們的類加載器是Bootstrap ClassLoader類加載器。而具體的數(shù)據(jù)庫(kù)驅(qū)動(dòng)相關(guān)功能卻是第三方提供的,第三方的類不能被引導(dǎo)類加載器(Bootstrap ClassLoader)加載。
所以java.util.ServiceLoader類進(jìn)行動(dòng)態(tài)裝載時(shí),使用了線程的上下文類加載器(ThreadContextClassLoader)讓父級(jí)類加載器能通過(guò)調(diào)用子級(jí)類加載器來(lái)加載類,這打破了雙親委派機(jī)制。
Tomcat
Tomcat是web容器,我們把war包放到 tomcat 的webapp目錄下,這意味著一個(gè)tomcat可以部署多個(gè)應(yīng)用程序。
不同的應(yīng)用程序可能會(huì)依賴同一個(gè)第三方類庫(kù)的不同版本,但是不同版本的類庫(kù)中某一個(gè)類的全路徑名可能是一樣的。防止出現(xiàn)一個(gè)應(yīng)用中加載的類庫(kù)會(huì)影響另一個(gè)應(yīng)用的情況。如果采用默認(rèn)的雙親委派類加載機(jī)制,那么是無(wú)法加載多個(gè)相同的類。
Tomcat類加載器種類
- 如果Tomcat本身的依賴和Web應(yīng)用還需要共享,Common類加載器(CommonClassLoader)來(lái)裝載實(shí)現(xiàn)共享
- Catalina類加載器(CatalinaClassLoader) 用來(lái) 隔絕Web應(yīng)用程序與Tomcat本身的類
- Shared類加載器(SharedClassLoader):如果WebAppClassLoader自身沒(méi)有加載到某個(gè)類,那就委托SharedClassLoader去加載
- WebAppClassLoader:為了實(shí)現(xiàn)隔離性,優(yōu)先加載 Web 應(yīng)用自己定義的類,所以沒(méi)有遵照雙親委派的約定,每一個(gè)應(yīng)用自己的類加載器WebAppClassLoader(多個(gè)應(yīng)用程序,就有多個(gè)WebAppClassLoader),負(fù)責(zé)
優(yōu)先加載
本身的目錄下的class文件,加載不到時(shí)再交給CommonClassLoader
以及上層的ClassLoader
進(jìn)行加載,這破壞了雙親委派機(jī)制。 - Jsp類加載器(JasperLoader):實(shí)現(xiàn)熱部署的功能,修改文件不用重啟就自動(dòng)重新裝載類庫(kù)。
JasperLoader
的加載范圍僅僅是這個(gè)JSP文件所編譯出來(lái)的那一個(gè).Class文件,它出現(xiàn)的目的就是為了被丟棄:當(dāng)Web容器檢測(cè)到JSP文件被修改時(shí),會(huì)替換掉目前的JasperLoader
的實(shí)例,并通過(guò)再建立一個(gè)新的Jsp類加載器來(lái)實(shí)現(xiàn)JSP文件的HotSwap
功能。
我們來(lái)模擬一下tomcat 多個(gè)版本代碼共存:
這邊的例子換了個(gè)電腦,所以目錄結(jié)構(gòu)、路徑與上面的例子有點(diǎn)變化
我們先編寫 App類
public?class?App?{ ????String?name?=?"webapp?1"; ????public?void?print()?{ ????????System.out.println("this?is?"+?name); ????} }
javac App生成的App.class 放入 tomcatTest\war1\com\zj\demotest\tomcatTest
目錄下
將name改為webapp 2
,重新生成的App.class
放入 tomcatTest\war2\com\zj\demotest\tomcatTest
目錄下
然后我們編寫類加載器:
public?class?MyTomcatClassloader?extends?ClassLoader?{ ????private?String?classPath; ????public?MyTomcatClassloader(String?classPath)?{ ????????this.classPath?=?classPath; ????} ????@Override ????protected?Class<?>?findClass(String?name)?throws?ClassNotFoundException ????{ ????????File?file?=?getClassFile(name); ????????try ????????{ ????????????byte[]?bytes?=?getClassBytes(file); ????????????Class<?>?c?=?this.defineClass(name,?bytes,?0,?bytes.length); ????????????return?c; ????????} ????????catch?(Exception?e) ????????{ ????????????e.printStackTrace(); ????????} ????????return?super.findClass(name); ????} ????private?File?getClassFile(String?name) ????{ ????????name?=?name.replaceAll("\\.",?"/"); ????????File?file?=?new?File(classPath+?"/"+?name?+?".class");//拼接路徑,找到class文件 ????????return?file; ????} ????private?byte[]?getClassBytes(File?file)?throws?Exception ????{ ????????//?這里要讀入.class的字節(jié),因此要使用字節(jié)流 ????????FileInputStream?fis?=?new?FileInputStream(file); ????????FileChannel?fc?=?fis.getChannel(); ????????ByteArrayOutputStream?baos?=?new?ByteArrayOutputStream(); ????????WritableByteChannel?wbc?=?Channels.newChannel(baos); ????????ByteBuffer?by?=?ByteBuffer.allocate(1024); ????????while?(true) ????????{ ????????????int?i?=?fc.read(by); ????????????if?(i?==?0?||?i?==?-1)?{ ????????????????break; ????????????} ????????????by.flip(); ????????????wbc.write(by); ????????????by.clear(); ????????} ????????fis.close(); ????????return?baos.toByteArray(); ????} ????//我們這邊要打破雙親委派模型,重寫整個(gè)loadClass方法 ????@Override ????public?Class<?>?loadClass(String?name)?throws?ClassNotFoundException?{ ????????Class<?>?c?=?findLoadedClass(name); ????????if?(c?==?null?&&?name.contains("tomcatTest")){//指定的目錄下的類,不走雙親委派機(jī)制,自定義加載 ????????????c?=?findClass(name); ????????????if?(c?!=?null){ ????????????????return?c; ????????????} ????????} ????????return?super.loadClass(name); ????} }
最后編寫測(cè)試controller:
@RestController public?class?TestController?{ ????@RequestMapping(value?=?"/testTomcat",method?=?{RequestMethod.GET,?RequestMethod.POST}) ????public?void?testclass()?throws?ClassNotFoundException,?IllegalAccessException,?InstantiationException,?NoSuchMethodException,?InvocationTargetException?{ ????????MyTomcatClassloader?myTomcatClassloader?=?new?MyTomcatClassloader("D:\\GiteeProjects\\study-java\\demo-test\\src\\main\\java\\com\\zj\\demotest\\tomcatTest\\war1"); ????????Class?cl?=?myTomcatClassloader.loadClass("com.zj.demotest.tomcatTest.App"); ????????Object?obj?=?cl.newInstance(); ????????System.out.println("當(dāng)前類加載器:"+obj.getClass().getClassLoader()); ????????obj.getClass().getMethod("print").invoke(obj); ????????MyTomcatClassloader?myTomcatClassloader22?=?new?MyTomcatClassloader("D:\\GiteeProjects\\study-java\\demo-test\\src\\main\\java\\com\\zj\\demotest\\tomcatTest\\war2"); ????????Class?cl22?=?myTomcatClassloader22.loadClass("com.zj.demotest.tomcatTest.App"); ????????Object?obj22?=?cl22.newInstance(); ????????System.out.println("當(dāng)前類加載器:"+obj22.getClass().getClassLoader()); ????????obj22.getClass().getMethod("print").invoke(obj22); ????} }
然后postman 調(diào)一下這個(gè)接口, 結(jié)果:
當(dāng)前類加載器:com.zj.demotest.tomcatTest.MyTomcatClassloader@18fbb876
this is webapp 1
當(dāng)前類加載器:com.zj.demotest.tomcatTest.MyTomcatClassloader@5f7ed4a9
this is webapp 2
我們發(fā)現(xiàn)2個(gè)同樣的類能共存在同一個(gè)JVM中,互不影響。
注意:同一個(gè)JVM內(nèi),2個(gè)相同的包名和類名的對(duì)象是可以共存的,前提是他們的類加載器不一樣。所以我們要判斷多個(gè)類對(duì)象是否是同一個(gè),除了要看包名和類名相同,還得注意他們的類加載器是否一致
SpringBoot Starter
springboot自動(dòng)配置的原因是因?yàn)槭褂昧?code>@EnableAutoConfiguration注解。
當(dāng)程序包含了EnableAutoConfiguration
注解,那么就會(huì)執(zhí)行下面的方法,然后會(huì)加載所有spring.factories
文件,將其內(nèi)容封裝成一個(gè)map,spring.factories
其實(shí)就是一個(gè)名字特殊的properties文件。
在spring-boot應(yīng)用啟動(dòng)時(shí),會(huì)調(diào)用loadFactoryNames方法,其中傳遞的一個(gè)參數(shù)就是:org.springframework.boot.autoconfigure.EnableAutoConfiguration
????protected?List<String>?getCandidateConfigurations(AnnotationMetadata?metadata,?AnnotationAttributes?attributes)?{ ????????List<String>?configurations?=?SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(),?this.getBeanClassLoader()); ????????Assert.notEmpty(configurations,?"No?auto?configuration?classes?found?in?META-INF/spring.factories.?If?you?are?using?a?custom?packaging,?make?sure?that?file?is?correct."); ????????return?configurations; ????}
META-INF/spring.factories會(huì)被讀取到。
它還使用了this.getBeanClassLoader() 獲取類加載器。所以我們立刻明白了文章一開始的例子,SpringBoot項(xiàng)目直接build項(xiàng)目,不重啟項(xiàng)目,就能實(shí)現(xiàn)熱部署效果。
尾語(yǔ)
類加載器是 Java 語(yǔ)言的一個(gè)創(chuàng)新,它使得動(dòng)態(tài)安裝和更新軟件組件成為可能。同時(shí)我們應(yīng)該了解雙親委派機(jī)制的優(yōu)缺點(diǎn)和應(yīng)用場(chǎng)景,這些可能比較難但對(duì)于我們來(lái)說(shuō)卻很重要。
到此這篇關(guān)于詳解Java類加載器與雙親委派機(jī)制的文章就介紹到這了,更多相關(guān)Java類加載器 雙親委派機(jī)制內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
解決執(zhí)行maven命令時(shí)提示Process terminated的問(wèn)題
這篇文章主要介紹了解決執(zhí)行maven命令時(shí)提示Process terminated的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-09-09Java基本數(shù)據(jù)類型與封裝類型詳解(int和Integer區(qū)別)
這篇文章主要介紹了Java基本數(shù)據(jù)類型與封裝類型詳解(int和Integer區(qū)別) ,需要的朋友可以參考下2017-02-02SpringBoot+Swagger-ui自動(dòng)生成API文檔
今天小編就為大家分享一篇關(guān)于SpringBoot+Swagger-ui自動(dòng)生成API文檔,小編覺(jué)得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來(lái)看看吧2019-03-03java中利用List的subList方法實(shí)現(xiàn)對(duì)List分頁(yè)(簡(jiǎn)單易學(xué))
本篇文章主要介紹了java中l(wèi)ist數(shù)據(jù)拆分為sublist實(shí)現(xiàn)頁(yè)面分頁(yè)的簡(jiǎn)單代碼,具有一定的參考價(jià)值,有需要的可以了解一下。2016-11-11springBoot使用JdbcTemplate代碼實(shí)例
這篇文章主要介紹了springBoot使用JdbcTemplate代碼實(shí)例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-09-09