JVM雙親委派模型知識(shí)詳細(xì)總結(jié)
一、簡(jiǎn)介
除了頂層的啟動(dòng)類(lèi)加載器(Bootstrap ClassLoader)外,其余的類(lèi)加載器都應(yīng)當(dāng)有自己的上層加載器,如果一個(gè)類(lèi)加載器收到了類(lèi)加載請(qǐng)求,它并不會(huì)自己先去加載,而是把這個(gè)請(qǐng)求委托給上層的加載器,如果上層類(lèi)加載器還存在其上層類(lèi)加載器,則進(jìn)一步向上委托,依次遞歸,直到請(qǐng)求最終到達(dá)頂層的啟動(dòng)類(lèi)加載器,從頂層類(lèi)加載器開(kāi)始,如果類(lèi)加載器根據(jù)類(lèi)的全限定名查詢(xún)到已經(jīng)加載過(guò)這個(gè)類(lèi),就成功返回加載過(guò)的此類(lèi)信息,倘若加載器未加載過(guò)此類(lèi),則原路返回給下層加載器繼續(xù)重復(fù)此過(guò)程,直到最先加載此類(lèi)的加載器所有上層加載器都未加載過(guò)此類(lèi)后,此類(lèi)加載器才會(huì)嘗試自己去加載,這便是雙親委派模式。

舉個(gè)栗子:
假如你是某個(gè)企業(yè)員工,你寫(xiě)了一份方案希望得到執(zhí)行,首先你得拿給你的經(jīng)理去審批吧,經(jīng)理說(shuō)這個(gè)事情他做不了主,于是經(jīng)理就拿給總經(jīng)理看,總經(jīng)理也做不了主,就給董事長(zhǎng)看,然后董事長(zhǎng)看了看,也看不明白于是讓總經(jīng)理自己拿主意吧,總經(jīng)理仔細(xì)一看,這個(gè)方案之前公司已經(jīng)做過(guò)了,于是讓經(jīng)理去告訴那個(gè)員工不用做了,直接用那個(gè)做過(guò)的方案吧。
二、雙親委派的意義
采用雙親委派模式的是好處是Java類(lèi)隨著它的類(lèi)加載器一起具備了一種帶有優(yōu)先級(jí)的層次關(guān)系,通過(guò)這種層級(jí)關(guān)系可以避免類(lèi)的重復(fù)加載,當(dāng)上層類(lèi)加載器已經(jīng)加載了該類(lèi)時(shí),就沒(méi)有必要下層的ClassLoader再加載一次。
其次是考慮到安全因素,jdk中定義的類(lèi)不會(huì)被隨意替換,假設(shè)我們?cè)赾lasspath路徑下自定義一個(gè)名為java.lang.Integer的類(lèi),通過(guò)雙親委托模式傳遞到啟動(dòng)類(lèi)加載器,而啟動(dòng)類(lèi)加載器通過(guò)索引發(fā)現(xiàn)同全限定名的類(lèi)已被加載,并不會(huì)重新加載網(wǎng)絡(luò)傳遞的過(guò)來(lái)的java.lang.Integer,而直接返回已加載過(guò)的Integer.class,那么你所定義的類(lèi)就不會(huì)被加載,這樣便可以防止核心API庫(kù)被隨意篡改。
如:
package java.lang;
public class Integer {
public void print(){
System.out.println("this is Integer.");
}
public static void main(String[] args) {
new Integer().print();
}
}
執(zhí)行main方法后輸出如下:

