ClassLoader 学习笔记

来源:互联网 发布:数据库云平台 编辑:程序博客网 时间:2024/05/16 04:43
        作为一种即时编译的编程语言,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 loadedClass c = findLoadedClass(name); //查找已经加载的类,如果已经加载过,则没必要再次加载
                                         //如果移除这个检查,则可能会因重复定义而抛出LinkageErrorif (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]; //属性数量及属性池}

        详情请见:《java虚拟机规范》阅读(三) class文件格式

        第七, 类加载之后发生的事情
        类加载成功之后,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中执行。

参考资料:
java类加载原理解析
深入探讨java类加载器
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 被发现假的增值税发票怎么办 高铁票身份证验证失败怎么办 网上订的火车票查不到怎么办 已经参加工作想学个本科证怎么办 火车晚点耽误下一趟列车怎么办 门外装监控没有预留电线怎么办 框架柱主筋柱顶预留长度不够怎么办 遇到很嚣张的人怎么办 在地板砖上铺木地板卧室门怎么办 宝宝打预防针的本子丢了怎么办 宝宝打预防针本子丢了怎么办 打疫苗的本子丢了怎么办 麦客收割机麦秸里加麦粒怎么办 高铁乘务员身高不够怎么办 坐火车买到站票怎么办 买上车补票原票怎么办? 买的商务座补票怎么办 12306账号被别人登录怎么办 飞机不提供餐食怎么办 12306退票支付宝失败怎么办 12306重复支付怎么办支付宝 支付宝登的12306账号怎么办 没买儿童高铁票怎么办 网上订的机票怎么办托运 半夜买高铁票不出票怎么办 轻轨少买了一站怎么办 高铁火车票丢了怎么办 如果高铁票丢了怎么办 高铁票丢了怎么办 报销 高铁如果没赶上怎么办 高铁管家待核验怎么办 动车没有票了怎么办 12306取消订单3次怎么办 【12306取消订单3次怎么办】 火车票取消订单3次怎么办 12306收不到验证码怎么办 安逸花验证码次数限制怎么办 航班晚点导致错过转机怎么办 想去沈阳站送站怎么办 高铁没有赶上车怎么办 火车晚点赶不上下一趟车怎么办