一、引子
二、Java 虛擬機類加載器結構簡述

1 //加載指定名稱(包括包名)的二進制類型,供用戶調用的接口 2 public Class<?> loadClass(String name) throws ClassNotFoundException{ … } 3 //加載指定名稱(包括包名)的二進制類型,同時指定是否解析(但是這里的resolve參數不一定真正能達到解析的效果),供繼承用 4 protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{ … } 5 //findClass方法一般被loadClass方法調用去加載指定名稱類,供繼承用 6 protected Class<?> findClass(String name) throws ClassNotFoundException { … } 7 //定義類型,一般在findClass方法中讀取到對應字節碼后調用,final的,不能被繼承 8 //這也從側面說明:JVM已經實現了對應的具體功能,解析對應的字節碼,產生對應的內部數據結構放置到方法區,所以無需覆寫,直接調用就可以了) 9 protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError{ … }
1 public Class<?> loadClass(String name) throws ClassNotFoundException { 2 return loadClass(name, false); 3 } 4 protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { 6 // 首先判斷該類型是否已經被加載 7 Class c = findLoadedClass(name); 8 if (c == null) { 9 //如果沒有被加載,就委托給父類加載或者委派給啟動類加載器加載 10 try { 11 if (parent != null) { 12 //如果存在父類加載器,就委派給父類加載器加載 13 c = parent.loadClass(name, false); 14 } else { // 遞歸終止條件 15 // 由於啟動類加載器無法被Java程序直接引用,因此默認用 null 替代 16 // parent == null就意味着由啟動類加載器嘗試加載該類, 17 // 即通過調用 native方法 findBootstrapClass0(String name)加載 18 c = findBootstrapClass0(name); 19 } 20 } catch (ClassNotFoundException e) { 21 // 如果父類加載器不能完成加載請求時,再調用自身的findClass方法進行類加載,若加載成功,findClass方法返回的是defineClass方法的返回值 22 // 注意,若自身也加載不了,會產生ClassNotFoundException異常並向上拋出 23 c = findClass(name); 24 } 25 } 26 if (resolve) { 27 resolveClass(c); 28 } 29 return c; 30 }

1 public class LoaderTest { 2 public static void main(String[] args) { 3 try { 4 System.out.println(ClassLoader.getSystemClassLoader()); 5 System.out.println(ClassLoader.getSystemClassLoader().getParent()); 6 System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent()); 7 } catch (Exception e) { 8 e.printStackTrace(); 9 } 10 } 11 }/* Output: 12 sun.misc.Launcher$AppClassLoader@6d06d69c 13 sun.misc.Launcher$ExtClassLoader@70dea4e 14 null 15 *///:~
通過以上的代碼輸出,我們知道:通過java.lang.ClassLoader.getSystemClassLoader()可以直接獲取到系統類加載器 ,並且可以判定系統類加載器的父加載器是標准擴展類加載器,但是我們試圖獲取標准擴展類加載器的父類加載器時卻得到了null。事實上,由於啟動類加載器無法被Java程序直接引用,因此JVM默認直接使用 null 代表啟動類加載器。我們還是借助於代碼分析一下,首先看一下java.lang.ClassLoader抽象類中默認實現的兩個構造函數:
1 protected ClassLoader() { 2 SecurityManager security = System.getSecurityManager(); 3 if (security != null) { 4 security.checkCreateClassLoader(); 5 } 6 //默認將父類加載器設置為系統類加載器,getSystemClassLoader()獲取系統類加載器 7 this.parent = getSystemClassLoader(); 8 initialized = true; 9} 10 11 protected ClassLoader(ClassLoader parent) { 12 SecurityManager security = System.getSecurityManager(); 13 if (security != null) { 14 security.checkCreateClassLoader(); 15 } 16 //強制設置父類加載器 17 this.parent = parent; 18 initialized = true; 19 }
緊接着,我們再看一下ClassLoader抽象類中parent成員的聲明:
1 // The parent class loader for delegation 2 private ClassLoader parent;
1 package classloader.test.bean; 2 3 public class TestBean { 4 public TestBean() { } 5 }
在現有當前工程中另外建立一個測試類(ClassLoaderTest.java)內容如下:
1 package classloader.test.bean; 2 public class ClassLoaderTest { 3 public static void main(String[] args) { 4 try { 5 //查看當前系統類路徑中包含的路徑條目 6 System.out.println(System.getProperty("java.class.path")); 7 //調用加載當前類的類加載器(這里即為系統類加載器)加載TestBean 8 Class typeLoaded = Class.forName("classloader.test.bean.TestBean"); 9 //查看被加載的TestBean類型是被那個類加載器加載的 10 System.out.println(typeLoaded.getClassLoader()); 11 } catch (Exception e) { 12 e.printStackTrace(); 13 } 14 } 15 }/* Output: 16 I:\AlgorithmPractice\TestClassLoader\bin 17 sun.misc.Launcher$AppClassLoader@6150818a 18 *///:~
將當前工程輸出目錄下的TestBean.class打包進test.jar剪貼到<Java_Runtime_Home>/lib/ext目錄下(現在工程輸出目錄下和JRE擴展目錄下都有待加載類型的class文件)。再運行測試一測試代碼,結果如下:
1 I:\AlgorithmPractice\TestClassLoader\bin 2 sun.misc.Launcher$ExtClassLoader@15db9742
1 I:\AlgorithmPractice\TestClassLoader\bin 2 sun.misc.Launcher$ExtClassLoader@15db9742
可以看到,后兩次輸出結果一致。那就是說,放置到<Java_Runtime_Home>/lib目錄下的TestBean對應的class字節碼並沒有被加載,這其實和前面講的雙親委派機制並不矛盾。虛擬機出於安全等因素考慮,不會加載<JAVA_HOME>/lib目錄下存在的陌生類。換句話說,虛擬機只加載<JAVA_HOME>/lib目錄下它可以識別的類。因此,開發者通過將要加載的非JDK自身的類放置到此目錄下期待啟動類加載器加載是不可能的。做個進一步驗證,刪除<JAVA_HOME>/lib/ext目錄下和工程輸出目錄下的TestBean對應的class文件,然后再運行測試代碼,則將會有ClassNotFoundException異常拋出。有關這個問題,大家可以在java.lang.ClassLoader中的loadClass(String name, boolean resolve)方法中設置相應斷點進行調試,會發現findBootstrapClass0()會拋出異常,然后在下面的findClass方法中被加載,當前運行的類加載器正是擴展類加載器(sun.misc.Launcher$ExtClassLoader),這一點可以通過JDT中變量視圖查看驗證。
三. Java 程序動態擴展方式
1 public static Class<?> forName(String name, boolean initialize, ClassLoader loader) throws ClassNotFoundException
這里的initialize參數是很重要的,它表示在加載同時是否完成初始化的工作(說明:單參數版本的forName方法默認是完成初始化的)。有些場景下需要將initialize設置為true來強制加載同時完成初始化,例如典型的就是加載數據庫驅動問題。因為JDBC驅動程序只有被注冊后才能被應用程序使用,這就要求驅動程序類必須被初始化,而不單單被加載。
1 // 加載並實例化JDBC驅動類 2 Class.forName(driver); 3 // JDBC驅動類的實現 4 public class Driver extends NonRegisteringDriver implements java.sql.Driver { 5 public Driver() throws SQLException { 6 } 7 // 將initialize設置為true來強制加載同時完成初始化,實現驅動注冊 8 static { 9 try { 10 DriverManager.registerDriver(new Driver()); 11 } catch (SQLException var1) { 12 throw new RuntimeException("Can\'t register driver!"); 13 } 14 } 15 }

四. 常見問題分析
1 public class TestBean { 2 3 public static void main(String[] args) throws Exception { 4 // 一個簡單的類加載器,逆向雙親委派機制 5 // 可以加載與自己在同一路徑下的Class文件 6 ClassLoader myClassLoader = new ClassLoader() { 7 @Override 8 public Class<?> loadClass(String name) 9 throws ClassNotFoundException { 10 try { 11 String filename = name.substring(name.lastIndexOf(".") + 1) 12 + ".class"; 13 InputStream is = getClass().getResourceAsStream(filename); 14 if (is == null) { 15 return super.loadClass(name); // 遞歸調用父類加載器 16 } 17 byte[] b = new byte[is.available()]; 18 is.read(b); 19 return defineClass(name, b, 0, b.length); 20 } catch (Exception e) { 21 throw new ClassNotFoundException(name); 22 } 23 } 24 }; 25 26 Object obj = myClassLoader.loadClass("classloader.test.bean.TestBean") 27 .newInstance(); 28 System.out.println(obj.getClass()); 29 System.out.println(obj instanceof classloader.test.bean.TestBean); 30 } 31 }/* Output: 32 class classloader.test.bean.TestBean 33 false 34 */
我們發現,obj 確實是類classloader.test.bean.TestBean實例化出來的對象,但當這個對象與類classloader.test.bean.TestBean做所屬類型檢查時卻返回了false。這是因為虛擬機中存在了兩個TestBean類,一個是由系統類加載器加載的,另一個則是由我們自定義的類加載器加載的,雖然它們來自同一個Class文件,但依然是兩個獨立的類,因此做所屬類型檢查時返回false。
1 //java.lang.Class.java 2 publicstatic Class<?> forName(String className) throws ClassNotFoundException { 3 return forName0(className, true, ClassLoader.getCallerClassLoader()); 4 } 5 //java.lang.ClassLoader.java 6 // Returns the invoker's class loader, or null if none. 7 static ClassLoader getCallerClassLoader() { 8 // 獲取調用類(caller)的類型 9 Class caller = Reflection.getCallerClass(3); 10 // This can be null if the VM is requesting it 11 if (caller == null) { 12 return null; 13 } 14 // 調用java.lang.Class中本地方法獲取加載該調用類(caller)的ClassLoader 15 return caller.getClassLoader0(); 16 } 17 //java.lang.Class.java 18 //虛擬機本地實現,獲取當前類的類加載器,前面介紹的Class的getClassLoader()也使用此方法 19 native ClassLoader getClassLoader0();
1 //摘自java.lang.ClassLoader.java 2 protected ClassLoader() { 3 SecurityManager security = System.getSecurityManager(); 4 if (security != null) { 5 security.checkCreateClassLoader(); 6 } 7 this.parent = getSystemClassLoader(); 8 initialized = true; 9 }
我們再來看一下對應的getSystemClassLoader()方法的實現:
1 private static synchronized void initSystemClassLoader() { 2 //... 3 sun.misc.Launcher l = sun.misc.Launcher.getLauncher(); 4 scl = l.getClassLoader(); 5 //... 6 }
我們可以寫簡單的測試代碼來測試一下:
System.out.println(sun.misc.Launcher.getLauncher().getClassLoader());
本機對應輸出如下:
1 sun.misc.Launcher$AppClassLoader@73d16e93
1 //用戶自定義類加載器WrongClassLoader.Java(覆寫loadClass邏輯) 2 public class WrongClassLoader extends ClassLoader { 3 public Class<?> loadClass(String name) throws ClassNotFoundException { 4 return this.findClass(name); 5 } 6 protected Class<?> findClass(String name) throws ClassNotFoundException { 7 // 假設此處只是到工程以外的特定目錄D:\library下去加載類 8 // 具體實現代碼省略 9 } 10 }
通過前面的分析我們已經知道,這個自定義類加載器WrongClassLoader的默認類加載器是系統類加載器,但是現在問題4中的結論就不成立了。大家可以簡單測試一下,現在<JAVA_HOME>/lib、<JAVA_HOME>/lib/ext 和 工程類路徑上的類都加載不上了。
1 //問題5測試代碼一 2 public class WrongClassLoaderTest { 3 publicstaticvoid main(String[] args) { 4 try { 5 WrongClassLoader loader = new WrongClassLoader(); 6 Class classLoaded = loader.loadClass("beans.Account"); 7 System.out.println(classLoaded.getName()); 8 System.out.println(classLoaded.getClassLoader()); 9 } catch (Exception e) { 10 e.printStackTrace(); 11 } 12 } 13 }/* Output: 14 java.io.FileNotFoundException: D:"classes"java"lang"Object.class (系統找不到指定的路徑。) 15 at java.io.FileInputStream.open(Native Method) 16 at java.io.FileInputStream.<init>(FileInputStream.java:106) 17 at WrongClassLoader.findClass(WrongClassLoader.java:40) 18 at WrongClassLoader.loadClass(WrongClassLoader.java:29) 19 at java.lang.ClassLoader.loadClassInternal(ClassLoader.java:319) 20 at java.lang.ClassLoader.defineClass1(Native Method) 21 at java.lang.ClassLoader.defineClass(ClassLoader.java:620) 22 at java.lang.ClassLoader.defineClass(ClassLoader.java:400) 23 at WrongClassLoader.findClass(WrongClassLoader.java:43) 24 at WrongClassLoader.loadClass(WrongClassLoader.java:29) 25 at WrongClassLoaderTest.main(WrongClassLoaderTest.java:27) 26 Exception in thread "main" java.lang.NoClassDefFoundError: java/lang/Object 27 at java.lang.ClassLoader.defineClass1(Native Method) 28 at java.lang.ClassLoader.defineClass(ClassLoader.java:620) 29 at java.lang.ClassLoader.defineClass(ClassLoader.java:400) 30 at WrongClassLoader.findClass(WrongClassLoader.java:43) 31 at WrongClassLoader.loadClass(WrongClassLoader.java:29) 32 at WrongClassLoaderTest.main(WrongClassLoaderTest.java:27) 33 */
注意,這里D:"classes"beans"Account.class是物理存在的。這說明,連要加載的類型的超類型java.lang.Object都加載不到了。這里列舉的由於覆寫loadClass()引起的邏輯錯誤明顯是比較簡單的,實際引起的邏輯錯誤可能復雜的多。
1 //問題5測試二 2 //用戶自定義類加載器WrongClassLoader.Java(不覆寫loadClass邏輯) 3 public class WrongClassLoader extends ClassLoader { 4 protected Class<?> findClass(String name) throws ClassNotFoundException { 5 //假設此處只是到工程以外的特定目錄D:\library下去加載類 6 //具體實現代碼省略 7 } 8 }/* Output: 9 beans.Account 10 WrongClassLoader@1c78e57 11 */
將自定義類加載器代碼WrongClassLoader.Java做以上修改后,再運行測試代碼,輸出正確。
1 public class Test { 2 public static void main(String[] args) { 3 System.out.println("Rico"); 4 Gson gson = new Gson(); 5 System.out.println(gson.getClass().getClassLoader()); 6 System.out.println(System.getProperty("java.class.path")); 7 } 8 }/* Output: 9 Rico 10 sun.misc.Launcher$AppClassLoader@6c68bcef 11 I:\AlgorithmPractice\TestClassLoader\bin;I:\Java\jars\Gson\gson-2.3.1.jar 12 */
如上述程序所示,Test類和Gson類由系統類加載器加載,並且其加載路徑就是用戶類路徑,包括當前類路徑和引用的第三方類庫的路徑。
1 import java.net.URL; 2 import java.net.URLClassLoader; 3 4 public class ClassLoaderTest { 5 /** 6 * @param args the command line arguments 7 */ 8 public static void main(String[] args) { 9 try { 10 URL[] extURLs = ((URLClassLoader) ClassLoader.getSystemClassLoader().getParent()).getURLs(); 11 for (int i = 0; i < extURLs.length; i++) { 12 System.out.println(extURLs[i]); 13 } 14 } catch (Exception e) { 15 //… 16 } 17 } 18 } /* Output: 19 file:/C:/Program%20Files/Java/jdk1.7.0_79/jre/lib/ext/access-bridge-64.jar 20 file:/C:/Program%20Files/Java/jdk1.7.0_79/jre/lib/ext/dnsns.jar 21 file:/C:/Program%20Files/Java/jdk1.7.0_79/jre/lib/ext/jaccess.jar 22 file:/C:/Program%20Files/Java/jdk1.7.0_79/jre/lib/ext/localedata.jar 23 file:/C:/Program%20Files/Java/jdk1.7.0_79/jre/lib/ext/sunec.jar 24 file:/C:/Program%20Files/Java/jdk1.7.0_79/jre/lib/ext/sunjce_provider.jar 25 file:/C:/Program%20Files/Java/jdk1.7.0_79/jre/lib/ext/sunmscapi.jar 26 file:/C:/Program%20Files/Java/jdk1.7.0_79/jre/lib/ext/zipfs.jar 27 */
五. 開發自己的類加載器
1、文件系統類加載器
1 package classloader; 2 import java.io.ByteArrayOutputStream; 3 import java.io.File; 4 import java.io.FileInputStream; 5 import java.io.IOException; 6 import java.io.InputStream; 7 // 文件系統類加載器 8 public class FileSystemClassLoader extends ClassLoader { 9 private String rootDir; 10 public FileSystemClassLoader(String rootDir) { 11 this.rootDir = rootDir; 12 } 13 // 獲取類的字節碼 14 @Override 15 protected Class<?> findClass(String name) throws ClassNotFoundException { 16 byte[] classData = getClassData(name); // 獲取類的字節數組 17 if (classData == null) { 18 throw new ClassNotFoundException(); 19 } else { 20 return defineClass(name, classData, 0, classData.length); 21 } 22 } 23 private byte[] getClassData(String className) { 24 // 讀取類文件的字節 25 String path = classNameToPath(className); 26 try { 27 InputStream ins = new FileInputStream(path); 28 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 29 int bufferSize = 4096; 30 byte[] buffer = new byte[bufferSize]; 31 int bytesNumRead = 0; 32 // 讀取類文件的字節碼 33 while ((bytesNumRead = ins.read(buffer)) != -1) { 34 baos.write(buffer, 0, bytesNumRead); 35 } 36 return baos.toByteArray(); 37 } catch (IOException e) { 38 e.printStackTrace(); 39 } 40 return null; 41 } 42 private String classNameToPath(String className) { 43 // 得到類文件的完全路徑 44 return rootDir + File.separatorChar 45 + className.replace('.', File.separatorChar) + ".class"; 46 } 47 }
1 package com.example; 2 public class Sample { 3 private Sample instance; 4 public void setSample(Object instance) { 5 System.out.println(instance.toString()); 6 this.instance = (Sample) instance; 7 } 8 }
1 package classloader; 2 import java.lang.reflect.Method; 3 public class ClassIdentity { 4 public static void main(String[] args) { 5 new ClassIdentity().testClassIdentity(); 6 } 7 public void testClassIdentity() { 8 String classDataRootPath = "C:\\Users\\JackZhou\\Documents\\NetBeansProjects\\classloader\\build\\classes"; 9 FileSystemClassLoader fscl1 = new FileSystemClassLoader(classDataRootPath); 10 FileSystemClassLoader fscl2 = new FileSystemClassLoader(classDataRootPath); 11 String className = "com.example.Sample"; 12 try { 13 Class<?> class1 = fscl1.loadClass(className); // 加載Sample類 14 Object obj1 = class1.newInstance(); // 創建對象 15 Class<?> class2 = fscl2.loadClass(className); 16 Object obj2 = class2.newInstance(); 17 Method setSampleMethod = class1.getMethod("setSample", java.lang.Object.class); 18 setSampleMethod.invoke(obj1, obj2); 19 } catch (Exception e) { 20 e.printStackTrace(); 21 } 22 } 23 }/* Output: 24 com.example.Sample@7852e922 25 */
1 package classloader; 2 import java.io.ByteArrayOutputStream; 3 import java.io.InputStream; 4 import java.net.URL; 5 public class NetworkClassLoader extends ClassLoader { 6 private String rootUrl; 7 public NetworkClassLoader(String rootUrl) { 8 // 指定URL 9 this.rootUrl = rootUrl; 10 } 11 // 獲取類的字節碼 12 @Override 13 protected Class<?> findClass(String name) throws ClassNotFoundException { 14 byte[] classData = getClassData(name); 15 if (classData == null) { 16 throw new ClassNotFoundException(); 17 } else { 18 return defineClass(name, classData, 0, classData.length); 19 } 20 } 21 private byte[] getClassData(String className) { 22 // 從網絡上讀取的類的字節 23 String path = classNameToPath(className); 24 try { 25 URL url = new URL(path); 26 InputStream ins = url.openStream(); 27 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 28 int bufferSize = 4096; 29 byte[] buffer = new byte[bufferSize]; 30 int bytesNumRead = 0; 31 // 讀取類文件的字節 32 while ((bytesNumRead = ins.read(buffer)) != -1) { 33 baos.write(buffer, 0, bytesNumRead); 34 } 35 return baos.toByteArray(); 36 } catch (Exception e) { 37 e.printStackTrace(); 38 } 39 return null; 40 } 41 private String classNameToPath(String className) { 42 // 得到類文件的URL 43 return rootUrl + "/" 44 + className.replace('.', '/') + ".class"; 45 } 46 }
在通過NetworkClassLoader加載了某個版本的類之后,一般有兩種做法來使用它。第一種做法是使用Java反射API。另外一種做法是使用接口。需要注意的是,並不能直接在客戶端代碼中引用從服務器上下載的類,因為客戶端代碼的類加載器找不到這些類。使用Java反射API可以直接調用Java類的方法。而使用接口的做法則是把接口的類放在客戶端中,從服務器上加載實現此接口的不同版本的類。在客戶端通過相同的接口來使用這些實現類。我們使用接口的方式。示例如下:
客戶端接口:
1 package classloader; 2 public interface Versioned { 3 String getVersion(); 4 }
package classloader; public interface ICalculator extends Versioned { String calculate(String expression); }
網絡上的不同版本的類:
1 package com.example; 2 import classloader.ICalculator; 3 public class CalculatorBasic implements ICalculator { 4 @Override 5 public String calculate(String expression) { 6 return expression; 7 } 8 9 @Override 10 public String getVersion() { 11 return "1.0"; 12 } 13 }
1 package com.example; 2 import classloader.ICalculator; 3 public class CalculatorAdvanced implements ICalculator { 4 @Override 5 public String calculate(String expression) { 6 return "Result is " + expression; 7 } 8 9 @Override 10 public String getVersion() { 11 return "2.0"; 12 } 13 } 14
在客戶端加載網絡上的類的過程:
1 package classloader; 2 public class CalculatorTest { 3 public static void main(String[] args) { 4 String url = "http://localhost:8080/ClassloaderTest/classes"; 5 NetworkClassLoader ncl = new NetworkClassLoader(url); 6 String basicClassName = "com.example.CalculatorBasic"; 7 String advancedClassName = "com.example.CalculatorAdvanced"; 8 try { 9 Class<?> clazz = ncl.loadClass(basicClassName); // 加載一個版本的類 10 ICalculator calculator = (ICalculator) clazz.newInstance(); // 創建對象 11 System.out.println(calculator.getVersion()); 12 clazz = ncl.loadClass(advancedClassName); // 加載另一個版本的類 13 calculator = (ICalculator) clazz.newInstance(); 14 System.out.println(calculator.getVersion()); 15 } catch (Exception e) { 16 e.printStackTrace(); 17 } 18 } 19 }
原創:書呆子Rico(https://blog.csdn.net/justloveyou)