Classloader总结

jopen 10年前

类加载机制, 线程上下文加载器Thread.setContextClassLoader(), 自定义类加载器。

顾名思义, ClassLoader就是类加载器, 而类加载是java程序运行的第一步, 如果没有类加载器来加载类,那么再牛逼的java程序也运行不了, 可见类加载器的重要性。理解类加载器的加载机制, 可以很好的帮助我们理解java类的执行过程, 深入理解java的原理, 帮助我们写出更有效、更高效、更牛逼的程序。

委托机制

java的类加载器采用向上委托机制,需要加载一个类的时候,它的过程如下:

1. 先提交给父加载器去寻找这个类,父加载器再交给它的父加载器, 一直到最顶层的加载器BootstrapClassloader。

2. 如果BootstrapClassloader加载器找到, 那么就直接将加载后的代码交给发起加载过程的加载器去调用, 如果没找到,就交给BootstrapClassloader他的子加载器,也就是ExtClassloader去加载。

3. ExtClassloader如果加载成功, 就把加载后的代码交给发起加载过程的加载器去调用, 如果没找到,就交给ExtClassloader他的子加载器,也就是AppClassloader去加载。

4. AppClassloader重复BootstrapClassloader、ExtClassloader类似的过程, 直到加载类成功, 或者找不到目标类, 抛出ClassNotFoundException。

整个过程如下图所示:

Classloader总结

其中BootstrapClassloader是JVM提供的初始化类加载器, 它是所有类加载器的根, 随着jvm启动而启动。

加载目录

如上图中, 我们看到每个加载器加载类的目录都是指定好的, 这个指定好的目录是怎么来的呢? 这里提供一段代码, 大家可以看输出结果和上图中的比较。

@SuppressWarnings("restriction")      public static void showClassLoaderPath() {          System.out.println("BootstrapClassLoader: ");          URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();          for(URL url : urls){              System.out.println(url.getPath());          }          System.out.println("BootstrapClassloader的加载目录: " + System.getProperty("sun.boot.class.path"));          System.out.println("----------------------------");          URLClassLoader extClassLoader = (URLClassLoader)ClassLoader.getSystemClassLoader().getParent();           System.out.println(extClassLoader.getClass().getName() + ": ");            urls = extClassLoader.getURLs();          for(URL url : urls) {              System.out.println(url);            }          System.out.println("ExtClassloader的加载目录: " + System.getProperty("java.ext.dirs"));          System.out.println("----------------------------");            URLClassLoader appClassLoader = (URLClassLoader)ClassLoader.getSystemClassLoader();          System.out.println(appClassLoader.getClass().getName() + ": ");            urls = appClassLoader.getURLs();          for(URL url : urls) {              System.out.println(url);            }          System.out.println("AppClassloader的加载目录: " + System.getProperty("java.class.path"));      }

输出结果如下:

