现在的位置: 首页 > 综合 > 正文

ClassLoader 学习笔记

2012年02月11日 ⁄ 综合 ⁄ 共 9317字 ⁄ 字号 评论关闭
        作为一种即时编译的编程语言,ClassLoader是Java程序运行的基础。虽然,大部分和我一样的攻城狮平时都不需要和ClassLoader打交道,但是相信大家对于ClassNotFoundExecption和NoClassDefFoundError多多少少有些印象。这两个类都和ClassLoader关系密切。

        首先,java虚拟机实现了三个类加载器:
        1. 启动(Bootstrap)类加载器: 负责加载<java_runtime_home>/lib下的类库,由本地代码实现,主要用于初始化java虚拟机,属于java虚拟机本地实现的部分。
        2. 标准扩展(Extension)类加载器:负责加载<java_runtime_home>/lib/ext下的类库,由java实现,开发者可以使用该加载器。
        3. 系统(system)类加载器:负责加载CLASSPATH路径下的类库,通常开发者自己编写的类库也由该加载器负责加载(bin文件夹也在CLASSPATH路径中),可以由ClassLoader.getSystemClassLoader()获取到该加载器的索引。
        此外,开发者自己编写的类加载器为自定义类加载器。

        第二,双亲委派机制:
        除了,启动类加载器,其他三类(标准扩展类加载器,system类加载器以及自定义加载器)都直接或间接继承自java.long.ClassLoader。
        以下为ClassLoader的部分实现代码(JDK1.6):
        
    // The parent class loader for delegation
    private ClassLoader parent;


    /**
     * Creates a new class loader using the specified parent class loader for
     * delegation.
     *
     * <p> If there is a security manager, its {@link
     * SecurityManager#checkCreateClassLoader()
     * <tt>checkCreateClassLoader</tt>} method is invoked.  This may result in
     * a security exception.  </p>
     *
     * @param  parent
     *         The parent class loader
     *
     * @throws  SecurityException
     *          If a security manager exists and its
     *          <tt>checkCreateClassLoader</tt> method doesn't allow creation
     *          of a new class loader.
     *
     * @since  1.2
     */
    protected ClassLoader(ClassLoader parent) {
        this(checkCreateClassLoader(), parent);
    }

    /**
     * Creates a new class loader using the <tt>ClassLoader</tt> returned by
     * the method {@link #getSystemClassLoader()
     * <tt>getSystemClassLoader()</tt>} as the parent class loader.
     *
     * <p> If there is a security manager, its {@link
     * SecurityManager#checkCreateClassLoader()
     * <tt>checkCreateClassLoader</tt>} method is invoked.  This may result in
     * a security exception.  </p>
     *
     * @throws  SecurityException
     *          If a security manager exists and its
     *          <tt>checkCreateClassLoader</tt> method doesn't allow creation
     *          of a new class loader.
     */
    protected ClassLoader() {
        this(checkCreateClassLoader(), getSystemClassLoader()); //默认情况下会使用SystemClassLoader作为parent
    }
    /**
     * Loads the class with the specified <a href="#name">binary name</a>.  The
     * default implementation of this method searches for classes in the
     * following order:
     *
     * <p><ol>
     *
     *   <li><p> Invoke {@link #findLoadedClass(String)} to check if the class
     *   has already been loaded.  </p></li>
     *
     *   <li><p> Invoke the {@link #loadClass(String) <tt>loadClass</tt>} method
     *   on the parent class loader.  If the parent is <tt>null</tt> the class
     *   loader built-in to the virtual machine is used, instead.  </p></li>
     *
     *   <li><p> Invoke the {@link #findClass(String)} method to find the
     *   class.  </p></li>
     *
     * </ol>
     *
     * <p> If the class was found using the above steps, and the
     * <tt>resolve</tt> flag is true, this method will then invoke the {@link
     * #resolveClass(Class)} method on the resulting <tt>Class</tt> object.
     *
     * <p> Subclasses of <tt>ClassLoader</tt> are encouraged to override {@link
     * #findClass(String)}, rather than this method.  </p>
     *
     * @param  name
     *         The <a href="#name">binary name</a> of the class
     *
     * @param  resolve
     *         If <tt>true</tt> then resolve the class
     *
     * @return  The resulting <tt>Class</tt> object
     *
     * @throws  ClassNotFoundException
     *          If the class could not be found
     */
    protected synchronized Class<?> loadClass(String name, boolean resolve)
	throws ClassNotFoundException
    {
	// First, check if the class has already been loaded
	Class c = findLoadedClass(name); //查找已经加载的类,如果已经加载过,则没必要再次加载
                                         //如果移除这个检查,则可能会因重复定义而抛出LinkageError
	if (c == null) {
	    try {
		if (parent != null) {     //先委托父加载器加载class
		    c = parent.loadClass(name, false);
		} else {                  //如果parent == null, 则我们认为父加载器为启动类加载器
		    c = findBootstrapClassOrNull(name);
		}
	    } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }
            if (c == null) {
	        // If still not found, then invoke findClass in order
	        // to find the class.
	        c = findClass(name); //如果父类加载器未能完成加载,则由子类具体实现
	    }
	}
	if (resolve) {
	    resolveClass(c);// 进行链接
	}
	return c;
    }


        




        从loadClass函数的实现,我们发现加载器进行加载时,首先会委托父类加载器尝试加载,这个就是双亲委派机制。

        四种类型的父子关系如下图,从代码上来说,BootStrapClassLoader并不是ExtensionClassLoader的parent(实际上BootStrapClassLoader为本地语言实现,所以ExtensionClassLoader的parent=null),但是从逻辑上来说,parent=null等效于parent=BootStrapClassLoader。

        以下是四类类加载器的父子关系示意图:
        

        所以,我们可以通过ClassLoader.getSystemClassLoader().getParent()来获得ExtensionClassLoader的索引。
        
        值得一提的是,双亲委派机制能较好的满足大部分java应用的需求。但是,在一些特殊场景下,也有为了提升性能而修改双亲委派机制的情况,例如:先由当前加载器加载类,加载失败再委托父类加载器加载。
        
        第三,每一个ClassLoader都对应一个命名空间,而jvm通过这个命名空间+class的全名(包括package name)唯一的标识一个类。所以,有几个问题需要注意:
       1. 同一个类其实是可以重复加载的,如果你使用两个不同的类加载器来load同一个class,则可以在jvm存在两个完全相同的类定义(它们的命名空间不同)。
       2. 由第一问题,引申出来的问题,同一个类经由不同的类加载器加载,对于JVM来说就是不同的类,所以,可能发生ClassCastException,例如:
      
 ClassA a = new ClassA();    // ClassA经由ClassLoader1加载

