Java类加载器学习总结

加载器概念

类加载器(classloader)用来加载Java类到Java虚拟机中。一般来说,Java虚拟机使用Java类的方式如下:Java源程序(.java文件)在经过Java编译器编译之后就被转换成Java字节代码(.class文件)。类加载器负责读取Java字节代码,并转换成java.lang.Class类的一个实例。每个这样的实例用来表示一个Java类。通过此实例的newInstance()方法就可以创建出该类的一个对象。

类加载器分类

JDK默认提供了如下几种ClassLoader:

1、BootstrpClassLoader Bootstrp加载器是用C++语言写的,它是在Java虚拟机启动后初始化的,它主要负责加载%JAVA_HOME%/jre/lib或者-Xbootclasspath参数指定的路径以及%JAVA_HOME%/jre/classes中的类,虚拟机出于安全等因素考虑,不会加载%JAVA_HOME%/jre/lib路径下存在的陌生类,开发者通过将要加载的非JDK自身的类放置到此目录下期待启动类加载器加载是不可能的。

2、ExtClassLoader Bootstrpclassloader加载ExtClassLoader,并且将ExtClassLoader的父加载器设置为Bootstrploader。ExtClassLoader是用Java写的,具体来说就是sun.misc.Launcher$ExtClassLoader,ExtClassLoader主要加载%JAVA_HOME%/jre/lib/ext,此路径下的所有classes目录以及java.ext.dirs系统变量指定的路径中类库。

3、AppClassLoader Bootstrpclassloader加载完ExtClassLoader后,就会加载AppClassLoader,并且将AppClassLoader的父加载器指定为ExtClassLoader。AppClassLoader也是用Java写成的,它的实现类是sun.misc.Launcher$AppClassLoader,另外我们知道ClassLoader中有个getSystemClassLoader方法,此方法返回的正是AppclassLoader。AppClassLoader主要负责加载classpath所指定的位置的类或者是jar文档,它也是Java程序默认的类加载器。

综上所述,它们之间的关系可以通过下图形象的描述:

双亲委托模型

Java中ClassLoader的加载采用了双亲委托机制,采用双亲委托机制加载类的时候采用如下的几个步骤:

1当前ClassLoader首先从自己已经加载的类中查询是否此类已经加载,如果已经加载则直接返回原来已经加载的类。每个类加载器都有自己的加载缓存,当一个类被加载了以后就会放入缓存,等下次加载时可以直接返回。

2当前classLoader的缓存中没有找到被加载的类时,委托父类加载器去加载,父类加载器采用同样的策略,首先查看自己的缓存,然后委托父类的父类去加载,一直到bootstrpClassLoader。

3当所有的父类加载器都没有加载的时候,再由当前的类加载器加载,并将其放入它自己的缓存中,以便下次有加载请求的时候直接返回。

线程上下文类加载器

ContextClassLoader是java.lang.Thread类的一个属性,Thread类中的方法getContextClassLoader()和setContextClassLoader(ClassLoadercl)用来获取和设置线程的上下文类加载器。如果没有通过setContextClassLoader(ClassLoadercl)方法进行设置的话,线程将继承其父线程的上下文类加载器。Java应用运行的初始线程的上下文类加载器是系统类加载器(AppClassLoader),在线程中运行的代码可以通过此类加载器来加载类和资源。

双亲委托模型并不能解决Java应用开发中会遇到的类加载器的全部问题。Java提供了很多服务提供者接口(ServiceProviderInterface,SPI),允许第三方为这些接口提供实现。常见的SPI有JDBC、JCE、JNDI、JAXP和JBI等。这些SPI的接口由Java核心库来提供,如JAXP的SPI接口定义包含在javax.xml.parsers包中(rt.jar)。这些SPI的实现代码很可能是作为Java应用所依赖的jar包被包含进来,可以通过类路径(CLASSPATH)来找到,如实现了JAXPSPI的ApacheXerces所包含的jar包。SPI接口中的代码经常需要加载具体的实现类。