BootstrapClassLoader:   /D:/server/java/jdk1.6.0_10/jre/lib/resources.jar  /D:/server/java/jdk1.6.0_10/jre/lib/rt.jar  /D:/server/java/jdk1.6.0_10/jre/lib/sunrsasign.jar  /D:/server/java/jdk1.6.0_10/jre/lib/jsse.jar  /D:/server/java/jdk1.6.0_10/jre/lib/jce.jar  /D:/server/java/jdk1.6.0_10/jre/lib/charsets.jar  /D:/server/java/jdk1.6.0_10/jre/classes  BootstrapClassloader的加载目录: D:\server\java\jdk1.6.0_10\jre\lib\resources.jar;D:\server\java\jdk1.6.0_10\jre\lib\rt.jar;D:\server\java\jdk1.6.0_10\jre\lib\sunrsasign.jar;D:\server\java\jdk1.6.0_10\jre\lib\jsse.jar;D:\server\java\jdk1.6.0_10\jre\lib\jce.jar;D:\server\java\jdk1.6.0_10\jre\lib\charsets.jar;D:\server\java\jdk1.6.0_10\jre\classes  ----------------------------  sun.misc.Launcher$ExtClassLoader:   file:/D:/server/java/jdk1.6.0_10/jre/lib/ext/dnsns.jar  file:/D:/server/java/jdk1.6.0_10/jre/lib/ext/localedata.jar  file:/D:/server/java/jdk1.6.0_10/jre/lib/ext/sunjce_provider.jar  file:/D:/server/java/jdk1.6.0_10/jre/lib/ext/sunmscapi.jar  file:/D:/server/java/jdk1.6.0_10/jre/lib/ext/sunpkcs11.jar  ExtClassloader的加载目录: D:\server\java\jdk1.6.0_10\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext  ----------------------------  sun.misc.Launcher$AppClassLoader:   file:/D:/workspace/98_myproject/jtest/target/classes/  file:/C:/Users/lenovo/.m2/repository/aopalliance/aopalliance/1.0/aopalliance-1.0.jar  file:/C:/Users/lenovo/.m2/repository/ch/qos/logback/logback-classic/1.1.2/logback-classic-1.1.2.jar  file:/C:/Users/lenovo/.m2/repository/org/slf4j/slf4j-api/1.7.6/slf4j-api-1.7.6.jar  file:/C:/Users/lenovo/.m2/repository/ch/qos/logback/logback-core/1.1.2/logback-core-1.1.2.jar  file:/C:/Users/lenovo/.m2/repository/commons-codec/commons-codec/1.9/commons-codec-1.9.jar  file:/C:/Users/lenovo/.m2/repository/commons-lang/commons-lang/2.6/commons-lang-2.6.jar  file:/C:/Users/lenovo/.m2/repository/commons-logging/commons-logging/1.1.3/commons-logging-1.1.3.jar  file:/C:/Users/lenovo/.m2/repository/commons-configuration/commons-configuration/1.10/commons-configuration-1.10.jar  file:/C:/Users/lenovo/.m2/repository/org/apache/httpcomponents/fluent-hc/4.3.3/fluent-hc-4.3.3.jar  file:/C:/Users/lenovo/.m2/repository/org/apache/httpcomponents/httpclient/4.3.3/httpclient-4.3.3.jar  file:/C:/Users/lenovo/.m2/repository/org/apache/httpcomponents/httpcore/4.3.2/httpcore-4.3.2.jar  file:/C:/Users/lenovo/.m2/repository/org/apache/httpcomponents/httpclient-cache/4.3.3/httpclient-cache-4.3.3.jar  file:/C:/Users/lenovo/.m2/repository/org/apache/httpcomponents/httpmime/4.3.3/httpmime-4.3.3.jar  file:/C:/Users/lenovo/.m2/repository/org/quartz-scheduler/quartz/2.2.1/quartz-2.2.1.jar  file:/C:/Users/lenovo/.m2/repository/postgresql/postgresql/9.1-901-1.jdbc4/postgresql-9.1-901-1.jdbc4.jar  file:/C:/Users/lenovo/.m2/repository/commons-dbutils/commons-dbutils/1.5/commons-dbutils-1.5.jar  file:/C:/Users/lenovo/.m2/repository/c3p0/c3p0/0.9.1.2/c3p0-0.9.1.2.jar  file:/C:/Users/lenovo/.m2/repository/junit/junit/4.12-beta-2/junit-4.12-beta-2.jar  file:/C:/Users/lenovo/.m2/repository/org/hamcrest/hamcrest-core/1.3/hamcrest-core-1.3.jar  AppClassloader的加载目录: D:\workspace\98_myproject\jtest\target\classes;C:\Users\lenovo\.m2\repository\aopalliance\aopalliance\1.0\aopalliance-1.0.jar;C:\Users\lenovo\.m2\repository\ch\qos\logback\logback-classic\1.1.2\logback-classic-1.1.2.jar;C:\Users\lenovo\.m2\repository\org\slf4j\slf4j-api\1.7.6\slf4j-api-1.7.6.jar;C:\Users\lenovo\.m2\repository\ch\qos\logback\logback-core\1.1.2\logback-core-1.1.2.jar;C:\Users\lenovo\.m2\repository\commons-codec\commons-codec\1.9\commons-codec-1.9.jar;C:\Users\lenovo\.m2\repository\commons-lang\commons-lang\2.6\commons-lang-2.6.jar;C:\Users\lenovo\.m2\repository\commons-logging\commons-logging\1.1.3\commons-logging-1.1.3.jar;C:\Users\lenovo\.m2\repository\commons-configuration\commons-configuration\1.10\commons-configuration-1.10.jar;C:\Users\lenovo\.m2\repository\org\apache\httpcomponents\fluent-hc\4.3.3\fluent-hc-4.3.3.jar;C:\Users\lenovo\.m2\repository\org\apache\httpcomponents\httpclient\4.3.3\httpclient-4.3.3.jar;C:\Users\lenovo\.m2\repository\org\apache\httpcomponents\httpcore\4.3.2\httpcore-4.3.2.jar;C:\Users\lenovo\.m2\repository\org\apache\httpcomponents\httpclient-cache\4.3.3\httpclient-cache-4.3.3.jar;C:\Users\lenovo\.m2\repository\org\apache\httpcomponents\httpmime\4.3.3\httpmime-4.3.3.jar;C:\Users\lenovo\.m2\repository\org\quartz-scheduler\quartz\2.2.1\quartz-2.2.1.jar;C:\Users\lenovo\.m2\repository\postgresql\postgresql\9.1-901-1.jdbc4\postgresql-9.1-901-1.jdbc4.jar;C:\Users\lenovo\.m2\repository\commons-dbutils\commons-dbutils\1.5\commons-dbutils-1.5.jar;C:\Users\lenovo\.m2\repository\c3p0\c3p0\0.9.1.2\c3p0-0.9.1.2.jar;C:\Users\lenovo\.m2\repository\junit\junit\4.12-beta-2\junit-4.12-beta-2.jar;C:\Users\lenovo\.m2\repository\org\hamcrest\hamcrest-core\1.3\hamcrest-core-1.3.jar

