淺談JAVA 類加載器
類加載機(jī)制
類加載器負(fù)責(zé)加載所有的類,系統(tǒng)為所有被載入內(nèi)存中的類生成一個(gè) java.lang.Class 實(shí)例。一旦一個(gè)類被載入 JVM 中,同個(gè)類就不會(huì)被再次載入了。現(xiàn)在的問題是,怎么樣才算“同一個(gè)類”?
正如一個(gè)對(duì)象有一個(gè)唯一的標(biāo)識(shí)一樣,一個(gè)載入 JVM 中的類也有一個(gè)唯一的標(biāo)識(shí)。在 Java 中,一個(gè)類用其全限定類名(包括包名和類名)作為標(biāo)識(shí):但在 JVM 中,一個(gè)類用其全限定類名和其類加載器作為唯一標(biāo)識(shí)。例如,如果在 pg 的包中有一個(gè)名為 Person 的類,被類加載器 ClassLoader 的實(shí)例 k1 負(fù)責(zé)加載,則該 Person 類對(duì)應(yīng)的 Class 對(duì)象在 JVM 中表示為(Person、pg、k1)。這意味著兩個(gè)類加載器加載的同名類:(Person、pg、k1)和(Person、pg、k12)是不同的,它們所加載的類也是完全不同、互不兼容的。
當(dāng) JVM 啟動(dòng)時(shí),會(huì)形成由三個(gè)類加載器組成的初始類加載器層次結(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 命令時(shí),使用 -Xbootclasspath 或 -D 選項(xiàng)指定 sun.boot.class.path 系統(tǒng)屬性值可以指定加載附加的類。
JVM的類加載機(jī)制主要有如下三種。
全盤負(fù)責(zé)。所謂全盤負(fù)責(zé),就是當(dāng)一個(gè)類加載器負(fù)責(zé)加載某個(gè) Class 時(shí),該 Class 所依賴的和引用的其他 Class 也將由該類加載器負(fù)責(zé)載入,除非顯式使用另外一個(gè)類加載器來載入。 父類委托。所謂父類委托,則是先讓 parent(父)類加載器試圖加載該 Class,只有在父類加載器無法加載該類時(shí)才嘗試從自己的類路徑中加載該類。 緩存機(jī)制。緩存機(jī)制將會(huì)保證所有加載過的 Class 都會(huì)被緩存,當(dāng)程序中需要使用某個(gè) Class 時(shí),類加載器先從緩存區(qū)中搜尋該 Class,只有當(dāng)緩存區(qū)中不存在該 Class 對(duì)象時(shí),系統(tǒng)才會(huì)讀取該類對(duì)應(yīng)的二進(jìn)制數(shù)據(jù),并將其轉(zhuǎn)換成 Class 對(duì)象,存入緩存區(qū)中。這就是為什么修改了 Class 后,必須重新啟動(dòng) JVM,程序所做的修改才會(huì)生效的原因。除了可以使用 Java 提供的類加載器之外,開發(fā)者也可以實(shí)現(xiàn)自己的類加載器,自定義的類加載器通過繼承 ClassLoader 來實(shí)現(xiàn)。JVM 中這4種類加載器的層次結(jié)構(gòu)如下圖所示。
注意:類加載器之間的父子關(guān)系并不是類繼承上的父子關(guān)系,這里的父子關(guān)系是類加載器實(shí)例之間的關(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)行上面的程序,會(huì)看到如下運(yùn)行結(jié)果
系統(tǒng)類加載器:sun.misc.Launcher$AppClassLoader@73d16e93file:/F:/EclipseProjects/demo/bin/擴(kuò)展類加載器:sun.misc.Launcher$ExtClassLoader@15db9742擴(kuò)展類加載器的加載路徑:C:Program FilesJavajre1.8.0_181libext;C:WindowsSunJavalibext擴(kuò)展類加載器的parent: null
從上面運(yùn)行結(jié)果可以看出,系統(tǒng)類加載器的加載路徑是程序運(yùn)行的當(dāng)前路徑,擴(kuò)展類加載器的加載路徑是null(與 Java8 有區(qū)別),但此處看到擴(kuò)展類加載器的父加載器是null,并不是根類加載器。這是因?yàn)楦惣虞d器并沒有繼承 ClassLoader 抽象類,所以擴(kuò)展類加載器的 getParent() 方法返回null。但實(shí)際上,擴(kuò)展類加載器的父類加載器是根類加載器,只是根類加載器并不是 Java 實(shí)現(xiàn)的。
從運(yùn)行結(jié)果可以看出,系統(tǒng)類加載器是 AppClassLoader 的實(shí)例,擴(kuò)展類加載器 ExtClassLoader 的實(shí)例。實(shí)際上,這兩個(gè)類都是 URLClassLoader 類的實(shí)例。
注意:JVM 的根類加載器并不是 Java 實(shí)現(xiàn)的,而且由于程序通常無須訪問根類加載器,因此訪問擴(kuò)展類加載器的父類加載器時(shí)返回null。
類加載器加載 Class 大致要經(jīng)過如下8個(gè)步驟。
檢測(cè)此 Class 是否載入過(即在緩存區(qū)中是否有此Class),如果有則直接進(jìn)入第8步,否則接著執(zhí)行第2步。 如果父類加載器不存在(如果沒有父類加載器,則要么 parent 一定是根類加載器,要么本身就是根類加載器),則跳到第4步執(zhí)行;如果父類加載器存在,則接著執(zhí)行第3步。 請(qǐng)求使用父類加載器去載入目標(biāo)類,如果成功載入則跳到第8步,否則接著執(zhí)行第5步。 請(qǐng)求使用根類加載器來載入目標(biāo)類,如果成功載入則跳到第8步,否則跳到第7步。 當(dāng)前類加載器嘗試尋找 Class 文件(從與此 ClassLoader 相關(guān)的類路徑中尋找),如果找到則執(zhí)行第6步,如果找不到則跳到第7步。 從文件中載入 Class,成功載入后跳到第8步。 拋出 ClassNotFoundExcepuon 異常。 返回對(duì)應(yīng)的 java.lang.Class 對(duì)象。其中,第5、6步允許重寫 ClassLoader的 findClass() 方法來實(shí)現(xiàn)自己的載入策略,甚至重寫 loadClass() 方法來實(shí)現(xiàn)自己的載入過程。
創(chuàng)建并使用自定義的類加載器
JVM 中除根類加載器之外的所有類加載器都是 ClassLoader 子類的實(shí)例,開發(fā)者可以通過擴(kuò)展 ClassLoader 的子類,并重寫該 ClassLoader 所包含的方法來實(shí)現(xiàn)自定義的類加載器。查閱API文檔中關(guān)于 ClassLoader 的方法不難發(fā)現(xiàn),ClassLoader 中包含了大量的 protected 方法——這些方法都可被子類重寫。
ClassLoader 類有如下兩個(gè)關(guān)鍵方法。
loadClass(String name, boolean resolve):該方法為 ClassLoader 的入口點(diǎn),根據(jù)指定名稱來加載類,系統(tǒng)就是調(diào)用 ClassLoader 的該方法來獲取指定類對(duì)應(yīng)的 Class 對(duì)象。 findClass(String name):根據(jù)指定名稱來查找類。如果需要實(shí)現(xiàn)自定義的 ClassLoader,則可以通過重寫以上兩個(gè)方法來實(shí)現(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() 方法,則實(shí)現(xiàn)邏輯更為復(fù)雜。
在 ClassLoader 里還有一個(gè)核心方法: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對(duì)象,該字節(jié)碼文件可以來源于文件、網(wǎng)絡(luò)等。
defineClass() 方法管理 JVM 的許多復(fù)雜的實(shí)現(xiàn),它負(fù)責(zé)將字節(jié)碼分析成運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu),并校驗(yàn)有效性等。不過不用擔(dān)心,程序員無須重寫該方法。實(shí)際上該方法是 final 的,即使想重寫也沒有機(jī)會(huì)。
除此之外,ClassLoader 里還包含如下一些普通方法。
findSystemClass(String name):從本地文件系統(tǒng)裝入文件。它在本地文件系統(tǒng)中尋找類文件,如果存在,就使用 defineClass() 方法將原始字節(jié)轉(zhuǎn)換成 Class 對(duì)象,以將該文件轉(zhuǎn)換成類。 static getSystemClassLoader():這是一個(gè)靜態(tài)方法,用于返回系統(tǒng)類加載器。 getParent():獲取該類加載器的父類加載器。 resolveClass(Class<?> c):鏈接指定的類。類加載器可以使用此方法來鏈接類c。讀者無須理會(huì)關(guān)于此方法的太多細(xì)節(jié)。 findLoadedClass(String name):如果此 Java 虛擬機(jī)已加載了名為 name 的類,則直接返回該類對(duì)應(yīng)的 Class 實(shí)例,否則返回null,該方法是 Java 類加載緩存機(jī)制的體現(xiàn)。下面程序開發(fā)了一個(gè)自定義的 ClassLoader,該 ClassLoader 通過重寫 findClass() 方法來實(shí)現(xiàn)自定義的類加載機(jī)制。這個(gè) ClassLoader 可以在加載類之前先編譯該類的文件,從而實(shí)現(xiàn)運(yùn)行 Java 之前先編譯該程序的目標(biāo),這樣即可通過該 ClassLoader 直接運(yùn)行 Java 源文件。
public class CompileClassLoader extends ClassLoader { // 讀取一個(gè)文件的內(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 { // 其他線程都等待這個(gè)線程完成 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; // 將包路徑中的點(diǎn)(.)替換成斜線(/)。 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源文件 // 的修改時(shí)間比class文件修改時(shí)間更晚,重新編譯 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對(duì)象 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對(duì)象 clazz = defineClass(name, raw, 0, raw.length); } catch (IOException ie) { ie.printStackTrace(); } } // 如果clazz為null,表明加載失敗,則拋出異常 if (clazz == null) { throw new ClassNotFoundException(name); } return clazz; } // 定義一個(gè)主方法 public static void main(String[] args) throws Exception { // 如果運(yùn)行該程序時(shí)沒有參數(shù),即沒有目標(biāo)類 if (args.length < 1) { System.out.println('缺少目標(biāo)類,請(qǐng)按如下格式運(yùn)行Java源文件:'); System.out.println('java CompileClassLoader ClassName'); } // 第一個(gè)參數(shù)是需要運(yùn)行的類 String progClass = args[0]; // 剩下的參數(shù)將作為運(yùn)行目標(biāo)類時(shí)的參數(shù), // 將這些參數(shù)復(fù)制到一個(gè)新數(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() 方法,通過重寫該方法就可以實(shí)現(xiàn)自定義的類加載機(jī)制。在本類的 findClass() 方法中先檢查需要加載類的 Class 文件是否存在,如果不存在則先編譯源文件,再調(diào)用 ClassLoader 的 defineClass() 方法來加載這個(gè) Class 文件,并生成相應(yīng)的 Class 對(duì)象。
接下來可以隨意提供一個(gè)簡單的主類,該主類無須編譯就可以使用上面的 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 源文件的功能。實(shí)際上,使用自定義的類加載器,可以實(shí)現(xiàn)如下常見功能。
執(zhí)行代碼前自動(dòng)驗(yàn)證數(shù)字簽名。 根據(jù)用戶提供的密碼解密代碼,從而可以實(shí)現(xiàn)代碼混淆器來避免反編譯 *.class 文件。 根據(jù)用戶需求來動(dòng)態(tài)地加載類。 根據(jù)應(yīng)用需求把其他數(shù)據(jù)以字節(jié)碼的形式加載到應(yīng)用中。URLClassLoader 類
Java 為 ClassLoader 提供了一個(gè) URLClassLoader 實(shí)現(xiàn)類,該類也是系統(tǒng)類加載器和擴(kuò)展類加載器的父類(此處的父類,就是指類與類之間的繼承關(guān)系)。URLClassLoader 功能比較強(qiáng)大,它既可以從本地文件系統(tǒng)獲取二進(jìn)制文件來加載類,也可以從遠(yuǎn)程主機(jī)獲取二進(jìn)制文件來加載類。
在應(yīng)用程序中可以直接使用 URLClassLoader 加載類,URLClassLoader 類提供了如下兩個(gè)構(gòu)造器。
URLClassLoader(URL[] urls):使用默認(rèn)的父類加載器創(chuàng)建一個(gè) ClassLoader 對(duì)象,該對(duì)象將從 urls 所指定的系列路徑來查詢并加載類。 URLClassLoader(URL[] urls, ClassLoader parent):使用指定的父類加載器創(chuàng)建一個(gè) ClassLoader 對(duì)象,其他功能與前一個(gè)構(gòu)造器相同。一旦得到了 URLClassLoader 對(duì)象之后,就可以調(diào)用該對(duì)象的 loadClass() 方法來加載指定類。下面程序示范了如何直接從文件系統(tǒng)中加載 MySQL 驅(qū)動(dòng),并使用該驅(qū)動(dòng)來獲取數(shù)據(jù)庫連接。通過這種方式來獲取數(shù)據(jù)厙連接,可以無須將 MySQL 驅(qū)動(dòng)添加到 CLASSPATH 環(huán)境變量中。
public class URLClassLoaderTest { private static Connection conn; // 定義一個(gè)獲取數(shù)據(jù)庫連接方法 public static Connection getConn(String url, String user, String pass) throws Exception { if (conn == null) { // 創(chuàng)建一個(gè)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ū)動(dòng),并創(chuàng)建默認(rèn)實(shí)例 Driver driver = (Driver) myClassLoader.loadClass('com.mysql.jdbc.Driver').getConstructor().newInstance(); // 創(chuàng)建一個(gè)設(shè)置JDBC連接屬性的Properties對(duì)象 Properties props = new Properties(); // 至少需要為該對(duì)象傳入user和password兩個(gè)屬性 props.setProperty('user', user); props.setProperty('password', pass); // 調(diào)用Driver對(duì)象的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)建了一個(gè) URLClassLoader 對(duì)象,該對(duì)象使用默認(rèn)的父類加載器,該類加載器的類加載路徑是當(dāng)前路徑下的 mysql-connector-java-5.1.30-bin.jar 文件,將 MySQL 驅(qū)動(dòng)復(fù)制到該路徑下,這樣保證該 ClassLoader 可以正常加載到 com.mysql.jdbc.Driver 類。
程序的第三行粗體字代碼使用 ClassLoader 的 loadClass() 加載指定類,并調(diào)用 Class 對(duì)象的 newInstance() 方法創(chuàng)建了一個(gè)該類的默認(rèn)實(shí)例——也就是得到 com.mysql.jdbc.Driver 類的對(duì)象,當(dāng)然該對(duì)象的實(shí)現(xiàn)類實(shí)現(xiàn)了 java.sql.Driver 接口,所以程序?qū)⑵鋸?qiáng)制類型轉(zhuǎn)換為 Driver,程序的最后一行粗體字代碼通過 Driver 而不是 DriverManager 來獲取數(shù)據(jù)庫連接,關(guān)于 Driver 接口的用法讀者可以自行查閱API文檔。
正如前面所看到的,創(chuàng)建 URLClassLoader 時(shí)傳入了一個(gè) 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 類加載器的資料請(qǐng)關(guān)注好吧啦網(wǎng)其它相關(guān)文章!
相關(guān)文章:
1. IntelliJ IDEA設(shè)置默認(rèn)瀏覽器的方法2. IntelliJ IDEA設(shè)置背景圖片的方法步驟3. Spring security 自定義過濾器實(shí)現(xiàn)Json參數(shù)傳遞并兼容表單參數(shù)(實(shí)例代碼)4. docker /var/lib/docker/aufs/mnt 目錄清理方法5. Python TestSuite生成測(cè)試報(bào)告過程解析6. Python 的 __str__ 和 __repr__ 方法對(duì)比7. JAMon(Java Application Monitor)備忘記8. Python Scrapy多頁數(shù)據(jù)爬取實(shí)現(xiàn)過程解析9. Python OpenCV去除字母后面的雜線操作10. 增大python字體的方法步驟