ClassA b = null; // ClassA经由ClassLoader2加载

b = (ClassA)a; //抛出ClassCastException

    3. 因为双亲委派机制的存在,所以,执行ClassLoaderA.loadClass("com.example.classA")加载的classA并不一定是由ClassLoaderA加载的,也有可能是由其父加载器加载的,例如(SystemClassLoader)。这种情况下,ClassLoaderA被成为classA的初始类加载器,而SystemClassLoader为定义类加载器,
类的命名空间由定义类加载器。
 
        第四,java动态加载
       当项目存在一些特殊需求,例如:
        1. app运行时需要从网络获取最近的class/jar文件,动态更新
        2. app有较高的安全需求,对class/jar文件做了额外的加密操作,使通常的类加载器无法解析class/java文件
        等特殊情况,就需要考虑动态加载.
        常用的java动态加载方案有Class.forName和自定义类加载器。

        利用Class.forName实现动态加载的常见案例是JDBC驱动的加载。

        Class.forName函数有两种重载:
        
    public static Class<?> forName(String className) 
                throws ClassNotFoundException {
        return forName0(className, true, ClassLoader.getCallerClassLoader());//默认情况下调用Class.forName函数的调用者的ClassLoader进行加载(classloader参数=ClassLoader.getCallerClassLoader()),并完成连接和初始化(initialize参数=true)。
    }
    public static Class<?> forName(String name, boolean initialize,
				   ClassLoader loader)
        throws ClassNotFoundException
    {
	if (loader == null) {
	    SecurityManager sm = System.getSecurityManager();
	    if (sm != null) {
		ClassLoader ccl = ClassLoader.getCallerClassLoader();
		if (ccl != null) {
		    sm.checkPermission(
			SecurityConstants.GET_CLASSLOADER_PERMISSION);
		}
	    }
	}
	return forName0(name, initialize, loader);
    }

       
       使用自定义类加载器实现动态加载的常见案例是,利用实现代码的动态更新(或许这个特性未来会在云OS这种特定平台上大展身手)。
       其实理论上来说,标准扩展类加载器和系统类加载器也可以实现动态加载,但是,一般来说,这两个加载器内类扫描路径(<java-runtime-home>/lib/ext,以及CLASSPATH)下的文件不会变动,所以一般都以静态方式使用。
       根据sun的建议实现一个自定义类加载器还是比较简单的。 继承ClassLoader,并覆盖findClass函数(在sun的标准实现中,标准扩展类加载器和系统类加载器都继承自URLClassLoader,它是ClassLoader的一个子孙类,所以,个人觉得自定义类加载器继承自URLClassLoader也是各不错的选择):