可以看到和图中所示的各个类加载器加载范围是一致的, 同时我们也可以看到, jvm提供的加载器所加载的目录所对应的系统属性值。

依赖顺序

那么问题来了。 假如我们的项目有个依赖包A被放到了jre/lib/ext目录下, 而这个依赖包依赖的另一个依赖包B放在项目目录, 这个时候我们可以加在成功吗?

答案是不可以的。

因为java的类加载机制是向上委托, 而不是向下委托, 也就是说ExtClassloader可以调用BootstrapClassLoader加载的类, AppClassLoader可以调用ExtClassloader和BootstrapClassLoader加载的类, 而这个过程反过来是不行的。

在我们的这个问题中, 依赖包A被ExtClassloader加载, B被AppClassloader加载, 这个时候A要引用B包中的类, 按照我们上面讲的机制是不可以的。 当然它并不是绝对的, java提供了一种绕开上述机制的方法, 下面我们会讲到。

反向依赖

那么怎么可以突破向上委托这种机制呢?

JDK 1.2提供了一个叫线程上下文类加载器, 对应代码就是java.lang.Thread中的方法getContextClassLoader()和setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。如果没有通过setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器。Java 应用运行的初始线程的上下文类加载器是系统类加载器。

那么什么情况下以上类加载机制会失效呢?

Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。 常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。这些 SPI 的接口由 Java 核心库来提供,如 JAXP 的 SPI 接口定义包含在 javax.xml.parsers包中。这些 SPI 的实现代码很可能是作为 Java 应用所依赖的 jar 包被包含进来,可以通过类路径(CLASSPATH)来找到,如实现了 JAXP SPI 的 Apache Xerces所包含的 jar 包。SPI 接口中的代码经常需要加载具体的实现类。如 JAXP 中的 javax.xml.parsers.DocumentBuilderFactory类中的 newInstance()方法用来生成一个新的 DocumentBuilderFactory的实例。这里的实例的真正的类是继承自 javax.xml.parsers.DocumentBuilderFactory,由 SPI 的实现所提供的。如在 Apache Xerces 中,实现的类是 org.apache.xerces.jaxp.DocumentBuilderFactoryImpl。而问题在于,SPI 的接口是 Java 核心库的一部分,是由引导类加载器来加载的;SPI 实现的 Java 类一般是由系统类加载器来加载的。引导类加载器是无法找到 SPI 的实现类的,因为它只加载 Java 的核心库。它也不能代理给系统类加载器,因为它是系统类加载器的祖先类加载器。也就是说,类加载器的代理模式无法解决这个问题。 引用自: http://www.ibm.com/developerworks/cn/java/j-lo-classloader/
线程上下文类加载器正好解决了这个问题。如果不做任何的设置,Java 应用的线程的上下文类加载器默认就是系统上下文类加载器。在 SPI 接口的代码中使用线程上下文类加载器,就可以成功的加载到 SPI 实现的类。线程上下文类加载器在很多 SPI 的实现中都会用到。

自定义类加载器

大多数情况下jdk提供的类加载器已经足够我们使用, 但是在某些特殊情况下, 需要我们编写自定义的类加载器来实现特定功能, 如OSGI框架。如我们在网络上传输类文件的时候对方加密了, 我们就要编写对应解密的类加载器来加载对方发过来的类。

自定义类加载器主要需要注意的就是继承ClassLoader, 并实现其中的findClass方法, 如下:

public class MyClassLoader extends ClassLoader{      @Override   protected Class<?> findClass(String name) throws ClassNotFoundException {}  }

这里再引用网络上其他人写好的一个类加载器示例来帮助大家更好的掌握自定义类加载器。
public class FileSystemClassLoader extends ClassLoader {         private String rootDir;         public FileSystemClassLoader(String rootDir) {           this.rootDir = rootDir;       }         protected Class<?> findClass(String name) throws ClassNotFoundException {           byte[] classData = getClassData(name);           if (classData == null) {               throw new ClassNotFoundException();           }           else {               return defineClass(name, classData, 0, classData.length);           }       }         private byte[] getClassData(String className) {           String path = classNameToPath(className);           try {               InputStream ins = new FileInputStream(path);               ByteArrayOutputStream baos = new ByteArrayOutputStream();               int bufferSize = 4096;               byte[] buffer = new byte[bufferSize];               int bytesNumRead = 0;               while ((bytesNumRead = ins.read(buffer)) != -1) {                   baos.write(buffer, 0, bytesNumRead);               }               return baos.toByteArray();           } catch (IOException e) {               e.printStackTrace();           }           return null;       }         private String classNameToPath(String className) {           return rootDir + File.separatorChar                   + className.replace('.', File.separatorChar) + ".class";       }    }