三、JVM提供的類(lèi)加載器
- 啟動(dòng)類(lèi)加載器(BootstrapClassLoader):由jvm負(fù)責(zé)管理,主要負(fù)責(zé)加載核心的類(lèi)庫(kù)(**rt.jar,也就是java.lang.***等),并構(gòu)造和啟動(dòng)ExtClassLoader和APPClassLoader。
- 擴(kuò)展類(lèi)加載器(ExtClassLoader):主要負(fù)責(zé)加載jre/lib/ext**目錄下的一些擴(kuò)展的jar。
- 應(yīng)用類(lèi)加載器(AppClassLoader):主要負(fù)責(zé)加載classpath下jar包和應(yīng)用程序的主函數(shù)類(lèi)。
如果當(dāng)前類(lèi)加載器加載的類(lèi)引用了其它類(lèi),那么也會(huì)通過(guò)遞歸的方式先對(duì)其所有引用進(jìn)行加載。
四、執(zhí)行類(lèi)加載的五種方式
認(rèn)識(shí)了這三種類(lèi)加載器,接下來(lái)我們看看類(lèi)加載的五種方式。
1.通過(guò)命令行使用java命令啟動(dòng)應(yīng)用時(shí)由JVM初始化加載含有main()方法的主類(lèi)。
2.使用new關(guān)鍵字初始化一個(gè)對(duì)象時(shí)
3.通過(guò)類(lèi)對(duì)應(yīng)的Class對(duì)象的newInstance()方法
4.通過(guò)Class.forName(name)方法動(dòng)態(tài)加載
5.通過(guò)ClassLoader.loadClass(name)方法動(dòng)態(tài)加載
五、自定義類(lèi)加載器
java系統(tǒng)為我們提供的三種類(lèi)加載器,還給出了他們的層次關(guān)系圖,最下面就是自定義類(lèi)加載器,那么我們?nèi)绾巫约憾x類(lèi)加載器呢?這主要有兩種方式
(1)遵守雙親委派模型:繼承ClassLoader,重寫(xiě)findClass方法。
通常我們推薦采用此方式自定義類(lèi)加載器,最大程度上的遵守雙親委派模型。
findClass(name)查找具有指定全限定名的類(lèi)。此方法用于遵循加載類(lèi)的委托模型的類(lèi)裝入器實(shí)現(xiàn)重寫(xiě),并將在檢查請(qǐng)求類(lèi)的父類(lèi)裝入器之后由loadClass方法調(diào)用。如果該類(lèi)沒(méi)有被加載過(guò)且其class文件讀取不到則拋出ClassNotFoundException異常。
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
其實(shí)現(xiàn)例子:
package com.aliencat.javabase.classloader;
public class MyClassLoader extends ClassLoader{
protected Class<?> findClass(String name) throws ClassNotFoundException {
switch (name){
case "java.lang.Integer":return Double.class;
case "com.aliencat.javabase.classloader.LoaderDemo" : return loadClassFromDisk(name);
}
throw new ClassNotFoundException(name);
}
//從calsspath下加載類(lèi)的字節(jié)碼文件
public byte[] loadClassFromDisk(String name) {
String classPathRoot = Thread.currentThread().getContextClassLoader().getResource("").getPath();
classPathRoot = classPathRoot.substring(1);
String filePath = classPathRoot + name.replace(".","/") + ".class";
try(InputStream in = new FileInputStream(filePath) ;
ByteOutputStream stream = new ByteOutputStream()) {
byte[] buff = new byte[1024];
for(int num = 0; (num=in.read(buff)) != -1;){
stream.write(buff,0,num);
}
return stream.toByteArray();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
public static void main(String[] args) throws ClassNotFoundException {
Class clzz = new MyClassLoader().loadClass("com.aliencat.javabase.classloader.LoaderDemo");
System.out.println(clzz);
clzz = new MyClassLoader().loadClass("java.lang.Integer");
System.out.println(clzz);
clzz = new MyClassLoader().loadClass("java.lang.String");
System.out.println(clzz);
clzz = new MyClassLoader().loadClass("java.lang.xxxxx");
System.out.println(clzz);
}
}
輸出如下:

(2)破壞雙親委派模型:繼承ClassLoader,重寫(xiě)loadClass方法。
我們先來(lái)看下loadClass方法的源碼是怎樣的:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先,在當(dāng)前加載器加載過(guò)的類(lèi)中檢查這個(gè)類(lèi)有沒(méi)有被加載過(guò)
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
//存在上層類(lèi)加載器,則讓上層取執(zhí)行l(wèi)oadClass方法
c = parent.loadClass(name, false);
} else {
//讓BootstrapClass類(lèi)加載器去查找該類(lèi)
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 上層類(lèi)加載器拋出了ClassNotFoundException 則不進(jìn)行處理
}
if (c == null) {
long t1 = System.nanoTime();
// 如果上層類(lèi)加載器都沒(méi)有找到
// 那么這個(gè)類(lèi)加載器自己去找
// 如果找到了,則將resolve置為true
c = findClass(name);
// 這是定義類(lèi)加載器;記錄統(tǒng)計(jì)數(shù)據(jù)
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
//解析此類(lèi)的信息
resolveClass(c);
}
return c;
}
}
示例如下:
public class MyClassLoader extends ClassLoader {
public static void main(String[] args) throws ClassNotFoundException {
Class clzz = new MyClassLoader().loadClass("com.aliencat.javabase.classloader.ClassTest");
System.out.println(clzz);
clzz = new MyClassLoader().loadClass("java.lang.Double");
System.out.println(clzz);
clzz = new MyClassLoader().loadClass("java.lang.String");
System.out.println(clzz);
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
switch (name) {
case "java.lang.Double":
return Integer.class;
case "java.lang.Object": //如果去掉此項(xiàng),則破壞雙親委任的情況下會(huì)報(bào)找不到Object的NoClassDefFoundError異常
return Object.class;
case "com.aliencat.javabase.classloader.ClassTest":
byte[] bytes = loadClassFromDisk(name);
if(bytes != null){
return defineClass(name,bytes,0,bytes.length);
}else {
return null;
}
}
throw new ClassNotFoundException(name);
}
//從calsspath下加載類(lèi)的字節(jié)碼文件
public byte[] loadClassFromDisk(String name) {
String classPathRoot = Thread.currentThread().getContextClassLoader().getResource("").getPath();
classPathRoot = classPathRoot.substring(1);
String filePath = classPathRoot + name.replace(".","/") + ".class";
try(InputStream in = new FileInputStream(filePath) ;
ByteOutputStream stream = new ByteOutputStream()) {
byte[] buff = new byte[1024];
for(int num = 0; (num=in.read(buff)) != -1;){
stream.write(buff,0,num);
}
return stream.toByteArray();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
synchronized (LoaderDemo.class) {
// 首先,在當(dāng)前加載器加載過(guò)的類(lèi)中檢查這個(gè)類(lèi)有沒(méi)有被加載過(guò)
Class<?> c = findLoadedClass(name);
if (c == null) {
//沒(méi)加載過(guò)的話(huà)就去磁盤(pán)對(duì)應(yīng)路徑下去找
c = findClass(name);
}
return c;
}
}
}
class ClassTest{
}
輸出如下:

所以破壞雙親委托的方法簡(jiǎn)單來(lái)說(shuō)就是通過(guò)繼承ClassLoader重寫(xiě)loadClass方法,去掉其中委托給上級(jí)加載類(lèi)的相關(guān)邏輯然后實(shí)現(xiàn)自定義的加載類(lèi)的findClass邏輯。(另外你可以試試把ClassTest替換String的類(lèi)名是什么效果哦,我就不演示了)
六、總結(jié)
Q:前面說(shuō)了一堆雙親委托的好處,那么為什么要破壞雙親委托呢?
A:因?yàn)樵谀承┣闆r下父類(lèi)加載器需要委托子類(lèi)加載器去加載class文件。受到加載范圍的限制,父類(lèi)加載器無(wú)法加載到需要的文件,以JDBC接口為例,由于JDBC接口定義在jdk當(dāng)中的,而其實(shí)現(xiàn)由各個(gè)數(shù)據(jù)庫(kù)的服務(wù)商來(lái)提供,比如mysql的就寫(xiě)了MySQL-Connector,那么問(wèn)題就來(lái)了,JDBC接口由啟動(dòng)類(lèi)加載器加載,而JDBC的實(shí)現(xiàn)是由服務(wù)商提供的,并不在啟動(dòng)類(lèi)加載器的加載目錄下,在不破壞雙親委派的情況下,啟動(dòng)類(lèi)加載器下層的類(lèi)加載器要加載JDBC則必然會(huì)返回啟動(dòng)類(lèi)加載器中已經(jīng)加載過(guò)的接口,那么服務(wù)商提供的JDBC就不會(huì)被加載,所以需要自定義類(lèi)加載器破壞雙親委派拿到服務(wù)商實(shí)現(xiàn)的JDBC接口,這里僅僅是舉了破壞雙親委派的其中一個(gè)情況,類(lèi)似的還有tomcat。
到此這篇關(guān)于JVM雙親委派模型知識(shí)詳細(xì)總結(jié)的文章就介紹到這了,更多相關(guān)JVM雙親委派模型內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java多線(xiàn)程通過(guò)CompletableFuture組裝異步計(jì)算單元
這篇文章主要為大家介紹了java多線(xiàn)程通過(guò)CompletableFuture組裝異步計(jì)算單元,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-04-04
Java 超詳細(xì)講解ThreadLocal類(lèi)的使用
寫(xiě)SpringBoot項(xiàng)目的時(shí)候,經(jīng)常用到的一個(gè)保存用戶(hù)信息的類(lèi)就是Threadlocal,我們今天就來(lái)詳細(xì)介紹一下這個(gè)類(lèi),感興趣的朋友來(lái)看看吧2022-04-04
Java數(shù)據(jù)結(jié)構(gòu)順序表用法詳解
順序表是計(jì)算機(jī)內(nèi)存中以數(shù)組的形式保存的線(xiàn)性表,線(xiàn)性表的順序存儲(chǔ)是指用一組地址連續(xù)的存儲(chǔ)單元依次存儲(chǔ)線(xiàn)性表中的各個(gè)元素、使得線(xiàn)性表中在邏輯結(jié)構(gòu)上相鄰的數(shù)據(jù)元素存儲(chǔ)在相鄰的物理存儲(chǔ)單元中,即通過(guò)數(shù)據(jù)元素物理存儲(chǔ)的相鄰關(guān)系來(lái)反映數(shù)據(jù)元素之間邏輯上的相鄰關(guān)系2021-10-10
Java 實(shí)現(xiàn)RSA非對(duì)稱(chēng)加密算法
RSA解決了對(duì)稱(chēng)加密的一個(gè)不足,比如AES算法加密和解密時(shí)使用的是同一個(gè)秘鑰,因此這個(gè)秘鑰不能公開(kāi),因此對(duì)于需要公開(kāi)秘鑰的場(chǎng)合,我們需要在加密和解密過(guò)程中使用不同的秘鑰,加密使用的公鑰可以公開(kāi),解密使用的私鑰要保密,這就是非對(duì)稱(chēng)加密的好處?!?/div> 2021-06-06
Spring Boot + Vue 前后端分離開(kāi)發(fā)之前端網(wǎng)絡(luò)請(qǐng)求封裝與配置
這篇文章主要介紹了Spring Boot + Vue 前后端分離開(kāi)發(fā)之前端網(wǎng)絡(luò)請(qǐng)求封裝與配置方法,本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值 ,需要的朋友可以參考下2019-05-05
SpringMVC接收前臺(tái)傳遞過(guò)來(lái)的值的實(shí)例
下面小編就為大家分享一篇SpringMVC接收前臺(tái)傳遞過(guò)來(lái)的值的實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-03-03
如何擴(kuò)展Spring Cache實(shí)現(xiàn)支持多級(jí)緩存
這篇文章主要介紹了如何擴(kuò)展Spring Cache實(shí)現(xiàn)支持多級(jí)緩存,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-11-11
學(xué)習(xí)Java之如何正確地跳出循環(huán)結(jié)構(gòu)
我們?cè)诶醚h(huán)執(zhí)行重復(fù)操作的過(guò)程中,存在著一個(gè)需求:如何中止,或者說(shuō)提前結(jié)束一個(gè)循環(huán),所以就給大家講解一下,如何在java代碼中返回一個(gè)結(jié)果,如何結(jié)束和跳出一個(gè)循環(huán),需要的朋友可以參考下2023-05-05
Java結(jié)構(gòu)型設(shè)計(jì)模式之組合模式詳解
組合模式,又叫部分整體模式,它創(chuàng)建了對(duì)象組的數(shù)據(jù)結(jié)構(gòu)組合模式使得用戶(hù)對(duì)單個(gè)對(duì)象和組合對(duì)象的訪(fǎng)問(wèn)具有一致性。本文將通過(guò)示例為大家詳細(xì)介紹一下組合模式,需要的可以參考一下2022-09-09最新評(píng)論

