深入分析ClassLoader原理

作者: 5e30faa7d323 | 来源:发表于2017-11-29 12:35 被阅读69次

    一、什么是ClassLoader?

    一个Java程序不是管是CS还是BS应用,都是由若干个.class文件组织而成的一个完整的Java应用程序,当程序在运行时,即会调用该程序的一个入口函数来调用系统的相关功能,而这些功能都被封装在不同的class文件当中,所以经常要从这个class文件中要调用另外一个class文件中的方法。

    如果另外一个文件不存在的,则会引发系统异常。而程序在启动的时候,并不会一次性加载程序所要用的所有class文件,而是根据程序的需要,通过Java的类加载机制(ClassLoader)来动态加载某个class文件到内存当中的,从而只有class文件被载入到了内存之后,才能被其它class所引用。所以ClassLoader就是用来动态加载class文件到内存当中用的。

    二、Java默认提供的三个ClassLoader

    1、BootStrap ClassLoader

    称为启动类加载器,是Java类加载层次中最顶层的类加载器,负责加载JDK中的核心类库,如:rt.jar、resources.jar、charsets.jar等,可通过如下程序获得该类加载器从哪些地方加载了相关的jar或class文件:

    System.out.println(System.getProperty("sun.boot.class.path"));
    

    输出如下:

    C:\Program Files\Java\jdk1.8.0_121\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_121\jre\lib\rt.jar;C:\Program Files\Java\jdk1.8.0_121\jre\lib\sunrsasign.jar;C:\Program Files\Java\jdk1.8.0_121\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_121\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_121\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_121\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_121\jre\classes

    可以看出加载的是$JAVA_HOME目录的lib下的包。

    2、Extension ClassLoader

    称为扩展类加载器,负责加载Java的扩展类库,默认加载JAVA_HOME/jre/lib/ext/目下的所有jar。

    System.out.println(System.getProperty("java.ext.dirs"));
    

    输出如下:

    C:\Program Files\Java\jdk1.8.0_121\jre\lib\ext;

    可以看出加载的是$JAVA_HOME目录lib\ext下的类。只要jar包放置这个位置,就会被虚拟机加载。一个常见的、类似的问题是,你将mysql的低版本驱动不小心放置在这儿,但你的Web应用程序的lib下有一个新的jdbc驱动,但怎么都报错,譬如不支持JDBC2.0的 DataSource,这时你就要当心你的新jdbc可能并没有被加载。这就是ClassLoader的delegate现象。常见的有log4j、 common-log、dbcp会出现问题,因为它们很容易被人塞到这个ext目录,或是Tomcat下的common/lib目录。

    3、App ClassLoader

    称为系统类加载器,负责加载应用程序classpath目录下的所有jar和class文件。

    我们创建的standalone应用的main class缺省情况下也是由它加载(通过Thread.currentThread().getContextClassLoader()查看)。实际开发中用ClassLoader更多时候是用其加载classpath下的资源,特别是配置文件,如ClassLoader.getResource(),比FileInputStream直接。

    注意:
    除了Java默认提供的三个ClassLoader之外,用户还可以根据需要定义自已的ClassLoader,而这些自定义的ClassLoader都必须继承自java.lang.ClassLoader类,也包括Java提供的另外二个ClassLoader(Extension ClassLoader和App ClassLoader)在内,但是Bootstrap ClassLoader不继承自ClassLoader,因为它不是一个普通的Java类,底层由C++编写,已嵌入到了JVM内核当中,当JVM启动后,Bootstrap ClassLoader也随着启动,负责加载完核心类库后,并构造Extension ClassLoader和App ClassLoader类加载器。

    类加载器 classloader 是具有层次结构的,也就是父子关系。其中,Bootstrap 是所有类加载器的父亲。如下图所示:

    classloader 的层次结构

    三、ClassLoader加载类的原理

    1、原理介绍

    先看ClassLoader的体系架构图:


    ClassLoader的体系架构

    ClassLoader使用的是双亲委托模型来搜索类的,每个ClassLoader实例都有一个父类加载器的引用(不是继承的关系,是一个包含的关系),虚拟机内置的类加载器(Bootstrap ClassLoader)本身没有父类加载器,但可以用作其它ClassLoader实例的的父类加载器。

    1、先检查需要加载的类是否已经被加载,这个过程是从下->上;

    2、如果没有被加载,则委托父加载器加载,如果加载不了,再由自己加载, 这个过程是从上->下

    当一个ClassLoader实例需要加载某个类时,它会试图亲自搜索某个类之前,先把这个任务委托给它的父类加载器,这个过程是由上至下依次检查的,首先由最顶层的类加载器Bootstrap ClassLoader试图加载,如果没加载到,则把任务转交给Extension ClassLoader试图加载,如果也没加载到,则转交给App ClassLoader 进行加载,如果它也没有加载得到的话,则返回给委托的发起者,由它到指定的文件系统或网络等URL中加载该类。如果它们都没有加载到这个类时,则抛出ClassNotFoundException异常。否则将这个找到的类生成一个类的定义,并将它加载到内存当中,最后返回这个类在内存中的Class实例对象。

    2、为什么要使用双亲委托这种模型呢?

    1)、避免重复加载。

    当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次。

    2)、考虑到安全因素。

    如果不使用这种委托模式,我们就可以随时使用自定义的ClassLoader来动态替代java核心api中定义的类型,这样会存在非常大的安全隐患,而双亲委托的方式,就可以避免这种情况。

    因为String已经在启动时就被引导类加载器(Bootstrcp ClassLoader)加载,所以用户自定义的ClassLoader永远也无法加载一个自己写的String,除非你改变JDK中ClassLoader搜索类的默认算法。

    3、JVM在搜索类的时候,如何判断两个class相同呢?

    JVM在判定两个class是否相同时,不仅要判断两个类名是否相同,而且要判断是否由同一个类加载器实例加载的。只有两者同时满足的情况下,JVM才认为这两个class是相同的。就算两个class是同一份class字节码,如果被两个不同的ClassLoader实例所加载,JVM也会认为它们是两个不同class。

    在一个单虚拟机环境下,标识一个类有两个因素:class的全路径名、该类的ClassLoader。

    类名和类加载器一起作为key

    四、自定义ClassLoader

    既然JVM已经提供了默认的类加载器,为什么还要定义自已的类加载器呢?

    因为Java中提供的默认ClassLoader,只加载指定目录下的jar和class,如果我们想加载其它位置的类或jar时,比如:我要加载网络上的一个class文件,通过动态加载到内存之后,要调用这个类中的方法实现我的业务逻辑。在这样的情况下,默认的ClassLoader就不能满足我们的需求了,所以需要定义自己的ClassLoader。

    自定义步骤
    1. 编写一个类继承自ClassLoader抽象类。
    2. 复写它的findClass()方法。
    3. findClass()方法中调用defineClass()

    defineClass()

    这个方法在编写自定义classloader的时候非常重要,它能将class二进制内容转换成Class对象,如果不符合要求的会抛出各种异常。

    注意点

    一个ClassLoader创建时如果没有指定parent,那么它的parent默认就是AppClassLoader。

    上面说的是,如果自定义一个ClassLoader,默认的parent父加载器是AppClassLoader,因为这样就能够保证它能访问系统内置加载器加载成功的class文件。

    自定义ClassLoader示例之DiskClassLoader。

    先定义Test.java

    public class Test {
    
        public void say(){
            System.out.println("Say Hello");
        }
    
    }
    

    然后将它编译过年class文件Test.class放到D:\lib这个路径下。

    我们编写DiskClassLoader的代码。

    import java.io.ByteArrayOutputStream;
    import java.io.File;
    import java.io.FileInputStream;
    import java.io.FileNotFoundException;
    import java.io.IOException;
    
    
    public class DiskClassLoader extends ClassLoader {
    
        private String mLibPath;
    
        public DiskClassLoader(String path) {
            // TODO Auto-generated constructor stub
            mLibPath = path;
        }
    
        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            // TODO Auto-generated method stub
    
            String fileName = getFileName(name);
    
            File file = new File(mLibPath,fileName);
    
            try {
                FileInputStream is = new FileInputStream(file);
    
                ByteArrayOutputStream bos = new ByteArrayOutputStream();
                int len = 0;
                try {
                    while ((len = is.read()) != -1) {
                        bos.write(len);
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
    
                byte[] data = bos.toByteArray();
                is.close();
                bos.close();
    
                return defineClass(name,data,0,data.length);
    
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
    
            return super.findClass(name);
        }
    
        //获取要加载 的class文件名
        private String getFileName(String name) {
            // TODO Auto-generated method stub
            int index = name.lastIndexOf('.');
            if(index == -1){ 
                return name+".class";
            }else{
                return name.substring(index)+".class";
            }
        }
    }
    

    我们在findClass()方法中定义了查找class的方法,然后数据通过defineClass()生成了Class对象。

    测试

    现在我们要编写测试代码。我们知道如果调用一个Test对象的say方法,它会输出”Say Hello”这条字符串。但现在是我们把Test.class放置在应用工程所有的目录之外,我们需要加载它,然后执行它的方法。

    import java.lang.reflect.InvocationTargetException;
    import java.lang.reflect.Method;
    
    public class ClassLoaderTest {
    
        public static void main(String[] args) {
          
            //创建自定义classloader对象。
            DiskClassLoader diskLoader = new DiskClassLoader("D:\\lib");
            try {
                //加载class文件
                Class c = diskLoader.loadClass("com.frank.test.Test");
    
                if(c != null){
                    try {
                        Object obj = c.newInstance();
                        Method method = c.getDeclaredMethod("say",null);
                        //通过反射调用Test类的say方法
                        method.invoke(obj, null);
                    } catch (InstantiationException | IllegalAccessException 
                            | NoSuchMethodException
                            | SecurityException | 
                            IllegalArgumentException | 
                            InvocationTargetException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
    
        }
    
    }
    

    测试结果:

    测试结果

    可以看到,Test类的say方法正确执行,也就是我们写的DiskClassLoader编写成功。

    参考:
    1、一看你就懂,超详细java中的ClassLoader详解
    2、 Java Classloader原理分析

    相关文章

      网友评论

        本文标题:深入分析ClassLoader原理

        本文链接:https://www.haomeiwen.com/subject/aihubxtx.html