java中類(lèi)加載與雙親委派機(jī)制詳解
類(lèi)加載是什么
把磁盤(pán)中的java文件加載到內(nèi)存中的過(guò)程叫做類(lèi)加載
當(dāng)我們用java命令運(yùn)行某個(gè)類(lèi)的main函數(shù)啟動(dòng)程序時(shí),首先需要通過(guò)類(lèi)加載器把主類(lèi)加載到JVM. 有如下 User 類(lèi)
package dc.dccmmtop; public Class User { public static void main(String[] args) { System.out.println("hello"); } }
運(yùn)行 java dc.dccmmtop.User
時(shí), 先要找到 User.Class 文件,查找全路徑就是 Class_PATH + {{package name}},對(duì)于User類(lèi)來(lái)說(shuō),就是 {$Class_APTH}/dc/dccmmtop.User.Class
假如 User.java
在F:\code
, 并且不在Class_PATH 下,可以通過(guò) java -Classpath "F:\code"
臨時(shí)指定。
加載類(lèi)之后還有后續(xù)的步驟:
- 驗(yàn)證
- 準(zhǔn)備
- 解析
- 初始化
- 使用
- 卸載
這篇文章主要來(lái)講講類(lèi)加載
類(lèi)加載器
不了解類(lèi)加載機(jī)制的,可能就認(rèn)為,只需找到j(luò)ava文件所在的磁盤(pán)位置,然后進(jìn)行一次讀文件的操作不就完成了加載嘛,其實(shí)遠(yuǎn)非如此。
總有一個(gè)加載類(lèi)的工具,這個(gè)工具叫做類(lèi)加載器,在java代碼中可以通過(guò)如下方式獲取當(dāng)前類(lèi)的類(lèi)加載器是什么
package dccmmtop; public Class User { public static void main(String[] args) { System.out.println("hello"); System.out.println(User.Class.getClassLoader()); } }
如圖可以看到類(lèi)加載器的名字叫做 AppClassLoader
我們?nèi)炙阉饕幌逻@個(gè)類(lèi),會(huì)發(fā)現(xiàn)在 sun.misc.Launcher.java
文件中找到。
那么這個(gè)AppClassLoader
本身也是一個(gè) java 文件,它又是什么時(shí)候被加載并初始化的呢?
我們滾動(dòng)到文件頂部,看到 Launcher 類(lèi)的構(gòu)造方法部分:
標(biāo)記1 和標(biāo)記2 實(shí)現(xiàn)了一個(gè)單例模式,在5 處獲取到了 AppClassLoader
實(shí)例。也就是說(shuō)在某一個(gè)地方通過(guò)調(diào)用 Launcher 類(lèi)中的 getLauncher()
方法,會(huì)得到 AppClassLoader
實(shí)例, 那么 getLauncher()
方法又是在哪里調(diào)用的呢?追蹤到這里已經(jīng)無(wú)法在java代碼中找到上一步了,其實(shí)這個(gè)方法是jvm (c++實(shí)現(xiàn))調(diào)用的,如下圖:
以上就是類(lèi)加載的主要步驟了。下面看一下雙親委派機(jī)制
雙親委派機(jī)制
我們繼續(xù)看AppClassLoader
實(shí)例化的過(guò)程:
在5處,實(shí)例化了一個(gè)AppClassLoader
的對(duì)象,同時(shí)傳進(jìn)去了一個(gè)參數(shù) var1
, 這個(gè) var1 是另外一個(gè)類(lèi)加載器ExtClassLoader
, 我們?cè)谶M(jìn)入 getAppClassLoader
方法看一看是怎么實(shí)現(xiàn)的:
先看一下 幾個(gè)ClassLoad的繼承關(guān)系:
有上面的繼承關(guān)系圖可以看出來(lái),AppClassLoader
和 ExtClassLoader
都是從 ClassLoader
繼承來(lái)的。
在 Launcher()
中可知,調(diào)用 AppClassLoader.getAppClassLoader()
方法時(shí), 把 ExtClassLoader
的實(shí)例作為參數(shù)傳遞進(jìn)來(lái),最終到4這一步,作為 var2 參數(shù),調(diào)用父類(lèi)的構(gòu)造方法,繼續(xù)追蹤父類(lèi)的構(gòu)造方法直到 ClassLoader
:
在 ClassLoader
構(gòu)造方法中,維護(hù)了一個(gè) parent 變量,到此我們知道了 AppClassLoader
中 parent 變量保存的是 ExtClassLoader
的實(shí)例, 如下圖表示
繼續(xù)看Launcher 構(gòu)造方法:
loadClass()
方法將 Class 文件加載到j(luò)vm中,我們跟蹤一下這個(gè)方法,會(huì)發(fā)現(xiàn)最后會(huì)調(diào)到 根類(lèi)ClassLoader
中:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the Class has already been loaded Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if Class not found // from the non-null parent Class loader } if (c == null) { // If still not found, then invoke findClass in order // to find the Class. long t1 = System.nanoTime(); 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; } }
上面代碼塊中的弟6行,findLoadedClass()
, 先從已加載到的類(lèi)集合中查找有沒(méi)有這個(gè)類(lèi),如果有的話,直接返回,沒(méi)有再進(jìn)行下一步, findLoadedClass
方法源碼如下:
到 native finnal Class<?> findLoadedClass0(String name);
這里已經(jīng)無(wú)法在向后追蹤了,看到 naive
,要明白 使用native關(guān)鍵字說(shuō)明這個(gè)方法是原生函數(shù),也就是這個(gè)方法是用C/C++語(yǔ)言實(shí)現(xiàn)的,并且被編譯成了DLL,由java去調(diào)用.
此時(shí) User.Class 是第一次加載,AppClassLoader
中肯定無(wú)法在已加載的集合中找到,所以繼續(xù)向下走到第 10,11 行. 上面已經(jīng)分析過(guò),AppClassLoader
中的 parent 是 ExtClassLoader
, 所以在11行由 ExtClassLoader
的實(shí)例執(zhí)行 laodClass
方法。 ExtClassLoader
沒(méi)有覆寫(xiě)根類(lèi)ClassLoader
的loaderClass
方法,所以也會(huì)到這里,只不過(guò) ExtClassLoader
的 parent 是 NUll, 會(huì)走到13行,調(diào)用findBootstrapClassOrNull()
方法,再看一下這個(gè)方法的實(shí)現(xiàn):
會(huì)發(fā)現(xiàn)這個(gè)方法也是C++實(shí)現(xiàn)的,雖然我們無(wú)法看到源碼,但是根據(jù)注釋可以知道,這個(gè)是保存了啟動(dòng)類(lèi)加載器加載過(guò)的類(lèi)。
到此為止,我們已經(jīng)見(jiàn)識(shí)過(guò)3中不同的類(lèi)加載器了:
- AppClassLoader
- ExtClassLoader
- BootStrapClassLoader
我們先不管這個(gè)后面兩個(gè)類(lèi)加載器是什么, 假定他們也找不到 User.Class. 繼續(xù)向下看:
執(zhí)行到第21行findClas()
這里,再看源碼
在A-2 這一步,ucp
其實(shí)保存的就是當(dāng)前 ClassLoader 的類(lèi)加載路徑,就不再展開(kāi)。要記住此時(shí)的 ClassLoader 是 ExtClassLoader
, 假如仍然找不到User.Class 會(huì)執(zhí)行到 A-3.然后返回到 loadClass 方法中, 此時(shí) c 是空,繼續(xù)執(zhí)行到33行,返回到 AppClassLoader
調(diào)用 parent.getAppClassLoader
處,在 AppClassLoader
實(shí)例的范圍下繼續(xù)向后執(zhí)行,然后再繼續(xù)調(diào)用 findClass
方法,如果在AppClassLoader
的類(lèi)加載路徑中找到User.Class 文件,就會(huì) 執(zhí)行 defindClass(name,res)
方法去加載類(lèi)文件了。
整個(gè)過(guò)程用文字描述起來(lái)比較復(fù)雜,來(lái)張圖就很清楚了,為什么叫做雙親委派:
把 loadedClassList 集合稱(chēng)作緩存:
- 先在 AppClassLoader 中緩存中找,如果找不到向 ExtClassLoader 找,如果能找到,直接返回
- 在 ExtClassLoader 中緩存找,如果找不到向 BootStrapClassLoader 找,如果能找到,直接返回
- 在 BootStrapClassLoader 找,如果找不到, 在 ExtClassLoader 類(lèi)路徑集合中找,
- 如果在 ExtClassLoader 類(lèi)路徑集合找不到,在 AppClassLoader 類(lèi)路徑集合找
- 如果在 AppClassLoader 類(lèi)路徑集合中能找到,加載該類(lèi),并放入緩存。找不到則報(bào)錯(cuò)
雙親指的是 ExtClassLoader
和 BootStrapClassLoader
, AppClassLoader 先不加載,而是向上讓其“父”加載,父加載不到時(shí),自己再加載。這里的父不是父類(lèi),而是調(diào)用層級(jí)的關(guān)系。
是時(shí)候介紹一下 這三個(gè)類(lèi)加載器
BootStrapClassLoader
引導(dǎo)類(lèi)加載器
負(fù)責(zé)加載支撐JVM運(yùn)行的位于JRE的lib目錄下的核心類(lèi)庫(kù),比如 rt.jar、charsets.jar等
ExtClassLoader
擴(kuò)展類(lèi)加載器
負(fù)責(zé)加載支撐JVM運(yùn)行的位于JRE的lib目錄下的ext擴(kuò)展目錄中的JAR 類(lèi)包
AppClassLoader
應(yīng)用程序加載器
負(fù)責(zé)加載ClassPath路徑下的類(lèi)包,主要就是加載你自己寫(xiě)的那些類(lèi)
我們可以寫(xiě)代碼驗(yàn)證一下:
package dccmmtop; import sun.misc.Launcher; import java.net.URL; public Class User { public static void main(String[] args) { System.out.println(String.Class.getClassLoader()); // null System.out.println(com.sun.crypto.provider.DESKeyFactory.Class.getClassLoader().getClass().getName()); //sun.misc.Launcher$ExtClassLoader System.out.println(User.Class.getClassLoader().getClass().getName()); // sun.misc.Launcher$AppClassLoader System.out.println(); System.out.println("bootstrapLoader加載以下文件:"); URL[] urls = Launcher.getBootstrapClassPath().getURLs(); for (int i = 0; i < urls.length; i++) { System.out.println(urls[i]); } System.out.println(); System.out.println("extClassloader加載以下文件:"); System.out.println(System.getProperty("java.ext.dirs")); System.out.println(); System.out.println("appClassLoader加載以下文件:"); System.out.println(System.getProperty("java.Class.path")); } }
輸入如下:
null // 因?yàn)檎{(diào)用了 c++ 實(shí)現(xiàn)。無(wú)法獲取到j(luò)ava對(duì)象 sun.misc.Launcher$ExtClassLoader sun.misc.Launcher$AppClassLoader the bootstrapLoader : null the extClassloader : sun.misc.Launcher$ExtClassLoader@77459877 the appClassLoader : sun.misc.Launcher$AppClassLoader@18b4aac2 bootstrapLoader加載以下文件: file:/C:/Program%20Files/Java/jdk1.8.0_261/jre/lib/resources.jar file:/C:/Program%20Files/Java/jdk1.8.0_261/jre/lib/rt.jar file:/C:/Program%20Files/Java/jdk1.8.0_261/jre/lib/sunrsasign.jar file:/C:/Program%20Files/Java/jdk1.8.0_261/jre/lib/jsse.jar file:/C:/Program%20Files/Java/jdk1.8.0_261/jre/lib/jce.jar file:/C:/Program%20Files/Java/jdk1.8.0_261/jre/lib/charsets.jar file:/C:/Program%20Files/Java/jdk1.8.0_261/jre/lib/jfr.jar file:/C:/Program%20Files/Java/jdk1.8.0_261/jre/Classes extClassloader加載以下文件: C:\Program Files\Java\jdk1.8.0_261\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext appClassLoader加載以下文件: C:\Program Files\Java\jdk1.8.0_261\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_261\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_261\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_261\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_261\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_261\jre\lib\ext\jaccess.jar;...省略
為什么使用雙親委派機(jī)制
- 沙箱安全機(jī)制: 自己寫(xiě)的java.lang.String.Class類(lèi)不會(huì)被加載,這樣便可以防止核心 API庫(kù)被隨意篡改
- 避免類(lèi)的重復(fù)加載:當(dāng)父親已經(jīng)加載了該類(lèi)時(shí),就沒(méi)有必要子ClassLoader再加載一 次,保證被加載類(lèi)的唯一性
全盤(pán)負(fù)責(zé)委托機(jī)制
“全盤(pán)負(fù)責(zé)”是指當(dāng)一個(gè)ClassLoder裝載一個(gè)類(lèi)時(shí),除非顯示的使用另外一個(gè)ClassLoder,該類(lèi)
所依賴(lài)及引用的類(lèi)也由這個(gè)ClassLoder載入
自定義類(lèi)加載器
從上述源碼的描述可知,類(lèi)加載器的核心方法是 findClass , 和 defineClass 。
defindClass 將class文件從磁盤(pán)加載文件到內(nèi)存,defineClass 開(kāi)始解析class文件:
所以自定義類(lèi)加載器只需繼承 ClassLoader,然后從寫(xiě) findClass 文件就行了:
目錄如下:
App.java:
import java.io.FileInputStream; import java.lang.reflect.Method; public class App { static class MyClassLoader extends ClassLoader { private String classPath; public MyClassLoader(String classPath) { this.classPath = classPath; } // 從磁盤(pán)加載文件 private byte[] loadByte(String name) throws Exception { name = name.replaceAll("\\.", "/"); FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class"); int len = fis.available(); byte[] data = new byte[len]; fis.read(data); fis.close(); return data; } // 重寫(xiě) protected Class<?> findClass(String name) throws ClassNotFoundException { try { byte[] data = loadByte(name); // defineClass將一個(gè)字節(jié)數(shù)組轉(zhuǎn)為Class對(duì)象,這個(gè)字節(jié)數(shù)組是class文件讀取后最終的字節(jié) 數(shù)組。 return defieClass(name, data, 0, data.length); } catch (Exception e) { e.printStackTrace(); throw new ClassNotFoundException(); } } } public static void main(String args[]) throws Exception { // 初始化自定義類(lèi)加載器,會(huì)先初始化父類(lèi)ClassLoader,其中會(huì)把自定義類(lèi)加載器的父加載 器設(shè)置為應(yīng)用程序類(lèi)加載器AppClassLoader MyClassLoader classLoader = new MyClassLoader("D:/dc_code/java"); // D盤(pán)創(chuàng)建 // 創(chuàng)建 io/dc 幾級(jí)目錄,將User類(lèi)的復(fù)制類(lèi)User.class丟入該目錄 Class clazz = classLoader.loadClass("io.dc.User"); Object obj = clazz.newInstance(); // 使用反射調(diào)用 User 類(lèi)的 sout 方法 Method method = clazz.getDeclaredMethod("sout", null); method.invoke(obj, null); System.out.println(clazz.getClassLoader().getClass().getName()); } }
打破雙親委派機(jī)制
經(jīng)過(guò)上面的源碼分析發(fā)現(xiàn),主要是 ClassLoader
類(lèi)中的laodClass
方法來(lái)實(shí)現(xiàn)的雙親委派機(jī)制,自己不加載而是先讓其父加載。
所以直接復(fù)寫(xiě) loadClass 方法即可,不再指定父級(jí)加載,當(dāng)前類(lèi)直接加載,如下:
import java.io.FileInputStream; import java.lang.reflect.Method; public class App { static class MyClassLoader extends ClassLoader { private String classPath; public MyClassLoader(String classPath) { this.classPath = classPath; } // 從磁盤(pán)加載文件 private byte[] loadByte(String name) throws Exception { name = name.replaceAll("\\.", "/"); FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class"); int len = fis.available(); byte[] data = new byte[len]; fis.read(data); fis.close(); return data; } protected Class<?> findClass(String name) throws ClassNotFoundException { try { byte[] data = loadByte(name); // defineClass將一個(gè)字節(jié)數(shù)組轉(zhuǎn)為Class對(duì)象,這個(gè)字節(jié)數(shù)組是class文件讀取后最終的字節(jié) 數(shù)組。 return defineClass(name, data, 0, data.length); } catch (Exception e) { e.printStackTrace(); throw new ClassNotFoundException(); } } protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded Class<?> c = findLoadedClass(name); if (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); if (name.startsWith("io.dc")) { // 直接查找, 限定包名 c = findClass(name); } else { // 其他包中的類(lèi)還是使用雙親委派機(jī)制 // 否則會(huì)報(bào)找不到 Object 類(lèi) c = this.getParent().loadClass(name); } // this is the defining class loader; record the stats sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } if (resolve) { resolveClass(c); } return c; } } } public static void main(String args[]) throws Exception { // 初始化自定義類(lèi)加載器,會(huì)先初始化父類(lèi)ClassLoader,其中會(huì)把自定義類(lèi)加載器的父加載 器設(shè)置為應(yīng)用程序類(lèi)加載器AppClassLoader MyClassLoader classLoader = new MyClassLoader("D:/dc_code/java"); // D盤(pán)創(chuàng)建 // 創(chuàng)建 io/dc 幾級(jí)目錄,將User類(lèi)的復(fù)制類(lèi)User.class丟入該目錄 Class clazz = classLoader.loadClass("io.dc.User"); Object obj = clazz.newInstance(); // 使用反射調(diào)用 User 類(lèi)的 sout 方法 Method method = clazz.getDeclaredMethod("sout", null); method.invoke(obj, null); System.out.println(clazz.getClassLoader().getClass().getName()); } }
到此這篇關(guān)于java中類(lèi)加載與雙親委派機(jī)制詳解的文章就介紹到這了,更多相關(guān)java類(lèi)加載內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Kotlin基礎(chǔ)教程之控制流(順序,分支,循環(huán))
這篇文章主要介紹了Kotlin基礎(chǔ)教程之控制流的相關(guān)資料,需要的朋友可以參考下2017-05-05Java tomcat環(huán)境變量及idea配置解析
這篇文章主要介紹了Java tomcat環(huán)境變量及idea配置解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-12-12Eclipse+Java+Swing+Mysql實(shí)現(xiàn)工資管理系統(tǒng)
這篇文章主要介紹了Eclipse+Java+Swing+Mysql實(shí)現(xiàn)工資管理系統(tǒng),對(duì)正在工作或者學(xué)習(xí)的你有一定的參考價(jià)值,需要的朋友可以參考一下2022-01-01SpringBoot導(dǎo)入導(dǎo)出數(shù)據(jù)實(shí)現(xiàn)方法詳解
這篇文章主要介紹了SpringBoot導(dǎo)入導(dǎo)出數(shù)據(jù)實(shí)現(xiàn)方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)吧2022-12-12Java和Ceylon對(duì)象的構(gòu)造和驗(yàn)證
這篇文章主要為大家詳細(xì)介紹了Java和Ceylon對(duì)象的構(gòu)造和驗(yàn)證,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-11-11使用Springboot實(shí)現(xiàn)OAuth服務(wù)的示例詳解
OAuth(Open Authorization)是一個(gè)開(kāi)放標(biāo)準(zhǔn),用于授權(quán)第三方應(yīng)用程序訪問(wèn)用戶資源,而不需要共享用戶憑證。本文主要介紹了如何使用Springboot實(shí)現(xiàn)一個(gè)OAuth服務(wù),需要的可以參考一下2023-05-05使用jaxws建立webservice客戶端并實(shí)現(xiàn)soap消息的handler驗(yàn)證示例
這篇文章主要介紹了使用jaxws建立webservice客戶端并實(shí)現(xiàn)soap消息的handler驗(yàn)證示例,需要的朋友可以參考下2014-03-03Springboot整合Freemarker的實(shí)現(xiàn)詳細(xì)過(guò)程
這篇文章主要介紹了Springboot整合Freemarker的實(shí)現(xiàn)詳細(xì)過(guò)程,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-12-12