以下代码摘录自:http://www.ibm.com/developerworks/cn/java/j-lo-classloader/
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"; 
    } 
 }
        ClassLoader中比较值得注意的函数有:
        1. loadClass函数,此函数实现了已加载类的检查和双亲委派机制的实现。没有不要的情况下,不要覆盖这个函数。
        2. findClass函数,此函数由loadClass函数调用,负责寻找需要加载的类对应的字节码(.class文件的内容),如果找不到对应的字节码,则抛出ClassNotFoundException。
        3. defineClass,此函数由findClass函数调用,负责解析字节码进而生成对应的类。如果解析失败,则抛NoClassDefFoundError。不建议覆盖。
        java动态加载虽然带来了优势,可以让开发者实现很多功能,但是,也存在一定的副作用,因为app的局部是动态变化的,那么静态的部分就无法使用通常的方式来调用动态更新的类。所以,一般需要通过如下两种方式来调用:
       1. 接口,动态加载的类始终实现指定的接口,静态部分通过接口来调用动态加载的类。
       2. 反射(Reflect),通过指定的类名、函数名(成员名)来调用动态加载的类。
       
        第五,线程上下文类加载器
        线程上下文下载器可以由Thread.setContextClassLoader函数和Thread.getContextClassLoader函数设置和获取。线程上下文类加载器可以是任何继承自ClassLoader的加载器实例,可以是系统类加载器,也可以是自定义类加载器。默认情况下,线程会继承其父线程的ContextClasLoader,而java初始线程的ContextClassLoader为系统类加载器,所以,在未设定的情况下,所有的线程上下文类加载器为系统类加载器。
       
       java为了提高开放性,提供了很多服务提供者接口(Service Provider Interface,SPI)。而双亲委派机制在这些情形下出现了问题。以JAXP(java xml 解析API)为例: javax.xml.parsers(JAXP的SPI接口)定义由java核心库提供,由启动类加载器负责加载,而对应的实现 Apache
xercers则存在于ClassPath路径下,由系统类加载器负责加载。根据class.forName函数的默认实现规则,java.xml.parsers包内的类会使用启动类加载器去加载Apache的实现类,但是启动类加载器为系统类加载器的父类加载器,启动类加载器不会,也无法调用系统加载器,所以,会导致加载失败。
       为了解决这些问题,从java1.2开始引入了上下文加载器,以便SPI接口加载其实现类。

       第六,java字节码的格式
       ClassLoader内的findClass最终由native代码实现,我一直无缘得见如何将.class文件解析为class,所以去查询了一些其他的资料。

       java字节码的格式在《jvm虚拟机规范》中有描述。
       
ClassFile { 
    u4 magic;          // magic number, 固定为0xCAFEBABE
    u2 minor_version;  // 子版本号, 一般为0x0
    u2 major_version;  // 主版本号, 由java的版本号决定,java1.5=0x31, java1.6=0x32, java1.7=0x33
    u2 constant_pool_count; //常量池长度
    cp_info constant_pool[constant_pool_count-1];  //常量池
    u2 access_flags;  // 访问标志,private, package, public...
    u2 this_class; // 本类,为常量池内的有效索引
    u2 super_class; // 超类,为常量池内的有效索引
    u2 interfaces_count; // 实现的接口数量
    u2 interfaces[interfaces_count]; // 实现的接口池
    u2 fields_count; field_info fields[fields_count]; // 成员数量以及成员池 
    u2 methods_count; method_info methods[methods_count]; // 方法数量及方法池
    u2 attributes_count; attribute_info attributes[attributes_count]; //属性数量及属性池
}


        第七, 类加载之后发生的事情
        类加载成功之后,jvm会开始执行链接操作(ClassLoader.resolveClass()就是链接一个类)。
        链接操作分为如下三个小步骤:
         1. 检验:检验操作是为了保证java字节码是正确的。.class文件可能本身不是有效的文件(例如空文件,或者由.mp3
文件修改后缀而来),也有可能是因为java版本不符(java1.5的虚拟机无法解析java1.6的class文件)。如果验证成功则继续链接,否则抛出java.lang.VerifyError错误。
         2. 准备:为类的静态变量分配内存空间,初始化默认值。
         3. 解析:需要链接的java类一般都会包含对于其他类和接口的引用(包括其父类,实现的接口,方法的参数、返回值所涉及到的类)。解析的目的就是为了保证这些类可以被找到。常用的解析策略包括递归解析和用时解析。递归解析的就是递归加载依赖的接口和类。而更常用的策略则是用时解析,当真正需要使用这个类的时候在进行解析。
        
        链接成功后,jvm接下来会进行初始化。初始化操作主要是执行静态代码块和初始化静态域。
        初始化静态域和链接操作中的准备步骤不一样。以如下代码为例:
private static int number = 1;

       准备步骤进行的操作,其实在heap上分配4个字节的空间,并在将其初始化为0;

       而初始化静态域则是把刚才刚才分配的空间设置为1。
       
       执行静态代码块,这是class文件内的代码第一次被执行。
       总结,经过jvm加载,链接,初始化三个步骤的操作,一个class文件转变为java.long.Class的子类,可以在jvm中执行。
参考资料:

抱歉!评论已关闭.