淺談JAVA 類加載器
類加載機(jī)制
類加載器負(fù)責(zé)加載所有的類,系統(tǒng)為所有被載入內(nèi)存中的類生成一個 java.lang.Class 實例。一旦一個類被載入 JVM 中,同個類就不會被再次載入了?,F(xiàn)在的問題是,怎么樣才算“同一個類”?
正如一個對象有一個唯一的標(biāo)識一樣,一個載入 JVM 中的類也有一個唯一的標(biāo)識。在 Java 中,一個類用其全限定類名(包括包名和類名)作為標(biāo)識:但在 JVM 中,一個類用其全限定類名和其類加載器作為唯一標(biāo)識。例如,如果在 pg 的包中有一個名為 Person 的類,被類加載器 ClassLoader 的實例 k1 負(fù)責(zé)加載,則該 Person 類對應(yīng)的 Class 對象在 JVM 中表示為(Person、pg、k1)。這意味著兩個類加載器加載的同名類:(Person、pg、k1)和(Person、pg、k12)是不同的,它們所加載的類也是完全不同、互不兼容的。
當(dāng) JVM 啟動時,會形成由三個類加載器組成的初始類加載器層次結(jié)構(gòu)。
- Bootstrap ClassLoader:根類加載器。
- Extension ClassLoader:擴(kuò)展類加載器。
- System ClassLoader:系統(tǒng)類加載器。
Bootstrap ClassLoader 被稱為引導(dǎo)(也稱為原始或根)類加載器,它負(fù)責(zé)加載 Java 的核心類。在Sun 的 JVM 中,當(dāng)執(zhí)行 java.exe 命令時,使用 -Xbootclasspath 或 -D 選項指定 sun.boot.class.path 系統(tǒng)屬性值可以指定加載附加的類。
JVM的類加載機(jī)制主要有如下三種。
- 全盤負(fù)責(zé)。所謂全盤負(fù)責(zé),就是當(dāng)一個類加載器負(fù)責(zé)加載某個 Class 時,該 Class 所依賴的和引用的其他 Class 也將由該類加載器負(fù)責(zé)載入,除非顯式使用另外一個類加載器來載入。
- 父類委托。所謂父類委托,則是先讓 parent(父)類加載器試圖加載該 Class,只有在父類加載器無法加載該類時才嘗試從自己的類路徑中加載該類。
- 緩存機(jī)制。緩存機(jī)制將會保證所有加載過的 Class 都會被緩存,當(dāng)程序中需要使用某個 Class 時,類加載器先從緩存區(qū)中搜尋該 Class,只有當(dāng)緩存區(qū)中不存在該 Class 對象時,系統(tǒng)才會讀取該類對應(yīng)的二進(jìn)制數(shù)據(jù),并將其轉(zhuǎn)換成 Class 對象,存入緩存區(qū)中。這就是為什么修改了 Class 后,必須重新啟動 JVM,程序所做的修改才會生效的原因。
除了可以使用 Java 提供的類加載器之外,開發(fā)者也可以實現(xiàn)自己的類加載器,自定義的類加載器通過繼承 ClassLoader 來實現(xiàn)。JVM 中這4種類加載器的層次結(jié)構(gòu)如下圖所示。
注意:類加載器之間的父子關(guān)系并不是類繼承上的父子關(guān)系,這里的父子關(guān)系是類加載器實例之間的關(guān)系
下面程序示范了訪問 JVM 的類加載器。
public class ClassLoaderPropTest { public static void main(String[] args) throws IOException { // 獲取系統(tǒng)類加載器 ClassLoader systemLoader = ClassLoader.getSystemClassLoader(); System.out.println("系統(tǒng)類加載器:" + systemLoader); /* * 獲取系統(tǒng)類加載器的加載路徑——通常由CLASSPATH環(huán)境變量指定 如果操作系統(tǒng)沒有指定CLASSPATH環(huán)境變量,默認(rèn)以當(dāng)前路徑作為 * 系統(tǒng)類加載器的加載路徑 */ Enumeration<URL> em1 = systemLoader.getResources(""); while (em1.hasMoreElements()) { System.out.println(em1.nextElement()); } // 獲取系統(tǒng)類加載器的父類加載器:得到擴(kuò)展類加載器 ClassLoader extensionLader = systemLoader.getParent(); System.out.println("擴(kuò)展類加載器:" + extensionLader); System.out.println("擴(kuò)展類加載器的加載路徑:" + System.getProperty("java.ext.dirs")); System.out.println("擴(kuò)展類加載器的parent: " + extensionLader.getParent()); } }
運(yùn)行上面的程序,會看到如下運(yùn)行結(jié)果
系統(tǒng)類加載器:sun.misc.Launcher$AppClassLoader@73d16e93
file:/F:/EclipseProjects/demo/bin/
擴(kuò)展類加載器:sun.misc.Launcher$ExtClassLoader@15db9742
擴(kuò)展類加載器的加載路徑:C:\Program Files\Java\jre1.8.0_181\lib\ext;C:\Windows\Sun\Java\lib\ext
擴(kuò)展類加載器的parent: null
從上面運(yùn)行結(jié)果可以看出,系統(tǒng)類加載器的加載路徑是程序運(yùn)行的當(dāng)前路徑,擴(kuò)展類加載器的加載路徑是null(與 Java8 有區(qū)別),但此處看到擴(kuò)展類加載器的父加載器是null,并不是根類加載器。這是因為根類加載器并沒有繼承 ClassLoader 抽象類,所以擴(kuò)展類加載器的 getParent() 方法返回null。但實際上,擴(kuò)展類加載器的父類加載器是根類加載器,只是根類加載器并不是 Java 實現(xiàn)的。
從運(yùn)行結(jié)果可以看出,系統(tǒng)類加載器是 AppClassLoader 的實例,擴(kuò)展類加載器 ExtClassLoader 的實例。實際上,這兩個類都是 URLClassLoader 類的實例。
注意:JVM 的根類加載器并不是 Java 實現(xiàn)的,而且由于程序通常無須訪問根類加載器,因此訪問擴(kuò)展類加載器的父類加載器時返回null。
類加載器加載 Class 大致要經(jīng)過如下8個步驟。
- 檢測此 Class 是否載入過(即在緩存區(qū)中是否有此Class),如果有則直接進(jìn)入第8步,否則接著執(zhí)行第2步。
- 如果父類加載器不存在(如果沒有父類加載器,則要么 parent 一定是根類加載器,要么本身就是根類加載器),則跳到第4步執(zhí)行;如果父類加載器存在,則接著執(zhí)行第3步。
- 請求使用父類加載器去載入目標(biāo)類,如果成功載入則跳到第8步,否則接著執(zhí)行第5步。
- 請求使用根類加載器來載入目標(biāo)類,如果成功載入則跳到第8步,否則跳到第7步。
- 當(dāng)前類加載器嘗試尋找 Class 文件(從與此 ClassLoader 相關(guān)的類路徑中尋找),如果找到則執(zhí)行第6步,如果找不到則跳到第7步。
- 從文件中載入 Class,成功載入后跳到第8步。
- 拋出 ClassNotFoundExcepuon 異常。
- 返回對應(yīng)的 java.lang.Class 對象。
其中,第5、6步允許重寫 ClassLoader的 findClass() 方法來實現(xiàn)自己的載入策略,甚至重寫 loadClass() 方法來實現(xiàn)自己的載入過程。
創(chuàng)建并使用自定義的類加載器
JVM 中除根類加載器之外的所有類加載器都是 ClassLoader 子類的實例,開發(fā)者可以通過擴(kuò)展 ClassLoader 的子類,并重寫該 ClassLoader 所包含的方法來實現(xiàn)自定義的類加載器。查閱API文檔中關(guān)于 ClassLoader 的方法不難發(fā)現(xiàn),ClassLoader 中包含了大量的 protected 方法——這些方法都可被子類重寫。
ClassLoader 類有如下兩個關(guān)鍵方法。
- loadClass(String name, boolean resolve):該方法為 ClassLoader 的入口點,根據(jù)指定名稱來加載類,系統(tǒng)就是調(diào)用 ClassLoader 的該方法來獲取指定類對應(yīng)的 Class 對象。
- findClass(String name):根據(jù)指定名稱來查找類。
如果需要實現(xiàn)自定義的 ClassLoader,則可以通過重寫以上兩個方法來實現(xiàn),通常推薦重寫 findClass() 方法,而不是重寫 loadClass() 方法。loadClass() 方法的執(zhí)行步驟如下。
- 用 findLoadedClass(String) 來檢查是否已經(jīng)加載類,如果已經(jīng)加載則直接返回。
- 在父類加載器上調(diào)用 loadClass() 方法。如果父類加載器為null,則使用根類加載器來加載。
- 調(diào)用 findClass(String) 方法查找類。
從上面步驟中可以看出,重寫 findClass()方法可以避免覆蓋默認(rèn)類加載器的父類委托、緩沖機(jī)制兩種策略:如果重寫 loadClass() 方法,則實現(xiàn)邏輯更為復(fù)雜。
在 ClassLoader 里還有一個核心方法:Class defineClass(String name, byte[] b, int off,int len) 該方法負(fù)責(zé)將指定類的字節(jié)碼文件(即 Class 文件,如 Hello.class)讀入字節(jié)數(shù)組 byte[] b 內(nèi),并把它轉(zhuǎn)換為 Class對象,該字節(jié)碼文件可以來源于文件、網(wǎng)絡(luò)等。
defineClass() 方法管理 JVM 的許多復(fù)雜的實現(xiàn),它負(fù)責(zé)將字節(jié)碼分析成運(yùn)行時數(shù)據(jù)結(jié)構(gòu),并校驗有效性等。不過不用擔(dān)心,程序員無須重寫該方法。實際上該方法是 final 的,即使想重寫也沒有機(jī)會。
除此之外,ClassLoader 里還包含如下一些普通方法。
- findSystemClass(String name):從本地文件系統(tǒng)裝入文件。它在本地文件系統(tǒng)中尋找類文件,如果存在,就使用 defineClass() 方法將原始字節(jié)轉(zhuǎn)換成 Class 對象,以將該文件轉(zhuǎn)換成類。
- static getSystemClassLoader():這是一個靜態(tài)方法,用于返回系統(tǒng)類加載器。
- getParent():獲取該類加載器的父類加載器。
- resolveClass(Class<?> c):鏈接指定的類。類加載器可以使用此方法來鏈接類c。讀者無須理會關(guān)于此方法的太多細(xì)節(jié)。
- findLoadedClass(String name):如果此 Java 虛擬機(jī)已加載了名為 name 的類,則直接返回該類對應(yīng)的 Class 實例,否則返回null,該方法是 Java 類加載緩存機(jī)制的體現(xiàn)。
下面程序開發(fā)了一個自定義的 ClassLoader,該 ClassLoader 通過重寫 findClass() 方法來實現(xiàn)自定義的類加載機(jī)制。這個 ClassLoader 可以在加載類之前先編譯該類的文件,從而實現(xiàn)運(yùn)行 Java 之前先編譯該程序的目標(biāo),這樣即可通過該 ClassLoader 直接運(yùn)行 Java 源文件。
public class CompileClassLoader extends ClassLoader { // 讀取一個文件的內(nèi)容 private byte[] getBytes(String filename) throws IOException { File file = new File(filename); long len = file.length(); byte[] raw = new byte[(int) len]; try (FileInputStream fin = new FileInputStream(file)) { // 一次讀取class文件的全部二進(jìn)制數(shù)據(jù) int r = fin.read(raw); if (r != len) throw new IOException("無法讀取全部文件:" + r + " != " + len); return raw; } } // 定義編譯指定Java文件的方法 private boolean compile(String javaFile) throws IOException { System.out.println("CompileClassLoader:正在編譯 " + javaFile + "..."); // 調(diào)用系統(tǒng)的javac命令 Process p = Runtime.getRuntime().exec("javac " + javaFile); try { // 其他線程都等待這個線程完成 p.waitFor(); } catch (InterruptedException ie) { System.out.println(ie); } // 獲取javac線程的退出值 int ret = p.exitValue(); // 返回編譯是否成功 return ret == 0; } // 重寫ClassLoader的findClass方法 protected Class<?> findClass(String name) throws ClassNotFoundException { Class clazz = null; // 將包路徑中的點(.)替換成斜線(/)。 String fileStub = name.replace(".", "/"); String javaFilename = fileStub + ".java"; String classFilename = fileStub + ".class"; File javaFile = new File(javaFilename); File classFile = new File(classFilename); // 當(dāng)指定Java源文件存在,且class文件不存在、或者Java源文件 // 的修改時間比class文件修改時間更晚,重新編譯 if (javaFile.exists() && (!classFile.exists() || javaFile.lastModified() > classFile.lastModified())) { try { // 如果編譯失敗,或者該Class文件不存在 if (!compile(javaFilename) || !classFile.exists()) { throw new ClassNotFoundException("ClassNotFoundExcetpion:" + javaFilename); } } catch (IOException ex) { ex.printStackTrace(); } } // 如果class文件存在,系統(tǒng)負(fù)責(zé)將該文件轉(zhuǎn)換成Class對象 if (classFile.exists()) { try { // 將class文件的二進(jìn)制數(shù)據(jù)讀入數(shù)組 byte[] raw = getBytes(classFilename); // 調(diào)用ClassLoader的defineClass方法將二進(jìn)制數(shù)據(jù)轉(zhuǎn)換成Class對象 clazz = defineClass(name, raw, 0, raw.length); } catch (IOException ie) { ie.printStackTrace(); } } // 如果clazz為null,表明加載失敗,則拋出異常 if (clazz == null) { throw new ClassNotFoundException(name); } return clazz; } // 定義一個主方法 public static void main(String[] args) throws Exception { // 如果運(yùn)行該程序時沒有參數(shù),即沒有目標(biāo)類 if (args.length < 1) { System.out.println("缺少目標(biāo)類,請按如下格式運(yùn)行Java源文件:"); System.out.println("java CompileClassLoader ClassName"); } // 第一個參數(shù)是需要運(yùn)行的類 String progClass = args[0]; // 剩下的參數(shù)將作為運(yùn)行目標(biāo)類時的參數(shù), // 將這些參數(shù)復(fù)制到一個新數(shù)組中 String[] progArgs = new String[args.length - 1]; System.arraycopy(args, 1, progArgs, 0, progArgs.length); CompileClassLoader ccl = new CompileClassLoader(); // 加載需要運(yùn)行的類 Class<?> clazz = ccl.loadClass(progClass); // 獲取需要運(yùn)行的類的主方法 Method main = clazz.getMethod("main", (new String[0]).getClass()); Object[] argsArray = { progArgs }; main.invoke(null, argsArray); } }
上面程序中的粗體字代碼重寫了 findClass() 方法,通過重寫該方法就可以實現(xiàn)自定義的類加載機(jī)制。在本類的 findClass() 方法中先檢查需要加載類的 Class 文件是否存在,如果不存在則先編譯源文件,再調(diào)用 ClassLoader 的 defineClass() 方法來加載這個 Class 文件,并生成相應(yīng)的 Class 對象。
接下來可以隨意提供一個簡單的主類,該主類無須編譯就可以使用上面的 CompileClassLoader 來運(yùn)行它。
public class Hello { public static void main(String[] args) { for (String arg : args) { System.out.println("運(yùn)行Hello的參數(shù):" + arg); } } }
本示例程序提供的類加載器功能比較簡單,僅僅提供了在運(yùn)行之前先編譯 Java 源文件的功能。實際上,使用自定義的類加載器,可以實現(xiàn)如下常見功能。
- 執(zhí)行代碼前自動驗證數(shù)字簽名。
- 根據(jù)用戶提供的密碼解密代碼,從而可以實現(xiàn)代碼混淆器來避免反編譯 *.class 文件。
- 根據(jù)用戶需求來動態(tài)地加載類。
- 根據(jù)應(yīng)用需求把其他數(shù)據(jù)以字節(jié)碼的形式加載到應(yīng)用中。
URLClassLoader 類
Java 為 ClassLoader 提供了一個 URLClassLoader 實現(xiàn)類,該類也是系統(tǒng)類加載器和擴(kuò)展類加載器的父類(此處的父類,就是指類與類之間的繼承關(guān)系)。URLClassLoader 功能比較強(qiáng)大,它既可以從本地文件系統(tǒng)獲取二進(jìn)制文件來加載類,也可以從遠(yuǎn)程主機(jī)獲取二進(jìn)制文件來加載類。
在應(yīng)用程序中可以直接使用 URLClassLoader 加載類,URLClassLoader 類提供了如下兩個構(gòu)造器。
- URLClassLoader(URL[] urls):使用默認(rèn)的父類加載器創(chuàng)建一個 ClassLoader 對象,該對象將從 urls 所指定的系列路徑來查詢并加載類。
- URLClassLoader(URL[] urls, ClassLoader parent):使用指定的父類加載器創(chuàng)建一個 ClassLoader 對象,其他功能與前一個構(gòu)造器相同。
一旦得到了 URLClassLoader 對象之后,就可以調(diào)用該對象的 loadClass() 方法來加載指定類。下面程序示范了如何直接從文件系統(tǒng)中加載 MySQL 驅(qū)動,并使用該驅(qū)動來獲取數(shù)據(jù)庫連接。通過這種方式來獲取數(shù)據(jù)厙連接,可以無須將 MySQL 驅(qū)動添加到 CLASSPATH 環(huán)境變量中。
public class URLClassLoaderTest { private static Connection conn; // 定義一個獲取數(shù)據(jù)庫連接方法 public static Connection getConn(String url, String user, String pass) throws Exception { if (conn == null) { // 創(chuàng)建一個URL數(shù)組 URL[] urls = { new URL("file:mysql-connector-java-5.1.30-bin.jar") }; // 以默認(rèn)的ClassLoader作為父ClassLoader,創(chuàng)建URLClassLoader URLClassLoader myClassLoader = new URLClassLoader(urls); // 加載MySQL的JDBC驅(qū)動,并創(chuàng)建默認(rèn)實例 Driver driver = (Driver) myClassLoader.loadClass("com.mysql.jdbc.Driver").getConstructor().newInstance(); // 創(chuàng)建一個設(shè)置JDBC連接屬性的Properties對象 Properties props = new Properties(); // 至少需要為該對象傳入user和password兩個屬性 props.setProperty("user", user); props.setProperty("password", pass); // 調(diào)用Driver對象的connect方法來取得數(shù)據(jù)庫連接 conn = driver.connect(url, props); } return conn; } public static void main(String[] args) throws Exception { System.out.println(getConn("jdbc:mysql://localhost:3306/mysql", "root", "32147")); } }
上面程序中的前兩行粗體字代碼創(chuàng)建了一個 URLClassLoader 對象,該對象使用默認(rèn)的父類加載器,該類加載器的類加載路徑是當(dāng)前路徑下的 mysql-connector-java-5.1.30-bin.jar 文件,將 MySQL 驅(qū)動復(fù)制到該路徑下,這樣保證該 ClassLoader 可以正常加載到 com.mysql.jdbc.Driver 類。
程序的第三行粗體字代碼使用 ClassLoader 的 loadClass() 加載指定類,并調(diào)用 Class 對象的 newInstance() 方法創(chuàng)建了一個該類的默認(rèn)實例——也就是得到 com.mysql.jdbc.Driver 類的對象,當(dāng)然該對象的實現(xiàn)類實現(xiàn)了 java.sql.Driver 接口,所以程序?qū)⑵鋸?qiáng)制類型轉(zhuǎn)換為 Driver,程序的最后一行粗體字代碼通過 Driver 而不是 DriverManager 來獲取數(shù)據(jù)庫連接,關(guān)于 Driver 接口的用法讀者可以自行查閱API文檔。
正如前面所看到的,創(chuàng)建 URLClassLoader 時傳入了一個 URL 數(shù)組參數(shù),該 ClassLoader 就可以從這系列 URL 指定的資源中加載指定類,這里的 URL 可以以 file: 為前綴,表明從本地文件系統(tǒng)加載;可以以 http: 為前綴,表明從互聯(lián)網(wǎng)通過 HTTP 訪問來加載;也可以以 ftp: 為前綴,表明從互聯(lián)網(wǎng)通過 FTP訪問來加載......功能非常強(qiáng)大。
以上就是淺談JAVA 類加載器的詳細(xì)內(nèi)容,更多關(guān)于JAVA 類加載器的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
@MapperScan和@ComponentScan一塊使用導(dǎo)致沖突的解決
這篇文章主要介紹了@MapperScan和@ComponentScan一塊使用導(dǎo)致沖突的解決,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-11-11使用java實現(xiàn)telnet-client工具分享
這篇文章主要介紹了使用java實現(xiàn)telnet-client工具,需要的朋友可以參考下2014-03-03spring基于通用Dao的多數(shù)據(jù)源配置詳解
這篇文章主要為大家詳細(xì)介紹了spring基于通用Dao的多數(shù)據(jù)源配置,具有一定的參考價值,感興趣的小伙伴們可以參考一下解2018-03-03