美文网首页
java的类加载-ClassLoader

java的类加载-ClassLoader

作者: 可乐爱上咖啡 | 来源:发表于2018-01-05 16:29 被阅读389次

    最近一个项目需求,实现基于http接口的外部jar包动态类加载。我平台提供标准化的接口,接口的具体实现由业务方实现。业务方根据开发规范,实现接口后,打包成jar文件,上传至平台上,用户调用接口的时候,动态载入jar文件,运行结果返回。整个过程,业务方开发人员通过平台的管理页面配置,并上传实现的jar包,即可把能力添加到我平台上。
      整个项目的一个关键点事如何动态加载类文件,还必须实现动态更新。
      基于这一目标,对java类加载做了具体学习。

    类的装载方式

    首先我们来认识一下ClassLoader,程序在启动的时候,并不会一次性加载程序所要用的所有class文件,而是根据程序的需要,通过Java的类加载机制(ClassLoader)来动态加载某个 class 文件到内存当中的,从而只有 class 文件被载入到了内存之后,才能被其它 class 所引用。所以 ClassLoader 就是用来动态加载 class 文件到内存当中用的。

    ClassLoader主要对类的请求提供服务,当JVM需要某类时,它根据名称向ClassLoader要求这个类,然后由ClassLoader返回这个类的class对象。
    Classloader就是负责将class加载到JVM中,基于某个加载模型确定到底由谁加载,最后将 Class 字节码重新解析成 JVM 统一要求的对象格式。

    JAVA类装载方式,有两种:
    1.隐式装载,程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中。
    2.显式装载,通过class.forname(),this.getClassLoader().loadClass()等方法,显式加载需要的类。

    类的装载过程
    JVM加载类的阶段.png

    看一下ClassLoader中loadClass方法的实现:
    1.调用findLoaderClass查看是否已存在装入的类。存在就直接返回class对象(java中任何类型都有对应的class对象,哪怕是void也有)。
    2.findSystemClass查找本地文件系统,根据不同的类装载器,文件系统位置不一样。没有找到则返回ClassNotFoundException.
    3.difineClass 接收以字节数组表示的类字节码,并把它转换成 Class 实例
    4.resolveClass 链接一个指定的类。(jvm装载类的三个过程为load ,link,initialize)

    loadClass方法的默认实现按照以下顺序查找类: 调用findLoadedClass(String) 方法检查这个类是否被加载过 使用父加载器调用 loadClass(String) 方法,如果父加载器为 Null,类加载器装载虚拟机内置的加载器调用 findClass(String) 方法装载类, 如果,按照以上的步骤成功的找到对应的类,并且该方法接收的 resolve 参数的值为 true,那么就调用resolveClass(Class) 方法来处理类。
    ClassLoader 的子类最好覆盖 findClass(String) 而不是这个方法。 除非被重写,这个方法默认在整个装载过程中都是同步的(线程安全的)。

    类装载器

    ClassLoader 在加载类时有一定的层次关系和规则,树状组织结构。在 Java 中,有四种类型的类加载器,分别为:BootStrapClassLoader、ExtClassLoader、AppClassLoader 以及用户自定义的 ClassLoader


    类加载器层次结构图.png

    1.BootStrapClassLoader(引导类加载器) 处于类加载器层次结构的最高层,负责 sun.boot.class.path 路径下类的加载,默认为 jre/lib 目录下的核心 API 或 -Xbootclasspath 选项指定的 jar 包。
    2.ExtClassLoader (扩展类加载器)的加载路径为 java.ext.dirs,默认为 jre/lib/ext 目录或者 -Djava.ext.dirs 指定目录下的 jar 包加载。
    3.AppClassLoader(系统类加载器) 的加载路径为 java.class.path,默认为环境变量 CLASSPATH 中设定的值。也可以通过 -classpath 选型进行指定。
    4.用户自定义 ClassLoader 可以根据用户的需要定制自己的类加载过程,在运行期进行指定类的动态实时加载。

    一般来说,这四种类加载器会形成一种父子关系,高层为低层的父加载器。在进行类加载时,首先会自底向上挨个检查是否已经加载了指定类,如果已经加载则直接返回该类的引用。如果到最高层也没有加载过指定类,那么会自顶向下挨个尝试加载,直到用户自定义类加载器,如果还不能成功,就会抛出异常。

    对于同一个类加载器实例来说,名字相同的类只能存在一个,并且仅加载一次。不管该类有没有变化,下次再需要加载时,它只是从自己的缓存中直接返回已经加载过的类引用。

    类加载原理——双亲委托模型

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

    为什么要使用双亲委托这种模型呢?
    因为这样可以避免重复加载,当父亲已经加载了该类的时候,就没有必要 ClassLoader再加载一次。考虑到安全因素,我们试想一下,如果不使用这种委托模式,那我们就可以随时使用自定义的String来动态替代java核心api中定义的类型,这样会存在非常大的安全隐患,而双亲委托的方式,就可以避免这种情况,因为String已经在启动时就被引导类加载器(Bootstrcp ClassLoader)加载,所以用户自定义的ClassLoader永远也无法加载一个自己写的String,除非你改变JDK中ClassLoader搜索类的默认算法。

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

    Java装载类使用“全盘负责委托机制”。“全盘负责”是指当一个ClassLoder装载一个类时,该类所依赖及引用的类也由这个ClassLoder载入(除非显示的使用另外一个ClassLoder);“委托机制”是指先委托父类装载器寻找目标类,只有在找不到的情况下才从自己的类路径中查找并装载目标类。

    线程上下文类加载器

    每个运行中的线程都有一个成员ContextClassLoader,用来在运行时动态地载入其它类,可以使用方法Thread.currentThread().setContextClassLoader(...);更改当前线程的contextClassLoader,来改变其载入类的行为;也可以通过方法Thread.currentThread().getContextClassLoader()来获得当前线程的ClassLoader。
      Java 应用运行的初始线程的上下文类加载器是系统类加载器。在线程中运行的代码可以通过此类加载器来加载类和资源。

    实际上,在Java应用中所有程序都运行在线程里,如果在程序中没有手工设置过ClassLoader,对于一般的java类如下两种方法获得的ClassLoader通常都是同一个。

    this.getClass.getClassLoader();
    Thread.currentThread().getContextClassLoader();
    方法一得到的Classloader是静态的,表明类的载入者是谁;
    方法二得到的Classloader是动态的,谁执行(某个线程),就是那个执行者的Classloader。
    对于单例模式的类,静态类等,载入一次后,这个实例会被很多程序(线程)调用,对于这些类,载入的Classloader和执行线程的Classloader通常都不同。

    WEB容器类加载器

    对于运行在 Java EE容器中的 Web 应用来说,类加载器的实现方式与一般的 Java 应用有所不同。不同的 Web 容器的实现方式也会有所不同。
      以 Apache Tomcat 来说,每个 Web 应用都有一个对应的类加载器实例。该类加载器也使用代理模式,所不同的是它是首先尝试去加载某个类,如果找不到再代理给父类加载器。这与一般类加载器的顺序是相反的。这是 Java Servlet 规范中的推荐做法,其目的是使得 Web 应用自己的类的优先级高于 Web 容器提供的类。
      这种代理模式的一个例外是:Java 核心库的类是不在查找范围之内的。这也是为了保证 Java 核心库的类型安全。

    对于WEB APP线程,它的contextClassLoader是WebAppClassLoader
    对于Tomcat Server线程,它的contextClassLoader是CatalinaClassLoader

    自定义类加载器

    默认的classloader只能加载指定目录下的jar和class,如果想加载其它位置的类或jar时,需要定义自己的classloader
    定义自已的类加载器分为两步:
    1、继承java.lang.ClassLoader
    2、重写父类的findClass方法

    文件类加载器
      加载存储在文件系统上的 Java 字节代码。
      类 FileSystemClassLoader继承自类java.lang.ClassLoader。为了保证类加载器都正确实现代理模式,在开发自己的类加载器时,最好不要覆写 loadClass()方法,而是覆写findClass()方法。
      类 FileSystemClassLoader的 findClass()方法首先根据类的全名在硬盘上查找类的字节代码文件(.class 文件),然后读取该文件内容,最后通过 defineClass()方法来把这些字节代码转换成 java.lang.Class类的实例。

    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 (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
            return null;
        }
    
        private String classNameToPath(String className) {
            return rootDir + File.separatorChar + className.replace('.', File.separatorChar) + ".class";
        }
    }
    

    网络类加载器
      一个网络类加载器来说明如何通过类加载器来实现组件的动态更新。即基本的场景是:Java 字节代码(.class)文件存放在服务器上,客户端通过网络的方式获取字节代码并执行。当有版本更新的时候,只需要替换掉服务器上保存的文件即可。通过类加载器可以比较简单的实现这种需求。
      类 NetworkClassLoader 负责通过网络下载 Java 类字节代码并定义出 Java 类。它的实现与FileSystemClassLoader 类似。在通过 NetworkClassLoader 加载了某个版本的类之后,一般有两种做法来使用它。第一种做法是使用 Java 反射 API。另外一种做法是使用接口。

    OSGI动态模块系统

    osgi是java动态化模块系统,动态模块部署。面向服务和基于组件的运行环境。组件尽可能解耦,能让组件动态的发现其他组件。
    这个有兴趣也可以去研究关注一下。

    关于《java类的热替换》参考:
    https://www.ibm.com/developerworks/cn/java/j-lo-hotswapcls/

    相关文章

      网友评论

          本文标题:java的类加载-ClassLoader

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