如JAXP中的javax.xml.parsers.DocumentBuilderFactory类中的newInstance()方法用来生成一个新的DocumentBuilderFactory的实例。这里的实例的真正的类是继承自javax.xml.parsers.DocumentBuilderFactory,由SPI的实现所提供的。如在ApacheXerces中,实现的类是org.apache.xerces.jaxp.DocumentBuilderFactoryImpl。

而问题在于,SPI的接口是Java核心库的一部分,是由引导类加载器(BootstrpClassLoader)来加载的;SPI实现的Java类一般是由系统类加载器(APPClassLoader)来加载的。引导类加载器是无法找到SPI的实现类的,因为它只加载Java的核心库。它也不能代理给系统类加载器,因为它是系统类加载器的祖先类加载器。也就是说,类加载器的双亲委托模型无法解决这个问题。

线程ContextClassLoader正好解决了这个问题。如果不做任何的设置,Java应用的线程的ContextClassLoader默认就是系统上下文类(AppClassLoader)加载器。在SPI接口的代码中使用线程ContextClassLoader,就可以成功的加载到SPI实现的类。线程ContextClassLoader在很多SPI的实现中都会用到。

以下是一个简单的Java应用,用于测试ContextClassLoader。

import javax.xml.parsers.DocumentBuilderFactory;public class TestClassLoader { public static void main(String[] args) throws Exception {TestClassLoader test = new TestClassLoader();test.testLoad();}private void testLoad() throws Exception {// 1System.out.println(Thread.currentThread());// Thread[main,5,main]// 2System.out.println(Thread.currentThread().getContextClassLoader());// sun.misc.Launcher$AppClassLoader@19821f// 3DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance("org.apache.xerces.jaxp.DocumentBuilderFactoryImpl", null);}}

注释//2显示当前主线程使用的上下文类加载器是AppClassLoader,注释//3由DocumentBuilderFactory负责加载并实例化它的一个实现类,该实现类由ApacheXerces提供,同时在newInstance中传入了一个空的加载器。

public class DocumentBuilderFactory{public static DocumentBuilderFactory newInstance(String factoryClassName, ClassLoader classLoader){        try {            return (DocumentBuilderFactory) FactoryFinder.newInstance(factoryClassName, classLoader, false);        } catch (FactoryFinder.ConfigurationError e) {            throw new FactoryConfigurationError(e.getException(), e.getMessage());        }           }   //......}

DocumentBuilderFactory中的newInstance方法将加载的任务交给FactoryFinder来完成。

class FactoryFinder { static Object newInstance(String className, ClassLoader cl, boolean doFallback)        throws ConfigurationError    {        try {            //使用给定的加载器cl,获取DocumentBuilderFactoryImpl类的Class对象,此处的cl为null            Class providerClass = getProviderClass(className, cl, doFallback);             //创建DocumentBuilderFactoryImpl的实例                        Object instance = providerClass.newInstance();            }            return instance;        }         catch (ClassNotFoundException x) {            throw new ConfigurationError(                "Provider " + className + " not found", x);        }         catch (Exception x) {            throw new ConfigurationError(                "Provider " + className + " could not be instantiated: " + x,                x);        }   }   //......}

获取DocumentBuilderFactoryImpl实现类的Class对象在getProviderClass方法中完成。

class FactoryFinder { static private Class getProviderClass(String className, ClassLoader cl,            boolean doFallback) throws ClassNotFoundException     {        try {            if (cl == null) {                //1                cl = ss.getContextClassLoader();                if (cl == null) {                    throw new ClassNotFoundException();                }                else {                //2由上下文类加载器完成实现类的加载                    return cl.loadClass(className);                }            }             else {                return cl.loadClass(className);            }        }        catch (ClassNotFoundException e1) {            //......        }    }//......}

由于cl传入的是一个null值,所以需要从ss对象中获取一个上下文类加载器,那ss对象中的getContextClassLoader方法如何获取一个上下文加载器的呢?

class SecuritySupport  {    ClassLoader getContextClassLoader() throws SecurityException{        return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction() {         public Object run() {                ClassLoader cl = null;                //1                cl = Thread.currentThread().getContextClassLoader();                                if (cl == null)                    cl = ClassLoader.getSystemClassLoader();                                return cl;            }        });}//......}

该方法中的其他代码不需要管,由注释//1处可以看出最后return语句返回的cl加载器,就是从当前线程中获得的上下文类加载器。使用myeclipse调试跟踪代码可以看出,当前只有一个mian主线程。

主线程中的上下文类加载器默认就是AppClassLoader。方法getProviderClass()中的注释//2处使用AppClassLoader加载器来完成实现类的加载。在注释//2处为什么不使用FactoryFinder.Class.getClassLoader()来完成实现类的加载呢?

因为FactoryFinder类位于核心类库中,由Bootstrp类加载器完成加载,而实现类DocumentBuilderFactoryImpl在我们自己的java应用中,在Bootstrp加载器的搜索范围中,找不到该实现类,因此这样的方式无法完成类的加载。另外一个原因,即使将实现类放到Bootstrp加载器的搜索范围中,也是不能完成加载,因为Bootstrp只负责加载固定的核心类库,其他的字节码文件一律都不加载。

正常的双亲委派模型中,下层的类加载器可以使用上层父加载器加载的对象,但是上层父类的加载器不可能使用子类加载的对象(因为,正常情况下加载器只会向上搜索被加载的类,不会向下搜索)。而有些时候程序的确需要上层调用下层,这时候就需要线程上下文类加载器来处理,以上就是这样的一个例子。

自定义类加载器

虽然在绝大多数情况下,系统默认提供的类加载器实现已经可以满足需求。但是在某些情况下,您还是需要为应用开发出自己的类加载器。比如您的应用通过网络来传输Java类的字节代码,为了保证安全性,这些字节代码经过了加密处理。这个时候您就需要自己的类加载器来从某个网络地址上读取加密后的字节代码,接着进行解密和验证,最后定义出要在Java虚拟机中运行的类来。 下面将通过具体的实例来说明类加载器的开发。

文件系统类加载器

第一个类加载器用来加载存储在文件系统上的Java字节代码。完整的实现下所示。

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";}}

类FileSystemClassLoader继承自类java.lang.ClassLoader。一般来说,自己开发的类加载器只需要覆写findClass(Stringname)方法即可。java.lang.ClassLoader类的方法loadClass()封装了前面提到的双亲委托机制的实现。loadClass()方法会首先调用findLoadedClass()方法来检查该类是否已经被加载过;如果没有加载过的话,会调用父类加载器的loadClass()方法来尝试加载该类;如果父类加载器无法加载该类的话,就调用findClass()方法来查找该类。因此,为了保证类加载器都正确实现双亲委托机制,在开发自己的类加载器时,最好不要覆写loadClass()方法,而是覆写findClass()方法。

类FileSystemClassLoader的findClass()方法首先根据类的全名在硬盘上查找类的字节代码文件(.class文件),然后读取该文件内容,最后通过defineClass()方法来把这些字节代码转换成java.lang.Class类的实例。

参考

http://www.ibm.com/developerworks/cn/java/j-lo-classloader/

http://imtiger.net/blog/2009/11/09/java-classloader/

http://www.cnblogs.com/clownfish/p/3348173.html

http://www.cnblogs.com/realviv/articles/1906110.html

http://my.oschina.net/u/1182621/blog/146349

http://blog.csdn.net/vernonzheng/article/details/8461380

http://blog.chinaunix.net/uid-21227800-id-65887.html

获得幸福的二法门是珍惜你所拥有的、遗忘你所没有的

Java类加载器学习总结

相关文章:

你感兴趣的文章:

标签云: