Java類加載器ClassLoader詳解
以下文章都是基于JDK1.8環(huán)境
一、類的加載過程
JDK8引用的都是jar包
JDK11引用的都是model
我們編寫的".java"文件需要通過javac編譯成".class"文件,而程序運行時,JVM會把".class"文件加載到內(nèi)存中,并創(chuàng)建對應(yīng)的class對象,這個過程被稱為類的加載。
簡單來說:將class文件讀入內(nèi)存,并為之創(chuàng)建一個Class對象。
JVM運行的是class文件,不單單是java語言,無論哪種語言編寫的,只要文件是.class類型的,就能夠在JVM上運行。
學習類加載器的目的:使用類加載器可以讓我們得代碼7*24小時,不間斷地運行。即修改代碼后無需重啟就能生效,類似于熱部署。
二、默認的類加載器
JVM通過類加載器把“.class”文件加載到內(nèi)存中,默認有 3 個類加載器,分別是:
- Bootstrap ClassLoader 啟動類加載器
- ExtClassLoader 擴展類加載器
- AppClassLoader 系統(tǒng)類加載器(應(yīng)用類加載器)
三個類加載器各有不同的作用
(一)Bootstrap ClassLoader1.基本介紹
作用:加載JDK核心類庫(String,Integer,Long,ArrayList等等)
方式:加載某個類時,在指定的路徑中(Jar包或文件夾)搜索這個類,如果搜到就加載,如果沒搜到,報:ClassNotFoundException
搜索路徑:由 sun.boot.class.path 所指定的,比如:%JRE_HOME%\jre\lib下的rt.jar、resources.jar、charsets.jar等,也就是JDK核心類庫,其中 rt.jar 里面就存放著常用的 JAVA API
代碼演示:
public static void main(String[] args) { // 輸出String類是被哪個類加載器,加載到內(nèi)存中 System.out.println(String.class.getClassLoader()); // 獲取系統(tǒng)配置 // bootstrap生效的時候會去sun.boot.class.path路徑下找對應(yīng)的類 String paths = System.getProperty("sun.boot.class.path"); // windows下使用;分隔 // linux使用:分隔 String[] arr = paths.split(";"); for (String s : arr) { System.out.println(s); } }
返回結(jié)果:
為什么String.class.getClassLoader() 返回null?
原因是:Bootstrap ClassLoader是由C/C++編寫的,是虛擬機的一部分,并不是JAVA中的類,所以無法在 Java 代碼中獲取它的引用,因此返回 null。
因此:如果一個類(System.out.println(String.class.getClassLoader());)輸出為null,說明該類是被Bootstrap ClassLoader加載的。前提是基于JDK1.8環(huán)境。
Bootstrap ClassLoader加載String類會去下面這些類中尋找:
最后一行不是默認路徑,是IDEA默認添加的,紅框部分才是默認加載路徑。
String類在rt包下,按照上圖的順序依次向下找,在rt包中找到后,就不會繼續(xù)向下尋找了。
2.自定義路徑
可通過 -Xbootclasspath 參數(shù)修改 Bootstrap ClassLoader 的搜索路徑
用法 | 含義 | 備注 |
-Xbootclasspath:路徑 | 指定的路徑會完全取代jdk核心的搜索路徑 | 堅決不要用 |
-Xbootclasspath/a:路徑 | 指定的路徑會在jdk核心類后搜索 | 可用 |
-Xbootclasspath/p:路徑 | 指定的路徑會在jdk核心類前搜索 | 可用,不建議使用 |
注意:如果配置多個路徑,linux/unix下用“:”分割,windows下用“;”分割。
代碼演示:在 pom.xml 中添加 commons-io,同時也要把這個jar包放到D:\test 中
<dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.8.0</version> </dependency>
添加JVM參數(shù),指定尋找的包路徑在jdk核心類后搜索
-Xbootclasspath/a:D:\test\commons-io-2.8.0.jar
去這個包下尋找ByteOrderMark這個類
修改代碼:
public static void main(String[] args) { // 輸出String類是被哪個類加載器,加載到內(nèi)存中 // System.out.println(String.class.getClassLoader()); System.out.println(ByteOrderMark.class.getClassLoader()); // 獲取系統(tǒng)配置 // bootstrap生效的時候會去sun.boot.class.path路徑下找對應(yīng)的類 String paths = System.getProperty("sun.boot.class.path"); // windows下使用;分隔 // linux使用:分隔 String[] arr = paths.split(";"); for (String s : arr) { System.out.println(s); } }
運行結(jié)果:
Bootstrap ClassLoader在JDK核心類中找不到就會去指定的路徑下尋找。
(二)ExtClassLoader
1.基本使用
擴展類加載器,加載擴展類庫,搜索路徑由-Djava.ext.dirs指定,比如:%JRE_HOME%\jre\lib\ext目錄下的jar包和class文件。即ExtClassLoader加載類的時候會去下面的路徑尋找,找不到就會報錯。
代碼演示:
public static void main(String[] args) { // 輸出String類是被哪個類加載器,加載到內(nèi)存中 // System.out.println(String.class.getClassLoader()); // System.out.println(ByteOrderMark.class.getClassLoader()); System.out.println(DNSNameService.class.getClassLoader()); // 獲取系統(tǒng)配置 // 輸出 ExtClassLoader 掃描的路徑,注意:輸出的是文件夾 String paths = System.getProperty("java.ext.dirs"); // windows下使用;分隔 // linux使用:分隔 String[] arr = paths.split(";"); for (String s : arr) { System.out.println(s); } }
運行結(jié)果:
sun.misc.Launcher$ExtClassLoader@383534aa
有$符號表示ExtClassLoader是Launcher這個類的內(nèi)部類。只要出現(xiàn)$就說明后面是前面的內(nèi)部類
搜索ExtClassLoader后,發(fā)現(xiàn)ExtClassLoader確實是個內(nèi)部類,并且還是靜態(tài)的。
2.配置指定路徑
-Djava.ext.dirs=D:\test
public static void main(String[] args) { // 輸出String類是被哪個類加載器,加載到內(nèi)存中 // System.out.println(String.class.getClassLoader()); System.out.println(ByteOrderMark.class.getClassLoader()); // System.out.println(DNSNameService.class.getClassLoader()); // 獲取系統(tǒng)配置 // 輸出 ExtClassLoader 掃描的路徑,注意:輸出的是文件夾 String paths = System.getProperty("java.ext.dirs"); // windows下使用;分隔 // linux使用:分隔 String[] arr = paths.split(";"); for (String s : arr) { System.out.println(s); } }
代碼中表示,讓ExtClassLoader去D:\test文件夾下尋找ByteOrderMark這個類。
運行結(jié)果:
自定義路徑會把默認路徑覆蓋掉,所以這種方法不建議使用,了解即可。
(三)AppClassLoader
AppClassLoader也叫SystemClassLoader(系統(tǒng)類加載器),搜索路徑由java.class.path(CLASSPATH) 指定。加載項目中自己寫的類 和 第三方依賴包。
public static void main(String[] args) { // 輸出String類是被哪個類加載器,加載到內(nèi)存中 // System.out.println(String.class.getClassLoader()); // System.out.println(ByteOrderMark.class.getClassLoader()); // System.out.println(DNSNameService.class.getClassLoader()); System.out.println(Main.class.getClassLoader()); // 獲取系統(tǒng)配置 // 輸出 ExtClassLoader 掃描的路徑,注意:輸出的是文件夾 String paths = System.getProperty("java.class.path"); // windows下使用;分隔 // linux使用:分隔 String[] arr = paths.split(";"); for (String s : arr) { System.out.println(s); } }
運行結(jié)果:
最下面的兩行是IDEA自帶的,無需理會
由此可以看出,IDEA在運行java代碼時,主動給我們添加了很多路徑(JDK核心類庫、擴展類庫、自己寫的類和第三方依賴包到classpath中),所以才會打印如此多的路徑。如下圖。
(四)類加載器的初始化
1.源碼跟蹤
從上面的輸出可以發(fā)現(xiàn) ExtClassLoader、AppClassLoader都是 Java 對象,接下來看一下它們是如何創(chuàng)建的。
這兩個對象的生成都是在 Launcher 中完成的:Launcher類是 java 程序的入口,在啟動 java 應(yīng)用的時候會首先創(chuàng)建Launcher類的對象,創(chuàng)建Launcher類的時候會創(chuàng)建ExtClassLoader、AppClassLoader。
Launcher類太過于底層,所以無法打斷點,構(gòu)造方法如下:
首先,創(chuàng)建 ExtClassLoader
ExtClassLoader對象創(chuàng)建成功后,將器傳入AppClassLoader中
點進這個getAppClassLoader靜態(tài)方法:
super一直點到最上層,在這里,parent參數(shù)就是傳入的 ExtClassLoader。
由此可以看出:ExtClassLoader是AppClassLoader的父類加載器。
2.ExtClassLoader和AppClassLoader的關(guān)系結(jié)論——父類加載器
在 Java 中,AppClassLoader 和 ExtClassLoader 都是由 sun.misc.Launcher 類創(chuàng)建的。盡管它們的名字中包含“ClassLoader”,但它們并不是通過繼承關(guān)系來定義父子關(guān)系的,而是通過設(shè)置父加載器的方式來實現(xiàn)的。這意味著 AppClassLoader 實際上是將 ExtClassLoader 作為其父類加載器,而不是通過類繼承的方式。
只有類之間才會存在繼承關(guān)系,我們這里說的是AppClassLoader 和 ExtClassLoader對象。
在 Java 類加載器的上下文中,“父類加載器”這個術(shù)語并不意味著類加載器之間存在繼承關(guān)系(即它們不是通過 extends 關(guān)鍵字定義的父子類關(guān)系),而是指類加載器之間的委派關(guān)系。具體來說,AppClassLoader 將 ExtClassLoader 作為其父類加載器,指的是當 AppClassLoader 需要加載某個類時,它首先會請求 ExtClassLoader(它的父類加載器)嘗試加載該類。這種機制是基于“雙親委派模型”的。
因此,AppClassLoader的父類是URLClassLoader;父類加載器是ExtClassLoader。
通過debug可以看出來,AppClassLoader的parent屬性是ExtClassLoader,ExtClassLoader的parent屬性是BootstrapClassLoader,前文提過,顯示null,就說明是BootstrapClassLoader。
三、雙親委派模式
(一)概念介紹
雙親委派模式是Java類加載機制的核心原理,用于規(guī)范類加載器之間的協(xié)作方式。其核心思想是:當一個類加載器收到類加載請求時,不會直接自己加載,而是將請求委派給父類加載器,只有當父類加載器無法加載時,才會由當前類加載器自己嘗試加載。
如果父加載器可以完成加載任務(wù),就成功返回;倘若父加載器無法完成此加載任務(wù),子加載器才會嘗試自己去加載。
(二)源碼解釋
加載一個類,一定先從 Launcher.AppClassLoader 的 loadClass 方法開始。
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { // 1. 檢查是否已加載 Class<?> c = findLoadedClass(name); if (c == null) { try { // 2. 委派給父類加載器(遞歸向上) if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // 父類加載器未找到,繼續(xù)執(zhí)行 } if (c == null) { // 3. 自己嘗試加載 c = findClass(name); } } if (resolve) { resolveClass(c); } return c; }
(三)雙親委派模式的核心優(yōu)勢
- 安全性:確保核心類(如java.lang.Object)只能由啟動類加載器加載,防止用戶自定義類覆蓋JDK核心類,避免惡意代碼篡改。保證java核心api不會被隨意替換,專業(yè)說法:沙箱安全機制。
- 唯一性:同一名稱的類只會被加載一次(由首個能加載它的類加載器完成),確保類實例的兼容性。
- 層級隔離:不同類加載器加載的類空間隔離,例如Tomcat的多個Web應(yīng)用可通過不同類加載器隔離類版本。
代碼示例:
自定義一個Integer類:
package java.lang; public class Integer { public static void main(String[] args) { System.out.println("運行自定義的Integer類......"); } }
運行結(jié)果:
原因是BootStrap ClassLoader已經(jīng)在核心類庫中找到j(luò)ava.lang.Integer類了,所以不會再加載自定義的java.lang.Integer類了,這充分體現(xiàn)了JDK核心類不會被覆蓋的優(yōu)勢。
四、動態(tài)加載
(一)URLClassLoader
URLClassLoader是Java中用于從指定的URL(統(tǒng)一資源定位符)加載類和資源的類加載器,屬于java.lang.ClassLoader的子類,也是ExtClassLoader和AppClassLoader的父類。它允許從網(wǎng)絡(luò)或本地文件系統(tǒng)中的目錄、JAR 文件等位置動態(tài)加載類,廣泛應(yīng)用于插件化開發(fā)、熱部署、動態(tài)模塊加載等場景。
常用的構(gòu)造方法:
- URLClassLoader(URL[] urls, ClassLoader parent):使用指定的父加載器創(chuàng)建對象,從指定的urls路徑來查詢、并加載類。
- URLClassLoader(URL[] urls):使用默認的父加載器(AppClassLoader)創(chuàng)建一個ClassLoader對象,從指定的urls路徑來查詢、并加載類。
(二)實戰(zhàn)
新建一個普通的java項目parse-excel-demo,并打成jar包。
在另一個項目classloader-demo中編寫下面的代碼,即可使用上面的方法
public class RunDemo { public static void main(String[] args) throws Exception { File file = new File( "G:\\develop\\workspace\\four\\" + "newSmProjects\\parse-excel-demo\\target\\" + "parse-excel-demo-1.0-SNAPSHOT.jar"); URL[] urls = {file.toURI().toURL()}; URLClassLoader myUrlClassLoader = new URLClassLoader(urls); Class<?> parseExcel = myUrlClassLoader.loadClass("com.test.excel.ParseExcel"); Object obj = parseExcel.newInstance(); Method parse = parseExcel.getMethod("parse"); parse.invoke(obj); } }
運行結(jié)果:
即使被引用的jar包內(nèi)容被修改,只要路徑正確,RunDemo所在的項目都不需要重啟就能運行:
RunDemo運行結(jié)果:
(三)依賴問題
有些時候,parse-excel-demo中可能引用一些第三方庫,比如:jackson-core-2.11.0.jar
<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> <version>2.11.0</version> </dependency>
并在代碼中使用:
package com.test.excel; import com.fasterxml.jackson.core.JsonFactory; public class ParseExcel { public void parse() { System.out.println("開始解析Excel......"); JsonFactory jsonFactory = new JsonFactory(); System.out.println("運行其getFormatGeneratorFeatures方法:" + jsonFactory.getFormatGeneratorFeatures()); } }
修改后重新對 parse 打包,然后運行clasterloader-demo中的代碼,結(jié)果:
問題分析:
myUrlClassLoader的父類加載器是
場景 | 父類加載器 | 原因 |
---|---|---|
默認構(gòu)造URLClassLoader(urls) | AppClassLoader | 默認使用系統(tǒng)類加載器作為父類加載器。 |
顯式指定父類加載器 | 任意(如ExtClassLoader) | 可通過構(gòu)造方法URLClassLoader(urls,parent)自定義。 |
繼承關(guān)系 | URLClassLoader是父類 | AppClassLoader和ExtClassLoader是URLClassLoader的子類。 |
根據(jù)雙親委派模式,myUrlClassLoader加載時parseExcel的時候會去加載JsonFactory,加載JsonFactory會交給父類加載器AppClassLoader,AppClassLoader無法加載就會交給其父類加載器ExtClassLoader,ExtClassLoader無法加載又交給自己的父類加載器BootStrap ClassLoader,都找不到才會報錯。
因此,當我們開發(fā)中碰到ClassNotFoundException的排錯方法要考慮類的加載過程。
解決方案:完善代碼,將JsonFactory所屬的包放在myUrlClassLoader的掃描路徑下:
public class RunDemo { public static void main(String[] args) throws Exception { File file = new File( "G:\\develop\\workspace\\four\\" + "newSmProjects\\parse-excel-demo\\target\\" + "parse-excel-demo-1.0-SNAPSHOT.jar"); File file2 = new File( "D:\\apache-maven-3.8.1\\repository\\" + "com\\fasterxml\\jackson\\core\\jackson-core\\2.11.0" + "\\jackson-core-2.11.0.jar"); URL[] urls = {file.toURI().toURL(), file2.toURI().toURL()}; URLClassLoader myUrlClassLoader = new URLClassLoader(urls); Class<?> parseExcel = myUrlClassLoader.loadClass("com.test.excel.ParseExcel"); Object obj = parseExcel.newInstance(); Method parse = parseExcel.getMethod("parse"); parse.invoke(obj); } }
也可以將依賴放入classloader-demo中。AppClassLoader可以成功加載第三方依賴包。
(四)版本沖突問題
我們降低classloader-demo中的依賴版本:
再次運行會報錯:
即使將2.11.0版本放到搜索路徑中還是會報錯,因為AppClassLoader已經(jīng)加載完畢了,myUrlClassLoader就不會再加載了。
而項目中已有的依賴也不能更改,那么如何解決呢?
讓程序同時運行兩個版本的jsonFactory即可。
public class RunDemo { public static void main(String[] args) throws Exception { File file = new File( "G:\\develop\\workspace\\four\\" + "newSmProjects\\parse-excel-demo\\target\\" + "parse-excel-demo-1.0-SNAPSHOT.jar"); File file2 = new File( "D:\\apache-maven-3.8.1\\repository\\" + "com\\fasterxml\\jackson\\core\\jackson-core\\2.11.0" + "\\jackson-core-2.11.0.jar"); URL[] urls = {file.toURI().toURL(),file2.toURI().toURL()}; // 創(chuàng)建自定義類加載器 // 這時候myUrlClassLoader的parent是ExtClassLoader URLClassLoader myUrlClassLoader = new URLClassLoader(urls,RunDemo.class.getClassLoader().getParent()); Class<?> parseExcel = myUrlClassLoader.loadClass("com.test.excel.ParseExcel"); Object obj = parseExcel.newInstance(); Method parse = parseExcel.getMethod("parse"); parse.invoke(obj); } }
總結(jié)
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
springbooot整合dynamic?datasource數(shù)據(jù)庫密碼加密方式
這篇文章主要介紹了springbooot整合dynamic?datasource?數(shù)據(jù)庫密碼加密方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-01-01使用lombok@Data存在extends時需要注意的問題
在Java編程中,正確實現(xiàn)equals方法是保證對象比較一致性的關(guān)鍵,使用instanceof檢查類型可能導致違反對稱性原則,即當子類和父類都重寫equals時可能出現(xiàn)a.equals(b)不等于b.equals(a)的情況,Lombok的@EqualsAndHashCode注解可以通過callSuper=true參數(shù)2024-10-10Java中增強for循環(huán)在一維數(shù)組和二維數(shù)組中的使用方法
下面小編就為大家?guī)硪黄狫ava中增強for循環(huán)在一維數(shù)組和二維數(shù)組中的使用方法。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2016-10-10JDBC中PreparedStatement詳解以及應(yīng)用場景實例介紹
PreparedStatement對象代表的是一個預編譯的SQL語句,用它提供的setter方法可以傳入查詢的變量,這篇文章主要給大家介紹了關(guān)于JDBC中PreparedStatement詳解以及應(yīng)用場景實例介紹的相關(guān)資料,需要的朋友可以參考下2024-02-02httpclient ConnectionHolder連接池連接保持源碼解析
這篇文章主要為大家介紹了httpclient ConnectionHolder連接池連接保持源碼解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-11-11