深入理解Java类加载器(一):Java类加载原理解析
一、引子
二、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)