期貨做程序化回測的網(wǎng)站網(wǎng)站網(wǎng)絡(luò)排名優(yōu)化方法
文章目錄
- Jvm基本組成
- 一.什么是JVM類的加載
- 二.類的生命周期
- 階段1:加載
- 階段2:驗(yàn)證
- 階段3:準(zhǔn)備
- 階段4:解析
- 階段5:初始化
- 三.類初始化時(shí)機(jī)
- 四.類加載器
- 1.引導(dǎo)類加載器(Bootstrap Class Loader)
- 2.拓展類加載器(Extension Class Loader)
- 3.應(yīng)用程序類加載器(System Class Loader)
- 4.自定義類加載器(Custom Class Loader)
- 5.類加載器的關(guān)系
- 6.JVM類加載策略
- 五.雙親委派機(jī)制
- 1.什么是雙親委派機(jī)制
- 2.淺析ClassLoader類
- 3.雙親委派模式具體實(shí)現(xiàn)
- 3.1.loadClass(String name)
- 3.2.findClass(String name)-(重點(diǎn))
- 3.3.defineClass(byte[] b, int off, int len)
- 3.4.resolveClass(Class??? c)
- 3.5.URLClassLoader類
- 4.設(shè)計(jì)雙親委派機(jī)制的目的
- 5.ExtClassLoader和AppClassLoader
- 六.Tomcat 中的類加載器
- 1.Tomcat類加載器類型說明
- 2.Tomcat為什么要打破雙親委派模型
- 3.線程上下文類加載器
- 七.如何打破雙親委派模型
- 八.打破雙親委派模型的常見場景
- 九.熱部署類加載器
Jvm基本組成
JVM 的結(jié)構(gòu)基本上由 5 部分組成:
- 類加載器(ClassLoader):在 JVM 啟動(dòng)時(shí)或者類運(yùn)行時(shí)將需要的
class文件
解析后生成一個(gè)Class對象
加載到到 JVM 中 - 執(zhí)行引擎(ExcutionEngine):執(zhí)行引擎的任務(wù)是負(fù)責(zé)執(zhí)行 class 文件中包含的
字節(jié)碼指令
,相當(dāng)于實(shí)際機(jī)器上的CPU
- 內(nèi)存區(qū)(Memory Area):將內(nèi)存劃分成若干個(gè)區(qū)以
模擬實(shí)際機(jī)器上的存儲(chǔ)
、記錄和調(diào)度功能模塊,如實(shí)際機(jī)器上的各種功能的寄存器或者 PC 指針的記錄器等,由Heap、程序計(jì)數(shù)器、虛擬機(jī)棧、本地方法棧和方法區(qū)
五部分組成。 - 本地方法調(diào)用(Native Method Interface):調(diào)用
C 或 C++ 實(shí)現(xiàn)的本地方法
的代碼返回結(jié)果 - 垃圾收集器(Garbage Collection):負(fù)責(zé)將內(nèi)存區(qū)中
無用對象的釋放
,主要是堆內(nèi)存和方法區(qū)
JVM被分為三個(gè)主要的子系統(tǒng):類加載器子系統(tǒng)、運(yùn)行時(shí)數(shù)據(jù)區(qū)、執(zhí)行引擎
一.什么是JVM類的加載
Java代碼編譯執(zhí)行過程
- 源碼編譯:通過Java源碼編譯器(
Javac命令
)將Java代碼編譯成Jvm字節(jié)碼(.class文件) - 類加載:通
過ClassLoader
及其子類來完成Jvm的類加載
- 類執(zhí)行:字節(jié)碼被裝入內(nèi)存,進(jìn)入Jvm,被解釋器(
Java命令
)解釋執(zhí)行
什么是類的加載
- 從上圖來看其實(shí)可以一句話來解釋:類的加載指的是將類的
.class
文件中的二進(jìn)制數(shù)據(jù)讀入到內(nèi)存
中,并對class文件中的數(shù)據(jù)進(jìn)行校驗(yàn)、轉(zhuǎn)換、解析、初始化
等操作后,將其放在運(yùn)行時(shí)數(shù)據(jù)區(qū)
的方法區(qū)內(nèi)
,然后在堆
區(qū)創(chuàng)建一個(gè)Java.lang.Class
對象,封裝類在方法區(qū)內(nèi)的數(shù)據(jù)結(jié)構(gòu)。
二.類的生命周期
類從JVM加載到卸載出內(nèi),整個(gè)生命周期包括:加載、驗(yàn)證、準(zhǔn)備、解析、初始化
、使用和卸載 7個(gè)階段
類加載包含了前5個(gè)
,其中 驗(yàn)證、準(zhǔn)備、解析為連接。具體如圖:
階段1:加載
- 通過一個(gè)類的
全路徑
查找此類字節(jié)碼文件,并利用class文件
創(chuàng)建對應(yīng)的Class對象
并加載到方法區(qū)
階段2:驗(yàn)證
- 確保Class文件的字節(jié)流中包含信息符合
虛擬機(jī)規(guī)范
,而不會(huì)危害虛擬機(jī)自身運(yùn)行安全。主要包括4種驗(yàn)證,文件格式驗(yàn)證,元數(shù)據(jù)驗(yàn)證,字節(jié)碼驗(yàn)證,符號(hào)引用驗(yàn)證
。
階段3:準(zhǔn)備
- 為類變量(即static修飾的字段變量)分配內(nèi)存并且設(shè)置該類變量的默認(rèn)值
- (如static int i=5; 這里只將i初始化為0,至于5的值將在初始化時(shí)賦值),這里不包含用final修飾的static變量,因?yàn)?code>final在編譯的時(shí)候就會(huì)分配了,且這里
不會(huì)為實(shí)例變量分配初始化
,類變量會(huì)分配在方法區(qū)中,而實(shí)例變量是會(huì)隨著對象一起分配到Java堆
中。
- (如static int i=5; 這里只將i初始化為0,至于5的值將在初始化時(shí)賦值),這里不包含用final修飾的static變量,因?yàn)?code>final在編譯的時(shí)候就會(huì)分配了,且這里
階段4:解析
- 解析階段是將
常量池
中的符號(hào)引用
替換為直接引用
的過程。- 將符號(hào)引用替換為直接引用,該階段會(huì)把一些靜態(tài)方法(符號(hào)引用,比如 main() 方法)替換為指向數(shù)據(jù)所存內(nèi)存的指針或句柄等(直接引用),這就是所謂的
靜態(tài)鏈接過程
(類加載期間完成),動(dòng)態(tài)鏈接是在程序運(yùn)行期間完成的將符號(hào)引用替換為直接引用。 - 主要工作:類或接口的解析,字段解析,類方法解析,接口方法解析
- 將符號(hào)引用替換為直接引用,該階段會(huì)把一些靜態(tài)方法(符號(hào)引用,比如 main() 方法)替換為指向數(shù)據(jù)所存內(nèi)存的指針或句柄等(直接引用),這就是所謂的
例如:在com.xxx.Person類中引用了com.xxx.Animal類,在編譯階段Person類
并不知道Animal的實(shí)際內(nèi)存地址
,因此只能用com.xxx.Animal來代表Animal真實(shí)的內(nèi)存地址
。而在解析階段,JVM可以通過解析該符號(hào)引用
,來確定com.xxx.Animal類的真實(shí)內(nèi)存地址(如果該類未被加載過,則先加載)。
符號(hào)引用就是一組符號(hào)來描述目標(biāo),可以是任何字面量,而直接引用就是
直接指向目標(biāo)的指針、相對偏移量或一個(gè)間接定位到目標(biāo)的句柄
。
階段5:初始化
初始化階段是類加載過程的最后一步
,這一步才 真正開始執(zhí)行
類中定義的Java程序代碼。
即對類的靜態(tài)變量初始化為指定的值,執(zhí)行靜態(tài)代碼塊
。
在之前的準(zhǔn)備階段
,類中定義的static靜態(tài)變量已經(jīng)被賦過一次默認(rèn)值
。而在初始化階段,則會(huì)調(diào)用類構(gòu)造器<clinit>
來完成初始化操作,為靜態(tài)變量賦原始值。
- 此處,需要注意的是
類構(gòu)造器<clinit>
和類構(gòu)造方法<init>
區(qū)別。類構(gòu)造方法<init>
每創(chuàng)建一次對象,就自動(dòng)調(diào)用一次,而類構(gòu)造器<clinit>
類似于一個(gè)無參的構(gòu)造函數(shù)
,只不過該函數(shù)是靜態(tài)修飾
,只會(huì)初始化靜態(tài)所修飾的代碼
。
public class Student{public static int x = 10;public String zz = "jiaboyan";static{System.out.println("12345");}
}
類構(gòu)造器<clinit>
是由編譯器自動(dòng)收集類中的所有靜態(tài)變量的賦值動(dòng)作
和靜態(tài)語句塊static{}
中的代碼合并
產(chǎn)生的,編譯器收集的順序
是由語句在源文件中出現(xiàn)的順序
所決定的
綜上所述,對于上面的例子來說,類構(gòu)造器<clinit>
為:
public class Student{<clinit>{public static int x = 10;System.out.println("12345");}
}
三.類初始化時(shí)機(jī)
JVM規(guī)范中并沒有強(qiáng)制約束何時(shí)進(jìn)行加載,但是規(guī)范嚴(yán)格規(guī)定了有且只有
下列5種情況必須對類進(jìn)行初始化(加載、驗(yàn)證、準(zhǔn)備都會(huì)隨著發(fā)生)
:
-
遇到
new、getstatic、putstatic、invokestatic
這4條字節(jié)碼指令時(shí),如果類沒有進(jìn)行過初始化,則必須先觸發(fā)其初始化。最常見的生成這 4 條指令的場景是:
1.
使用 new 關(guān)鍵字實(shí)例化對象的時(shí)候;2.
讀取或設(shè)置類的靜態(tài)字段(被 final 修飾、已在編譯器把結(jié)果放入常量池的靜態(tài)字段除外)的時(shí)候3.
調(diào)用一個(gè)類的靜態(tài)方法的時(shí)候。 -
使用
java.lang.reflect 包
的方法對類進(jìn)行反射調(diào)用
的時(shí)候,如果類沒有進(jìn)行初始化,則需要先觸發(fā)其初始化。 -
當(dāng)初始化一個(gè)類的時(shí)候,如果發(fā)現(xiàn)其
父類還沒有進(jìn)行過初始化
,則需要先觸發(fā)其父類的初始化。 -
當(dāng)虛擬機(jī)啟動(dòng)時(shí),
用戶需要指定一個(gè)要執(zhí)行的主類(啟動(dòng)類)
(包含 main() 方法的那個(gè)類),虛擬機(jī)會(huì)先初始化這個(gè)主類; -
當(dāng)使用 JDK.7 的
動(dòng)態(tài)語言
支持時(shí),如果一個(gè)java.lang.invoke.MethodHandle
實(shí)例最后的解析結(jié)果為REF_getStatic, REF_putStatic, REF_invokeStatic
的方法句柄,并且這個(gè)方法句柄所對應(yīng)的類沒有進(jìn)行過初始化,則需要先觸發(fā)其初始化;
以上 5 種場景中的行為稱為對一個(gè)類進(jìn)行主動(dòng)引用
。除此之外,所有引用類的方式都不會(huì)觸發(fā)初始化,稱為被動(dòng)引用
。被動(dòng)引用的常見例子包括:
通過子類引用父類的靜態(tài)字段
,不會(huì)導(dǎo)致子類初始化。通過數(shù)組定義來引用類
,不會(huì)觸發(fā)此類的初始化。該過程會(huì)對數(shù)組類進(jìn)行初始化,數(shù)組類是一個(gè)由虛擬機(jī)自動(dòng)生成的、直接繼承自 Object 的子類,其中包含了數(shù)組的屬性和方法。常量在編譯階段會(huì)存入調(diào)用類的常量池中
,本質(zhì)上并沒有直接引用到定義常量的類,因此不會(huì)觸發(fā)定義常量的類的初始化。
特殊情況
- 當(dāng) Java 虛擬機(jī)初始化一個(gè)類時(shí),要求
它所有的父類都被初始化
,但這一條規(guī)則并不適用于接口。一個(gè)父接口并不會(huì)因?yàn)樗淖咏涌诨蛘邔?shí)現(xiàn)類的初始化而初始化,只有當(dāng)程序首次被使用特定接口的靜態(tài)變量時(shí),才會(huì)導(dǎo)致該接口的初始化。
代碼塊、靜態(tài)方法、構(gòu)造方法加載順序
public class Son extends Father{static { System.out.println("子類靜態(tài)代碼塊"); }{ System.out.println("子類代碼塊"); }public Son() { System.out.println("子類構(gòu)造方法"); }public static void main(String[] args) {new Son();}}class Father{static { System.out.println("父類靜態(tài)代碼塊"); }{System.out.println("父類代碼塊");}public Father() { System.out.println("父類構(gòu)造方法");}public static void find() {System.out.println("靜態(tài)方法");}
}
//代碼塊和構(gòu)造方法執(zhí)行順序
//父類靜態(tài)代碼塊
//子類靜態(tài)代碼塊
//父類代碼塊
//父類構(gòu)造方法
//子類代碼塊
//子類構(gòu)造方法
四.類加載器
- 類加載器的作用 : 通過
類全限定名
讀取類的二進(jìn)制字節(jié)流到JVM中,然后轉(zhuǎn)換為一個(gè)與目標(biāo)類對應(yīng)的java.lang.Class
對象實(shí)例
1.引導(dǎo)類加載器(Bootstrap Class Loader)
- 引導(dǎo)類加載器,也叫啟動(dòng)類加載器 主要加載的是JVM自身需要的類,該類加載使用
C++
實(shí)現(xiàn)的,是虛擬機(jī)自身的一部分,負(fù)責(zé)加載<JAVA_HOME>\lib\ 目錄下的核心類庫
或被-Dbootclaspath 參數(shù)指定的類
, 如: rt.jar, tool.jar 等
2.拓展類加載器(Extension Class Loader)
- 拓展類加載器: 由Sun公司通過Java在
sun.misc.Launcher$ExtClassLoader類
中實(shí)現(xiàn), 負(fù)責(zé)加載<JAVA_HOME>\lib\ext 擴(kuò)展目錄中的類庫
或-Djava.ext.dirs 選項(xiàng)所指定目錄下的類和 jar包
,開發(fā)者可以直接使用標(biāo)準(zhǔn)擴(kuò)展類加載器,
//Launcher$ExtClassLoader類中獲取路徑的代碼
private static File[] getExtDirs() {//加載<JAVA_HOME>/lib/ext目錄中的類庫String s = System.getProperty("java.ext.dirs");File[] dirs;if (s != null) {StringTokenizer st = new StringTokenizer(s, File.pathSeparator);int count = st.countTokens();dirs = new File[count];for (int i = 0; i < count; i++) {dirs[i] = new File(st.nextToken());}} else {dirs = new File[0];}return dirs;}
3.應(yīng)用程序類加載器(System Class Loader)
- 應(yīng)用程序類加載器,也稱為系統(tǒng)類加載器:由Sun公司通過Java在
sun.misc.Launcher$AppClassLoader
中實(shí)現(xiàn), 負(fù)責(zé)加載環(huán)境變量 CLASSPATH
或-Djava.class.path、java -cp所指定的目錄下的類和 jar 包
??梢酝ㄟ^ClassLoader#getSystemClassLoader()
獲取到該類加載器,開發(fā)者可以直接使用系統(tǒng)類加載器,該加載器是程序中默認(rèn)類加載器
,
public class MainClassLoaderTest {public static void main(String[] args) {System.out.println(String.class.getClassLoader());System.out.println(DESKeyFactory.class.getClassLoader());System.out.println(MainClassLoaderTest.class.getClassLoader());/*nullsun.misc.Launcher$ExtClassLoader@246b179dsun.misc.Launcher$AppClassLoader@18b4aac2*/System.out.println();ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();//應(yīng)用程序類加載器ClassLoader extClassLoader = appClassLoader.getParent();//拓展類加載器ClassLoader bootstrapClassLoader = extClassLoader.getParent();//引導(dǎo)類加載器System.out.println("bootstrapClassLoader: " + bootstrapClassLoader);System.out.println("extClassLoader: " + extClassLoader);System.out.println("appClassLoader: " + appClassLoader);/*bootstrapClassLoader: nullextClassLoader: sun.misc.Launcher$ExtClassLoader@246b179dappClassLoader: sun.misc.Launcher$AppClassLoader@18b4aac2*///BootstrapLoader 父加載器 及 加載路徑System.out.println();System.out.println("bootstrapLoader 加載以下文件:");URL[] urls = Launcher.getBootstrapClassPath().getURLs();for (URL url : urls) {System.out.println(url);}/*bootstrapLoader 加載以下文件:file:/E:/dev_tools/jdk/jdk1.8.0_181/jre/lib/resources.jarfile:/E:/dev_tools/jdk/jdk1.8.0_181/jre/lib/rt.jarfile:/E:/dev_tools/jdk/jdk1.8.0_181/jre/lib/sunrsasign.jarfile:/E:/dev_tools/jdk/jdk1.8.0_181/jre/lib/jsse.jarfile:/E:/dev_tools/jdk/jdk1.8.0_181/jre/lib/jce.jarfile:/E:/dev_tools/jdk/jdk1.8.0_181/jre/lib/charsets.jarfile:/E:/dev_tools/jdk/jdk1.8.0_181/jre/lib/jfr.jarfile:/E:/dev_tools/jdk/jdk1.8.0_181/jre/classes*///ExtClassLoader 父加載器 及 加載路徑System.out.println();System.out.println("extClassLoader 加載以下文件:");System.out.println(System.getProperty("java.ext.dirs"));/*extClassLoader 加載以下文件:E:\dev_tools\jdk\jdk1.8.0_181\jre\lib\ext;C:\Windows\Sun\Java\lib\ext*///AppClassLoader父加載器 及 加載路徑System.out.println();System.out.println("appClassLoader 加載以下文件:");System.out.println(System.getProperty("java.class.path"));/*appClassLoader 加載以下文件:E:\dev_tools\jdk\jdk1.8.0_181\jre\lib\charsets.jar;省略.....E:\dev_tools\jdk\jdk1.8.0_181\jre\lib\rt.jar;省略E:\dev_tools\jdk\jdk1.8.0_181\jre\lib\resources.jar;省略E:\04_resource_study\java_base_demo\target\classes;省略E:\dev_tools\mavenLocalStorage\org\springframework\boot\spring-boot-starter-web\2.2.0.RELEASE\spring-boot-starter-web-2.2.0.RELEASE.jar;E:\dev_tools\mavenLocalStorage\com\alibaba\fastjson\1.2.78\fastjson-1.2.78.jar;E:\dev_tools\mavenLocalStorage\mysql\mysql-connector-java\8.0.18\mysql-connector-java-8.0.18.jar;*/}
}
- 可看出,Launcher采用了
單例設(shè)計(jì)模式
,其中BootstrapClassLoader 、ExtClassLoader和AppClassLoader的掃描路徑分別對應(yīng)系統(tǒng)屬性sun.boot.class.path、java.ext.dirs、java.class.path
4.自定義類加載器(Custom Class Loader)
- 自定義類加載器:負(fù)責(zé)
加載用戶自定義包路徑下的類包
,通過 ClassLoader 的子類實(shí)現(xiàn) Class 的加載。 - 如何實(shí)現(xiàn):
繼承抽象類ClassLoader
,重寫findClass方法
,判斷當(dāng)前類的class文件是否已被加載
/*** 自定義類加載器*/
public class MyClassLoader extends ClassLoader {//包路徑private String path;//構(gòu)造方法,用于初始化Path屬性public MyClassLoader(String path) {this.path = path;}//重寫findClass方法,參數(shù)name表示要加載類的全類名(包名.類名)@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {//檢查該類的class文件是否已被加載,如果已加載則返回class文件(字節(jié)碼文件)對象,如果沒有加載返回nullClass<?> loadedClass = findLoadedClass(name);//如果已加載直接返回該類的class文件(字節(jié)碼文件)對象if (loadedClass != null) {return loadedClass;}//字節(jié)數(shù)組,用于存儲(chǔ)class文件的字節(jié)流byte[] bytes = null;try {//獲取class文件的字節(jié)流bytes = getBytes(name);} catch (Exception e) {e.printStackTrace();}if (bytes == null) {throw new ClassNotFoundException();}//如果字節(jié)數(shù)組不為空,則將class文件加載到JVM中//將class文件加載到JVM中,返回class文件對象//字節(jié)碼數(shù)組加載到 JVM 的方法區(qū),并在 JVM 的堆區(qū)建立一個(gè)java.lang.Class對象的實(shí)例,用來封裝 Java 類相關(guān)的數(shù)據(jù)和方法return this.defineClass(name, bytes, 0, bytes.length);}//獲取class文件的字節(jié)流private byte[] getBytes(String name) throws Exception {//拼接class文件路徑 replace(".",File.separator) 表示將全類名中的"."替換為當(dāng)前系統(tǒng)的分隔符,File.separator返回當(dāng)前系統(tǒng)的分隔符String fileUrl = path + name.replace(".", File.separator) + ".class";//緩沖區(qū)byte[] buffer = new byte[1024];//輸入流InputStream fis = new FileInputStream(new File(fileUrl));//相當(dāng)于一個(gè)緩存區(qū),動(dòng)態(tài)擴(kuò)容,也就是隨著寫入字節(jié)的增加自動(dòng)擴(kuò)容ByteArrayOutputStream baos = new ByteArrayOutputStream();//循環(huán)將輸入流中的所有數(shù)據(jù)寫入到緩存區(qū)中int bytesNumRead = 0;// 讀取類文件的字節(jié)碼while ((bytesNumRead = fis.read(buffer)) != -1) {baos.write(buffer, 0, bytesNumRead);}baos.flush();baos.close();return baos.toByteArray();}public static void main(String[] args) throws IllegalAccessException, InstantiationException, ClassNotFoundException {String path = "E:/04_resource_study/java_base_demo/target/classes/";//創(chuàng)建自定義類加載器對象MyClassLoader classLoader = new MyClassLoader(path);System.out.println("MyDamageClassLoader的父加載器:" + classLoader.getParent());//返回加載的class對象Class<?> clazz = classLoader.findClass("com.demo.classload.ClassLoaderTest");//調(diào)用類的構(gòu)造方法創(chuàng)建對象Object o = clazz.newInstance();//輸出創(chuàng)建的對象System.out.println("創(chuàng)建的對象:"+o);//輸出當(dāng)前類加載器System.out.println("ClassLoaderTest當(dāng)前類加載器:"+clazz.getClassLoader());//輸出當(dāng)前類加載器的父類System.out.println("ClassLoaderTest當(dāng)前類加載器的父類:"+clazz.getClassLoader().getParent());//輸出當(dāng)前類加載器的父類的父類System.out.println("ClassLoaderTest當(dāng)前類加載器的父類的父類:"+clazz.getClassLoader().getParent().getParent());//輸出當(dāng)前類加載器的父類的父類的父類System.out.println("ClassLoaderTest當(dāng)前類加載器的父類的父類的父類:"+clazz.getClassLoader().getParent().getParent().getParent());/*執(zhí)行結(jié)果:MyDamageClassLoader的父加載器:sun.misc.Launcher$AppClassLoader@18b4aac2創(chuàng)建的對象:com.demo.classload.ClassLoaderTest@238e0d81ClassLoaderTest當(dāng)前類加載器:com.demo.classload.MyDamageClassLoader@7daf6eccClassLoaderTest當(dāng)前類加載器的父類:sun.misc.Launcher$AppClassLoader@18b4aac2ClassLoaderTest當(dāng)前類加載器的父類的父類:sun.misc.Launcher$ExtClassLoader@31221be2ClassLoaderTest當(dāng)前類加載器的父類的父類的父類:null*/}
}
為什么要自定義類加載器?
- 當(dāng)class文件不在ClassPath路徑下,默認(rèn)系統(tǒng)類加載器無法找到該class文件
- 當(dāng)一個(gè)class文件是通過網(wǎng)絡(luò)傳輸并且可能會(huì)進(jìn)行相應(yīng)的加密操作時(shí),需要先對class文件進(jìn)行相應(yīng)的解密后再加載到JVM內(nèi)存中
- 當(dāng)需要實(shí)現(xiàn)熱部署功能時(shí)(
一個(gè)class文件通過不同的類加載器產(chǎn)生不同class對象從而實(shí)現(xiàn)熱部署功能
)
5.類加載器的關(guān)系
- JVM 的類加載器具有父子關(guān)系,但不是通過繼承來實(shí)現(xiàn),
而是每個(gè)類加載器通過組合方式維護(hù)一個(gè) parent字段,指向父加載器
。 - 啟動(dòng)類加載器(Bootstrap Class Loader),由C++實(shí)現(xiàn),沒有父類。啟動(dòng)類加載器無法被java程序直接引用。
- 拓展類加載器(Extension ClassLoader),由Java語言實(shí)現(xiàn),父類加載器為null(
如果parent=null,則它的父級(jí)就是啟動(dòng)類加載器。
) - 系統(tǒng)類加載器(AppClassLoader),由Java語言實(shí)現(xiàn),父類加載器為ExtClassLoader
- 自定義類加載器(Custom Class Loader),父類加載器肯定為
AppClassLoader。
6.JVM類加載策略
-
按需加載: JVM對Class文件采用的是
按需加載
的方式,也就是說當(dāng)需要使用該類時(shí)才會(huì)將它的Class文件加載到內(nèi)存生成Class對象
-
全盤負(fù)責(zé):當(dāng)一個(gè)類加載器負(fù)責(zé)加載某個(gè)Class時(shí),
該Class所依賴的和引用的其他Class也將由該類加載器負(fù)責(zé)一起加載
,除非顯示使用另外一個(gè)類加載器來載加載 -
父類委托(雙親委派):先讓父類加載器試圖加載該類,只有在父類加載器無法加載該類時(shí)才嘗試從自己的類路徑中加載該類(為了避免不同的加載器 加載相同的類 出現(xiàn)重復(fù)字節(jié))
-
緩存機(jī)制:緩存機(jī)制將會(huì)保證所有加載過的Class都會(huì)被緩存,當(dāng)程序中需要使用某個(gè)Class時(shí),類加載器先從緩存區(qū)尋找該Class,只有緩存區(qū)不存在,系統(tǒng)才會(huì)讀取該類對應(yīng)的二進(jìn)制數(shù)據(jù),并將其轉(zhuǎn)換成Class對象,存入緩存區(qū)。這就是為什么修改了Class后,必須重啟JVM,程序的修改才會(huì)生效
在JVM中表示兩個(gè)class對象是否為同一個(gè)類對象存在2個(gè)必要條件
- 類的全限定名必須一致(即包名.類名)
- 加載這個(gè)類的ClassLoader(指ClassLoader實(shí)例對象)必須相同。
即JVM中兩個(gè)類對象(class對象)就算來源同一個(gè)Class文件,被同一個(gè)虛擬機(jī)所加載,但只要加載它們的ClassLoader實(shí)例對象不同,那么這兩個(gè)類對象也是不相等的,這是因?yàn)?code>不同的ClassLoader實(shí)例對象都擁有不同的獨(dú)立的類名稱空間,所以加載的class對象也會(huì)存在不同的類名空間中
,但前提是重寫loadClass()
, 因?yàn)殡p親委派模型第一步會(huì)通過Class c = findLoadedClass(name)從緩存查找,類全限定名相同則不會(huì)再次被加載
,因此我們必須跳過緩存查詢
才能重新加載class對象。當(dāng)然也可直接調(diào)用findClass()
方法,這樣也避免從緩存查找.
五.雙親委派機(jī)制
1.什么是雙親委派機(jī)制
JVM 的類加載器具有父子關(guān)系,雙親委派機(jī)制是在Java 1.2
后引入的,其工作原理的是 ,如果一個(gè)類加載器收到了類加載請求,它并不會(huì)自己先去加載,而是把這個(gè)請求委托給父類的加載器去執(zhí)行,如果父類加載器還存在其父類加載器,則進(jìn)一步向上委托,依次遞歸,請求最終將到達(dá)頂層的啟動(dòng)類加載器(Bootstrap Class Loader),如果啟動(dòng)類加載器可以完成類加載任務(wù),就成功返回,否則就一層一層向下委派子加載器去嘗試加載,這就是雙親委派模式
,即每個(gè)兒子都很懶,每次有活就丟給父親去干,直到父親說這件事我也干不了時(shí),兒子自己想辦法去完成
,這不就是傳說中的實(shí)力坑爹啊?
如圖所示
- 當(dāng)System ClassLoader拿到一個(gè)class 文件之后, 會(huì)先問父加載器Extension ClassLoader能不能加載
- 當(dāng)Extension ClassLoader 接收到請求時(shí),會(huì)先問問他的父加載器BootStrap ClassLoader能不能加載
- 如果 Bootstrap ClassLoader可以加載,則由Bootstrap ClassLoader來加載,如不能則由Extension ClassLoader來加載. 如果 Extension ClassLoader也加載不了的話,最后由System ClassLoader來加載.
那么采用這種模式有啥用呢?
- 雙親委派機(jī)制保證類加載器,
自下而上的委派,又自上而下的加載
,保證每一個(gè)類在各個(gè)類加載器中都是同一個(gè)類
。
2.淺析ClassLoader類
JVM 并不是在啟動(dòng)時(shí)就把所有的.class文件都加載一遍,而是程序在運(yùn)行過程中用到了這個(gè)類才去加載
。除了啟動(dòng)類加載器外,其他所有類加載器都需要繼承抽象類ClassLoader
,這個(gè)抽象類中定義了3個(gè)關(guān)鍵方法,理解清楚它們的作用和關(guān)系非常重要。
public abstract class ClassLoader {//委托的父類加載器private final ClassLoader parent;//name為類的全限定名(包名.類名) public Class<?> loadClass(String name) throws ClassNotFoundException {return loadClass(name, false);}//name為類的全限定名(包名.類名) ,resolve如果為true,則在生成class對象的同時(shí)進(jìn)行解析相關(guān)操作protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {//同步控制:開啟并發(fā)加載的情況下,鎖對類對應(yīng)的鎖對象,否則為ClassLoader對象本身synchronized (getClassLoadingLock(name)) {//首先,檢查類是否已經(jīng)加載Class<?> c = findLoadedClass(name);//1.如果沒有被加載if (c == null) {long t0 = System.nanoTime();try {//如果有父加載器,則先委派給父加載器去加載(注意這里是遞歸調(diào)用)if (parent != null) {c = parent.loadClass(name, false);// 如果父加載器為空,則委托啟動(dòng)類加載器去加載 } else {c = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {//如果沒有從非空父加載器中找到類,則拋出類異常ClassNotFoundException }//2.如果父加載器沒加載成功,調(diào)用自己實(shí)現(xiàn)的findClass去加載if (c == null) {//如果仍然沒有找到,則調(diào)用findClass來查找類。long t1 = System.nanoTime();c = findClass(name);//這是定義類裝入器;記錄數(shù)據(jù)sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}//是否生成class對象的同時(shí)進(jìn)行解析相關(guān)操作if (resolve) {resolveClass(c);}return c;}}//name為類的全限定名(包名.類名) protected Class<?> findClass(String name){//1. 根據(jù)傳入的類名name,到在特定目錄下去尋找類文件,把.class文件讀入內(nèi)存//......省略具體實(shí)現(xiàn)......//2. 調(diào)用defineClass將字節(jié)數(shù)組轉(zhuǎn)成Class對象return defineClass(buf, off, len);}//將字節(jié)碼數(shù)組解析成一個(gè)Class對象,用native方法實(shí)現(xiàn)//b:class文件字節(jié)數(shù)組,off:開始讀取位置,len:每次字節(jié)數(shù)protected final Class<?> defineClass(byte[] b, int off, int len){//......省略具體實(shí)現(xiàn)......}//獲取類名對應(yīng)的鎖對象//ClassLoader并發(fā)加載是通過一個(gè)ConcurrentHashMap<String,Object>實(shí)現(xiàn)的,Key為類名,對應(yīng)的Value為一個(gè)new Object(),//所以它可以同時(shí)加載多個(gè)類,但同一個(gè)類重復(fù)加載時(shí)則可以鎖住。通過registerAsParallelCapable()可以啟用并發(fā)加載。//詳見https://blog.csdn.net/w1673492580/article/details/81912344protected Object getClassLoadingLock(String className) {Object lock = this;if (parallelLockMap != null) {Object newLock = new Object();lock = parallelLockMap.putIfAbsent(className, newLock);if (lock == null) {lock = newLock;}}return lock;}
}
從上面的代碼可以得到幾個(gè)關(guān)鍵信息:
- JVM 的類加載器具有父子關(guān)系,但不是通過繼承來實(shí)現(xiàn),
而是每個(gè)類加載器通過組合方式維護(hù)一個(gè) parent字段,指向父加載器
。- AppClassLoader的parent是ExtClassLoader,ExtClassLoader的parent是BootstrapClassLoader,但是ExtClassLoader的parent=null
- defineClass():通過
調(diào)用 native 方法
把 Java 類的字節(jié)碼解析成一個(gè) Class 對象。 - findClass():用于
找到.class文件并把.class文件讀到內(nèi)存得到字節(jié)碼數(shù)組
,然后在方法內(nèi)部調(diào)用 defineClass方法得到 Class 對象子類必須實(shí)現(xiàn)findClass
。
- loadClass():主要職責(zé)就是
實(shí)現(xiàn)雙親委派機(jī)制
:首先檢查類是不是被加載過,如果加載過直接返回,否則委派給父加載器加載,這是一個(gè)遞歸調(diào)用,一層一層向上委派,最頂層的類加載器(啟動(dòng)類加載器)無法加載該類時(shí),再一層一層向下委派給子類加載器加載
。
如圖所示:
3.雙親委派模式具體實(shí)現(xiàn)
- 可以看出頂級(jí)類加載器是
抽象類ClassLoader類
,除啟動(dòng)類加載器外,所有的類加載器都繼承自ClassLoader
,這里主要介紹ClassLoader中幾個(gè)比較重要的方法。
3.1.loadClass(String name)
該方法就是雙親委派機(jī)制的具體實(shí)現(xiàn),用于加載指定全限定名的類,由ClassLoader類實(shí)現(xiàn),在JDK1.2之后不建議開發(fā)者重寫但可以直接通過this.getClass().getClassLoder.loadClass("className")
調(diào)用,源碼如下:
- 業(yè)務(wù)邏輯:當(dāng)類加載請求到來時(shí),
先從緩存中查找該類對象,如果存在直接返回
,如果不存在則交給該類加載器去的父加載器去加載
,倘若沒有父加載器則交給頂級(jí)啟動(dòng)類加載器
去加載,最后倘若仍沒有找到,則使用findClass()
方法去加載
//name為類的全限定名(包名.類名) public Class<?> loadClass(String name) throws ClassNotFoundException {return loadClass(name, false);}//name為類的全限定名(包名.類名) ,resolve如果為true,則在生成class對象的同時(shí)進(jìn)行解析相關(guān)操作protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {//同步鎖synchronized (getClassLoadingLock(name)) {//首先,檢查類是否已經(jīng)加載Class<?> c = findLoadedClass(name);//1.如果沒有被加載if (c == null) {long t0 = System.nanoTime();try {//如果有父加載器,則先委派給父加載器去加載(注意這里是遞歸調(diào)用)if (parent != null) {c = parent.loadClass(name, false);// 如果父加載器為空,則委托啟動(dòng)類加載器去加載 } else {c = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {//如果沒有從非空父加載器中找到類,則拋出類異常ClassNotFoundException }//2.如果父加載器沒加載成功,調(diào)用自己實(shí)現(xiàn)的findClass去加載if (c == null) {//如果仍然沒有找到,則調(diào)用findClass來查找類。long t1 = System.nanoTime();c = findClass(name);//這是定義類裝入器;記錄數(shù)據(jù)sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}//是否生成class對象的同時(shí)進(jìn)行解析相關(guān)操作if (resolve) {resolveClass(c);}return c;}}
3.2.findClass(String name)-(重點(diǎn))
該方法在JDK1.2之后已不建議用戶去覆蓋loadClass()方法
,而是建議把自定義的類加載邏輯寫在findClass()方法中
- findClass()在
loadClass()
中被調(diào)用的,在loadClass()
中當(dāng)父加載器
加載失敗后,則會(huì)調(diào)用自己的findClass()
方法來完成類加載,這樣就可以保證自定義的類加載器也符合雙親委托模式。 - ClassLoader類沒有實(shí)現(xiàn)
findClass()
方法,只是簡單的拋出ClassNotFoundException
protected Class<?> findClass(String name) throws ClassNotFoundException {throw new ClassNotFoundException(name);//直接拋出異常 }
- 在findClass()中
把class文件讀到內(nèi)存得到字節(jié)碼數(shù)組
后,需要在該方法內(nèi)部調(diào)用 defineClass()方法得到 Class 對象
3.3.defineClass(byte[] b, int off, int len)
該方法由ClassLoader類中實(shí)現(xiàn),用于將byte字節(jié)流解析成JVM能夠識(shí)別的Class對象
(),通常與findClass()方法一起使用
- 一般情況下,在自定義類加載器時(shí),會(huì)直接覆蓋ClassLoader的findClass()方法并重寫類加載規(guī)則,取得要加載類的字節(jié)碼后轉(zhuǎn)換成流,然后調(diào)用defineClass()方法生成類的Class對象
- 直接調(diào)用defineClass()生成類的Class對象,該Class對象并沒有
解析
(也可以理解為鏈接階段,畢竟解析是鏈接的最后一步),其解析操作需要等待初始化階段進(jìn)行。
代碼示例
protected Class<?> findClass(String name) throws ClassNotFoundException {// 獲取類的字節(jié)數(shù)組byte[] classData = getClassData(name); if (classData == null) {throw new ClassNotFoundException();} else {//使用defineClass生成class對象return defineClass(name, classData, 0, classData.length);}}
3.4.resolveClass(Class??? c)
- 使用該方法可以使用類的
Class對象創(chuàng)建完成也同時(shí)被解析
。前面我們說鏈接階段主要是對字節(jié)碼進(jìn)行驗(yàn)證
,為類變量分配內(nèi)存并設(shè)置初始值同時(shí)將字節(jié)碼文件中的符號(hào)引用轉(zhuǎn)換為直接引用
。
3.5.URLClassLoader類
URLClassLoader是ClassLoade的具體實(shí)現(xiàn)
,并新增URLClassPath類
輔助獲取Class字節(jié)碼流
等功能,如果自定義類加載器時(shí),沒有太復(fù)雜的需求,可以直接繼承URLClassLoader類
,這樣就可以避免自己去編寫findClass()方法及其獲取字節(jié)碼流的方式,使自定義類加載器編寫更加簡潔
4.設(shè)計(jì)雙親委派機(jī)制的目的
-
避免類的重復(fù)加載:當(dāng)父類加載器已經(jīng)加載類后子類加載器沒必要再次加載,保證了類加載的唯一性。
- 雙親委派保證類加載器,自下而上的委派,又自上而下的加載,保證每一個(gè)類在各個(gè)類加載器中都是同一個(gè)類。
-
保證 Java 核心庫的類型安全:Java 核心庫中的類加載工作都是由
啟動(dòng)類加載器
統(tǒng)一來完成的。從而確保了Java 應(yīng)用所使用的都是同一個(gè)版本的 Java 核心類庫,他們之間是相互兼容的。- 如
java.lang.Object
,存在于rt.jar
中,無論哪個(gè)類加載器要加載這個(gè)類,最終都是委派給啟動(dòng)類加載器加載
,因此Object類在程序的各種類加載器環(huán)境中都是同一個(gè)類。
- 如
-
不同的類加載器可以為
相同類(binary name)的類
創(chuàng)建額外的命名空間。相同名稱的類可以并存在JVM中,只需要不同的類加載器來加載他們即可,不同的類加載器的類之間是不兼容的,這相當(dāng)于在JVM內(nèi)部創(chuàng)建了一個(gè)又一個(gè)相互隔離的Java類空間,這類技術(shù)在很多框架中得到了實(shí)際運(yùn)用。
5.ExtClassLoader和AppClassLoader
拓展類加載器ExtClassLoader和系統(tǒng)類加載器AppClassLoader都繼承自URLClassLoader
,是sun.misc.Launcher的靜態(tài)內(nèi)部類
。
- sun.misc.Launcher主要被系統(tǒng)用于
啟動(dòng)主應(yīng)用程序
ExtClassLoader并沒有重寫loadClass()方法
,這說明其遵循雙親委派模式
,而AppClassLoader重載了loadCass()方法
,但最終調(diào)用的還是父類loadClass()方法
,因此依然遵守雙親委派模式,重載方法源碼如下:
/*** Override loadClass 方法,新增包權(quán)限檢測功能*/public Class loadClass(String name, boolean resolve) throws ClassNotFoundException{int i = name.lastIndexOf('.');if (i != -1) {SecurityManager sm = System.getSecurityManager();if (sm != null) {sm.checkPackageAccess(name.substring(0, i));}}//依然調(diào)用父類的方法return (super.loadClass(name, resolve));}
- 其實(shí)無論是ExtClassLoader還是AppClassLoader都繼承URLClassLoader類,因此它們都遵守雙親委托模型
六.Tomcat 中的類加載器
1.Tomcat類加載器類型說明
在Java類加載器基礎(chǔ)上,Tomcat新增了幾個(gè)類加載器,包括3個(gè)基礎(chǔ)類加載器和每個(gè)Web應(yīng)用的私有類加載器,其中3個(gè)基礎(chǔ)類加載器可在conf/catalina.properties
中配置,具體介紹下:
-
Common Class Loader::以
System Class Loader
為父類,是tomcat頂層的公用類加載器,其路徑由conf/catalina.properties
中的common.loader指定,默認(rèn)加載$CATALINE_HOME/lib
下的包。 -
Catalina Class Loader:以
Common Class Loader
為父類,是用于加載Tomcat應(yīng)用服務(wù)器的類加載器,其路徑由server.loader
指定,默認(rèn)為空,此時(shí)tomcat使用Common Class Loader
加載應(yīng)用服務(wù)器。 -
Shared ClassLoader:以
Common Class Loader
為父類,是所有Web應(yīng)用的父加載器,其路徑由shared.loader
指定,默認(rèn)為空,此時(shí)Tomcat使用Common Class Loader
類加載器作為Web應(yīng)用的父加載器。 -
Webapp ClassLoader: 以
Shared ClassLoader
為父類,加載/WEB-INF/classes
目錄下的未壓縮的Class
和資源文件
以及/WEB-INF/lib目錄下的jar包
,是各個(gè) Webapp 私有的類加載器
, 加載路徑中的 class 只對當(dāng)前 webapp 可見- 好處:每個(gè)的Context(web應(yīng)用/war包)都使用獨(dú)立的ClassLoader來加載web應(yīng)用中的
WEB-INF/libs
目錄下的jar包, 因此不同的web應(yīng)用包不會(huì)沖突。如A應(yīng)用用的是spring 4.X , B應(yīng)用用的是spring 5.X , 他們可以在同一個(gè)tomcat中運(yùn)行
- 好處:每個(gè)的Context(web應(yīng)用/war包)都使用獨(dú)立的ClassLoader來加載web應(yīng)用中的
默認(rèn)情況下,Common、Catalina、Shared類加載器是同一個(gè)
,但可以配置3個(gè)不同的類加載器,使他們各司其職。
2.Tomcat為什么要打破雙親委派模型
- 我們都知道
一個(gè)Tomcat 是可以部署多個(gè) war 包的
,如果部署的多個(gè)war包中依賴的Jar包是不同版本的,比如War包A依賴 Spring 4,War包B依賴 Spring5 ,這時(shí)根據(jù)雙親委派機(jī)制,Spring4首先被加載進(jìn)來,那么另一個(gè)依賴 Spring 5 的 War包B在加載時(shí)就不會(huì)再去加載 Spring 5 。因?yàn)橥脑驎?huì)直接給他返回已加載過的 Spring 4 。這時(shí)會(huì)出現(xiàn)版本不一致
的問題。因此對于 Tomcat 來說他就需要自己實(shí)現(xiàn)類加載器來打破雙親委派模型
,并給每一個(gè)war包去生成一個(gè)自己對應(yīng)的類加載器
如何打破?
- Tomcat在初始化的時(shí)候通過
Thread.currentThread().setContextClassLoader(xx)
設(shè)置成了Catalina ClassLoader
,使用Catalina ClassLoader來加載Tomcat使用的類,當(dāng)Tomcat加載WebApp中的類時(shí)設(shè)置成了WebappClassLoader
,而WebappClassLoader重寫了loadClass方法打破了雙親委派
。
Web應(yīng)用默認(rèn)的類加載順序是(打破了雙親委派規(guī)則):
- 先從JVM的BootStrapClassLoader中加載。
- 加載Web應(yīng)用下
/WEB-INF/classes
中的類。 - 加載Web應(yīng)用下
/WEB-INF/lib/
中的jar包中的類。 - 加載上面定義的System路徑下面的類。
- 加載上面定義的Common路徑下面的類。
如果在配置文件中配置了<Loader delegate="true"/>
,那么就是遵循雙親委派規(guī)則,加載順序如下:
- 先從JVM的BootStrapClassLoader中加載。
- 加載上面定義的System路徑下面的類。
- 加載上面定義的Common路徑下面的類。
- 加載Web應(yīng)用下
/WEB-INF/classes
中的類。
加載Web應(yīng)用下/WEB-INF/lib/
中的jar包中的類。
3.線程上下文類加載器
線程上下文類加載器(ContextClassLoader)是JDK 1.2
開始引入一種類加載器傳遞機(jī)制??梢酝ㄟ^Thread.currentThread().setContextClassLoader(xx)
方法給一個(gè)線程設(shè)置上下文類加載器,在該線程后續(xù)執(zhí)行過程中就能通過Thread.currentThread().getContextClassLoader()
獲取該類加載器使用。
- 該加載器的出現(xiàn)就是
為了方便破壞雙親委派
,如果沒有手動(dòng)設(shè)置上下文類加載器,線程將繼承其父線程的上下文類加載器
,默認(rèn)
線程的上下文類加載器是系統(tǒng)類加載器(AppClassLoader)
, 在線程中運(yùn)行的代碼可以通過該
類加載器來加載類和資源
七.如何打破雙親委派模型
雙親委派模型是Java設(shè)計(jì)者推薦給開發(fā)者們的默認(rèn)類加載器實(shí)現(xiàn)方式
。這個(gè)委派和加載順序是非強(qiáng)制性,可破壞的。主要有2種方式
-
自定義類加載器,
繼承ClassLoader
,重寫findClass()
,然后再重寫loadClass()
改變雙親委派的類加載順序。 -
通過
線程上下文類加載器
的傳遞性,讓父類加載器中調(diào)用子類加載器的加載動(dòng)作。
下面使用方式1,基于代碼 4.自定義類加載器(Custom Class Loader)重寫loadClass()
即可
public class MyClassLoader extends ClassLoader {//......................省略其他代碼......................//重點(diǎn): 重寫loadClass()改變雙親委派的類加載順序@Overridepublic Class<?> loadClass(String name) throws ClassNotFoundException{//1.破壞雙親委派的位置:自定義類加載機(jī)制先委派給ExtClassLoader加載,ExtClassLoader再委派給BootstrapClassLoader,如果都加載不了,然后自定義類加載器加載,自定義類加載器加載不了才交給SystemClassLoaderClassLoader classLoader = getSystemClassLoader();while (classLoader.getParent() != null) {classLoader = classLoader.getParent();}Class<?> clazz = null;try {clazz = classLoader.loadClass(name);} catch (ClassNotFoundException e) {// Ignore}if (clazz != null) {return clazz;}//2.自己加載clazz = this.findClass(name);if (clazz != null) {return clazz;}// 3.自己加載不了,再調(diào)用父類loadClass,保持雙親委派模式return super.loadClass(name);}//......................省略其他代碼......................public static void main(String[] args) throws IllegalAccessException, InstantiationException, ClassNotFoundException {String path = "E:/04_resource_study/java_base_demo/target/classes/";//創(chuàng)建自定義類加載器對象MyClassLoader classLoader = new MyClassLoader(path);System.out.println("MyDamageClassLoader的父加載器:" + classLoader.getParent());//返回加載的class對象Class<?> clazz = classLoader.findClass("com.demo.classload.ClassLoaderTest");//調(diào)用類的構(gòu)造方法創(chuàng)建對象Object o = clazz.newInstance();//輸出創(chuàng)建的對象System.out.println("創(chuàng)建的對象:"+o);//輸出當(dāng)前類加載器System.out.println("ClassLoaderTest當(dāng)前類加載器:"+clazz.getClassLoader());//輸出當(dāng)前類加載器的父類System.out.println("ClassLoaderTest當(dāng)前類加載器的父類:"+clazz.getClassLoader().getParent());//輸出當(dāng)前類加載器的父類的父類System.out.println("ClassLoaderTest當(dāng)前類加載器的父類的父類:"+clazz.getClassLoader().getParent().getParent());//輸出當(dāng)前類加載器的父類的父類的父類System.out.println("ClassLoaderTest當(dāng)前類加載器的父類的父類的父類:"+clazz.getClassLoader().getParent().getParent().getParent());/*執(zhí)行結(jié)果:MyDamageClassLoader的父加載器:sun.misc.Launcher$AppClassLoader@18b4aac2創(chuàng)建的對象:com.demo.classload.ClassLoaderTest@2e5d6d97ClassLoaderTest當(dāng)前類加載器:com.demo.classload.MyDamageClassLoader@685f4c2eClassLoaderTest當(dāng)前類加載器的父類:sun.misc.Launcher$AppClassLoader@18b4aac2ClassLoaderTest當(dāng)前類加載器的父類的父類:sun.misc.Launcher$ExtClassLoader@238e0d81ClassLoaderTest當(dāng)前類加載器的父類的父類的父類:nullnull*/}
}
重寫loadClass()后,
類加載時(shí)先委派給ExtClassLoader加載,ExtClassLoader再委派給BootstrapClassLoader,如果都加載不了,然后自定義類加載器加載,自定義類加載器加載不了才交給SystemClassLoader。
為什么不能直接讓自定義類加載器加載呢?
-
不能!雙親委派的破壞只能發(fā)生在
SystemClassLoader及其以下的加載委派順序,ExtClassLoader上面的雙親委派是不能破壞的!
- 因?yàn)槿魏晤惗际抢^承自頂級(jí)類
java.lang.Object
,而加載一個(gè)類時(shí),也會(huì)加載繼承的類
,如果該類中還引用了其他類,則按需加載,且類加載器都是加載當(dāng)前類的類加載器
。
- 因?yàn)槿魏晤惗际抢^承自頂級(jí)類
-
如ClassLoaderTest類只
隱式繼承了Object
,自定義MyDamageClassLoader 加載了ClassLoaderTest,也會(huì)加載Object。
如果loadClass()
直接調(diào)用MyDamageClassLoader的findClass()
會(huì)報(bào)錯(cuò)java.lang.SecurityException: Prohibited package name: java.lang
八.打破雙親委派模型的常見場景
目前比較常見的場景主要有:
-
線程上下文類加載器(
Thread.currentThread().setContextClassLoader(xx)、Thread.currentThread().getContextClassLoader()
),如:JDBC 使用線程上下文類加載器加載 Driver 實(shí)現(xiàn)類 -
Tomcat 的多 Web 應(yīng)用程序
-
OSGI 實(shí)現(xiàn)模塊化熱部署
九.熱部署類加載器
熱部署類加載器: 即利用同一個(gè)class文件不同的類加載器在內(nèi)存創(chuàng)建出兩個(gè)不同的class對象
-
JVM在加載類之前會(huì)檢測請求類是否已加載過(即在
loadClass()
方法中調(diào)用findLoadedClass()
方法),如果被加載過,則直接從緩存
獲取,不會(huì)重新加載。 -
前面的自定義類加載器MyClassLoader通過直接調(diào)用
findClass()
方法已具備這個(gè)熱加載功能- 為什么要調(diào)用
findClass()
來實(shí)現(xiàn)熱加載,而不是loadClass()
呢?ClassLoader
中默認(rèn)實(shí)現(xiàn)的loadClass()
方法中調(diào)用findLoadedClass()
方法進(jìn)行了檢測是否已被加載
,因此直接調(diào)用findClass()
方法就可以繞過重用class緩存問題, 當(dāng)然也可以重寫loadClass()方法
,但強(qiáng)烈不建議這么干
- 為什么要調(diào)用
-
注意; 同一個(gè)class文件最多只能被同一個(gè)類加載器的實(shí)例調(diào)用
findClass()
加載一次,多次加載將報(bào)錯(cuò)
, 因此必須讓同一類加載器的不同實(shí)例加載同一個(gè)class文件
,以實(shí)現(xiàn)所謂的熱部署。
public class FileClassLoader extends ClassLoader {/*** 根路徑*/private String rootDir;public FileClassLoader(String rootDir) {this.rootDir = rootDir;}/*** 重寫findClass邏輯** @param className* @return* @throws ClassNotFoundException*/@Overrideprotected Class<?> findClass(String className) throws ClassNotFoundException {//獲取類的class字節(jié)數(shù)組,用于存儲(chǔ)class文件的字節(jié)流byte[] bytes = null;try {//獲取class文件的字節(jié)流bytes = getBytes(className);} catch (Exception e) {e.printStackTrace();}//如果文件為空if (bytes == null) {throw new ClassNotFoundException();}//直接生成class對象return defineClass(className, bytes, 0, bytes.length);}//獲取class文件的字節(jié)流private byte[] getBytes(String className) throws IOException {// 讀取類文件的字節(jié)String path = classNameToPath(className);//緩沖區(qū)byte[] buffer = new byte[1024];//輸入流InputStream fis = new FileInputStream(new File(path));//相當(dāng)于一個(gè)緩存區(qū),動(dòng)態(tài)擴(kuò)容,也就是隨著寫入字節(jié)的增加自動(dòng)擴(kuò)容ByteArrayOutputStream baos = new ByteArrayOutputStream();//循環(huán)將輸入流中的所有數(shù)據(jù)寫入到緩存區(qū)中int bytesNumRead = 0;// 讀取類文件的字節(jié)碼while ((bytesNumRead = fis.read(buffer)) != -1) {baos.write(buffer, 0, bytesNumRead);}baos.flush();baos.close();return baos.toByteArray();}/*** 類文件的完全路徑* @param className* @return*/private String classNameToPath(String className) {//拼接class文件路徑 replace(".",File.separator) 表示將全類名中的"."替換為當(dāng)前系統(tǒng)的分隔符,File.separator返回當(dāng)前系統(tǒng)的分隔符return rootDir + File.separatorChar + className.replace('.', File.separatorChar) + ".class";}
}
測試方法
public static void main(String[] args) {//類所在包的絕對路徑String rootDir = "E:/04_resource_study/java_base_demo/target/classes/";//類的全限定名String classPath = "com.demo.classload.ClassLoaderTest";//創(chuàng)建自定義文件類加載器FileClassLoader loaderA = new FileClassLoader(rootDir);FileClassLoader loaderB = new FileClassLoader(rootDir);try {//加載指定的class文件,調(diào)用loadClass()Class<?> objectL1 = loaderA.loadClass(classPath);Class<?> objectL2 = loaderB.loadClass(classPath);System.out.println("loadClass->object1:" + objectL1.hashCode());System.out.println("loadClass->object2:" + objectL2.hashCode());Class<?> object3 = loaderA.loadClass(classPath);Class<?> object4 = loaderB.loadClass(classPath);System.out.println("loadClass->obj3:" + object3.hashCode());System.out.println("loadClass->obj4:" + object4.hashCode());Class<?> object5 = loaderA.findClass(classPath);Class<?> object6 = loaderB.findClass(classPath);System.out.println("findClass->object5:" + object5.hashCode());System.out.println("findClass->object6:" + object6.hashCode());//執(zhí)行到這里會(huì)報(bào)錯(cuò): java.lang.LinkageError: loader (instance of com/demo/classload/FileClassLoader): attempted duplicate class definition for name: "com/demo/classload/ClassLoaderTest"//原因: 同一個(gè)class文件最多只能被同一個(gè)類加載器的實(shí)例調(diào)用`findClass()`加載一次,多次加載將`報(bào)錯(cuò)`, 因此必須讓`同一類加載器的不同實(shí)例加載同一個(gè)class文件`,以實(shí)現(xiàn)所謂的熱部署。// Class<?> object7 = loaderA.findClass(classPath);// Class<?> object8 = loaderB.findClass(classPath);FileClassLoader loaderC= new FileClassLoader(rootDir);FileClassLoader loaderD = new FileClassLoader(rootDir);Class<?> object7 = loaderC.findClass(classPath);Class<?> object8 = loaderD.findClass(classPath);System.out.println("findClass->object7:" + object7.hashCode());System.out.println("findClass->object8:" + object8.hashCode());} catch (Exception e) {e.printStackTrace();}// 執(zhí)行結(jié)果// loadClass->object1:1751075886// loadClass->object2:1751075886// loadClass->obj3:1751075886// loadClass->obj4:1751075886// findClass->object5:930990596// findClass->object6:1921595561// findClass->object7:87285178// findClass->object8:610998173}
結(jié)論:
- 同一個(gè)類加載器不同實(shí)例調(diào)用
loadClass()
加載class,每次返回的都是同一個(gè)class實(shí)例 - 同一個(gè)類加載器實(shí)例調(diào)用
findClass()
加載class,第2次調(diào)用會(huì)拋出異常java.lang.LinkageError
, 因此每次熱部署都要new一個(gè)新類加載器實(shí)例來調(diào)用findClass()
加載class,以返回不同的class實(shí)例
熱部署加載監(jiān)控線程
- 如何測試: 熱部署監(jiān)控線程啟動(dòng)時(shí),只需要我們調(diào)整
type()
方法中,println打印內(nèi)容,然后將重新編譯后的Pay.class
文件放到指定類加載路徑中就行了
package com.demo.classload;
public class Pay {public void type() {System.out.println("微信支付");}
}
@Slf4j
public class HotDeploymentTest {public static void main(String[] args) throws InterruptedException {//記錄文件上次修改時(shí)間AtomicReference<Long> lastModified = new AtomicReference<>(0L);new Thread(() -> {while (true) {//類文件所在包根路徑String rootDir = "E:/04_resource_study/java_base_demo/target/classes/";//類文件所在包絕對路徑String classAbsolutePath = rootDir + "/com/demo/classload/Pay.class";//類的全限定名String classSourcePath = "com.demo.classload.Pay";try {File file = new File(classAbsolutePath);//文件不存在,線程休眠if (!file.exists()) {int sleep = 2000;log.info("文件{}不存在,休眠{}ms", file.getAbsolutePath(), sleep);TimeUnit.MILLISECONDS.sleep(sleep);continue;}//獲取文件的上次修改時(shí)間Long modified = file.lastModified();//如果文件上次修改時(shí)間發(fā)生改變,使用類加載器重載文件if (modified > lastModified.get()) {//當(dāng)前文件的文件上次修改時(shí)間lastModified.set(modified);log.info("文件{}存在且發(fā)生改變,lastModified:{},modified:{},開始熱部署", file.getAbsolutePath(), lastModified.get(), modified);//創(chuàng)建自定義文件類加載器FileClassLoader loader = new FileClassLoader(rootDir);//加載指定的class文件,直接調(diào)用findClass(),繞過檢測機(jī)制,創(chuàng)建不同class對象。Class<?> clazz = loader.findClass(classSourcePath);//調(diào)用類的構(gòu)造方法創(chuàng)建對象Object obj = clazz.newInstance();//獲取該對象的方法String methodName = "type";Method method = clazz.getMethod(methodName, null);//執(zhí)行方法method.invoke(obj, null);}//沒有改變,線程休眠else {log.info("文件{}存在未發(fā)生改變,lastModified:{},modified:{}", file.getAbsolutePath(), lastModified.get(), modified);TimeUnit.MILLISECONDS.sleep(5000);}} catch (Exception e) {e.printStackTrace();}}}).start();}
}
執(zhí)行結(jié)果