美文网首页
ClassLoader总结

ClassLoader总结

作者: 知止9528 | 来源:发表于2019-01-19 11:14 被阅读4次

它的作用

它是用来加载 Class 的。它负责将 Class 的字节码形式转换成内存形式的 Class 对象。字节码可以来自于磁盘文件 *.class,也可以是 jar 包里的 *.class,也可以来自远程服务器提供的字节流,字节码的本质就是一个字节数组 []byte,它有特定的复杂的内部格式。

JVM 运行实例中会存在多个 ClassLoader,不同的 ClassLoader 会从不同的地方加载字节码文件。它可以从不同的文件目录加载,也可以从不同的 jar 文件中加载,也可以从网络上不同的静态文件服务器来下载字节码再加载。

jvm里ClassLoader的层次结构

类加载器层次结构.png

BootstrapClassLoader(启动类加载器)

负责加载 JVM 运行时核心类,加载System.getProperty("sun.boot.class.path")所指定的路径或jar

ExtensionClassLoader

负责加载 JVM 扩展类,比如 swing 系列、内置的 js 引擎、xml 解析器 等等,这些库名通常以 javax 开头,它们的 jar 包位于 JAVAHOME/lib/rt.jar文件中.
加载System.getProperty("java.ext.dirs")所指定的路径或jar。在使用Java运行程序时,也可以指定其搜索路径,例如:java -Djava.ext.dirs=d:\projects\testproj\classes HelloWorld。

AppClassLoader

才是直接面向我们用户的加载器,它会加载 Classpath 环境变量里定义的路径中的 jar 包和目录。我们自己编写的代码以及使用的第三方 jar 包通常都是由它来加载的。
加载System.getProperty("java.class.path")所指定的路径或jar。在使用Java运行程序时,也可以加上-cp来覆盖原有的Classpath设置,例如: java -cp ./lavasoft/classes HelloWorld

此外

URLClassLoader

那些位于网络上静态文件服务器提供的 jar 包和 class文件,jdk 内置了一个 URLClassLoader,用户只需要传递规范的网络路径给构造器,就可以使用 URLClassLoader 来加载远程类库了。URLClassLoader 不但可以加载远程类库,还可以加载本地路径的类库,取决于构造器中不同的地址形式。ExtensionClassLoader 和 AppClassLoader 都是 URLClassLoader 的子类,它们都是从本地文件系统里加载类库。

双亲委派机制

就是优先由父亲来加载,这样可以保证只有一份,不然每个子类都加载,我岂不是不知道哪个是正确的?
源码如下

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }
                //父亲都加载不到了,那就只能往下找,看下子类是否重写了findClass,这也是双亲委派机制的破坏
                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

关于双亲委派机制的破坏

也就是不是一直向上的,顶级父类都加载不到,那就只能向下了,即先想上,再向下

ClassLoader 里面有三个重要的方法 loadClass()、findClass() 和 defineClass()。

loadClass() 方法是加载目标类的入口,它首先会查找当前 ClassLoader 以及它的双亲里面是否已经加载了目标类,如果没有找到就会让双亲尝试加载,如果双亲都加载不了,就会调用 findClass() 让自定义加载器自己来加载目标类。ClassLoader 的 findClass() 方法是需要子类来覆盖的,不同的加载器将使用不同的逻辑来获取目标类的字节码。拿到这个字节码之后再调用 defineClass() 方法将字节码转换成 Class 对象。下面我使用伪代码表示一下基本过程.

class ClassLoader {

  // 加载入口,定义了双亲委派规则
  Class loadClass(String name) {
    // 是否已经加载了
    Class t = this.findFromLoaded(name);
    if(t == null) {
      // 交给双亲
      t = this.parent.loadClass(name)
    }
    if(t == null) {
      // 双亲都不行,只能靠自己了
      t = this.findClass(name);
    }
    return t;
  }

  // 交给子类自己去实现
  Class findClass(String name) {
    throw ClassNotFoundException();
  }

  // 组装Class对象
  Class defineClass(byte[] code, String name) {
    return buildClassFromCode(code, name);
  }
}

class CustomClassLoader extends ClassLoader {

  Class findClass(String name) {
    // 寻找字节码
    byte[] code = findCodeFromSomewhere(name);
    // 组装Class对象
    return this.defineClass(code, name);
  }
}

注意

不要轻易覆盖 loadClass 方法。否则可能会导致自定义加载器无法加载内置的核心类库。在使用自定义加载器时,要明确好它的父加载器是谁,将父加载器通过子类的构造器传入。如果父类加载器是 null,那就表示父加载器是「根加载器」。

Class.forName()与ClassLoader

    /**
     * Returns the {@code Class} object associated with the class or
     * interface with the given string name, using the given class loader.
     * Given the fully qualified name for a class or interface (in the same
     * format returned by {@code getName}) this method attempts to
     * locate, load, and link the class or interface.  The specified class
     * loader is used to load the class or interface.  If the parameter
     * {@code loader} is null, the class is loaded through the bootstrap
     * class loader.  The class is initialized only if the
     * {@code initialize} parameter is {@code true} and if it has
     * not been initialized earlier.
    大概意思就是你传入一个全路径名,我就给你返回对应的Class对象,但你没有指定类加载器时,我就用bootstrapClassLoader来加载,此外当为true时,如果Class之前还没有初始化,那么Class会立马初始化.
也从侧面说明ClassLoader本身是延时加载的     

     * <p> If {@code name} denotes a primitive type or void, an attempt
     * will be made to locate a user-defined class in the unnamed package whose
     * name is {@code name}. Therefore, this method cannot be used to
     * obtain any of the {@code Class} objects representing primitive
     * types or void.
     *
     * <p> If {@code name} denotes an array class, the component type of
     * the array class is loaded but not initialized.
     *
     * <p> For example, in an instance method the expression:
     *
     * <blockquote>
     *  {@code Class.forName("Foo")}
     * </blockquote>
     *
     * is equivalent to:
     *
     * <blockquote>
     *  {@code Class.forName("Foo", true, this.getClass().getClassLoader())}
     * </blockquote>
     *
     * Note that this method throws errors related to loading, linking or
     * initializing as specified in Sections 12.2, 12.3 and 12.4 of <em>The
     * Java Language Specification</em>.
     * Note that this method does not check whether the requested class
     * is accessible to its caller.
     *
     * <p> If the {@code loader} is {@code null}, and a security
     * manager is present, and the caller's class loader is not null, then this
     * method calls the security manager's {@code checkPermission} method
     * with a {@code RuntimePermission("getClassLoader")} permission to
     * ensure it's ok to access the bootstrap class loader.
     *
     * @param name       fully qualified name of the desired class
     * @param initialize if {@code true} the class will be initialized.
     *                   See Section 12.4 of <em>The Java Language Specification</em>.
     * @param loader     class loader from which the class must be loaded
     * @return           class object representing the desired class
     *
     * @exception LinkageError if the linkage fails
     * @exception ExceptionInInitializerError if the initialization provoked
     *            by this method fails
     * @exception ClassNotFoundException if the class cannot be located by
     *            the specified class loader
     *
     * @see       java.lang.Class#forName(String)
     * @see       java.lang.ClassLoader
     * @since     1.2
     */
    @CallerSensitive
    public static Class<?> forName(String name, boolean initialize,
                                   ClassLoader loader)
        throws ClassNotFoundException
    {
        Class<?> caller = null;
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            // Reflective call to get caller class is only needed if a security manager
            // is present.  Avoid the overhead of making this call otherwise.
            caller = Reflection.getCallerClass();
            if (sun.misc.VM.isSystemDomainLoader(loader)) {
                ClassLoader ccl = ClassLoader.getClassLoader(caller);
                if (!sun.misc.VM.isSystemDomainLoader(ccl)) {
                    sm.checkPermission(
                        SecurityConstants.GET_CLASSLOADER_PERMISSION);
                }
            }
        }
        return forName0(name, initialize, loader, caller);
    }

关于jvm类的加载

在Java中,类装载器把一个类装入Java虚拟机中,要经过三个步骤来完成:装载、链接和初始化,其中链接又可以分成校验、准备和解析三步,除了解析外,其它步骤是严格按照顺序完成的,各个步骤的主要工作如下:

    装载:查找和导入类或接口的二进制数据; //byte[]

  链接:执行下面的校验、准备和解析步骤,其中解析步骤是可以选择的;//包含了虚方法表的初始化

  校验:检查导入类或接口的二进制数据的正确性; //魔数 babycafe?

  准备:给类的静态变量分配并初始化存储空间; //分配内存空间

  解析:将符号引用转成直接引用; 

  初始化:激活类的静态变量的初始化Java代码和静态Java代码块。
//对应的为<clinit>()方法,该方法在多线环境中如果有多个线程同时去初始化一个类,那么久只有一个线程去执行。
//这也是我们写单例时,为什么可以使用静态内部类了,保证了内部重排序对外部线程时不可见的,具体可以见对DCL的分析

具体的可参见周志明老师的深入理解JAVA虚拟机

小结一下

Class.forName(className)方法,其实调用的方法是Class.forName(className,true,classloader);注意看第2个boolean参数,它表示的意思,在loadClass后必须初始化。在执行过此方法后,目标对象的 static块代码已经被执行,static参数也已经被初始化。

ClassLoader.loadClass(className)方法,其实他调用的方法是ClassLoader.loadClass(className,false);还是注意看第2个 boolean参数,该参数表示目标对象被装载后不进行链接,这就意味这不会去执行该类静态块中间的内容

所以,如果你在静态代码块里面初始化了东西,那么你就应该用Class.forName而不是直接用类加载器去加载

Thread.contextClassLoader

class Thread {
  ...
  private ClassLoader contextClassLoader;

  public ClassLoader getContextClassLoader() {
    return contextClassLoader;
  }

  public void setContextClassLoader(ClassLoader cl) {
    this.contextClassLoader = cl;
  }
  ...
}

contextClassLoader「线程上下文类加载器」,这究竟是什么东西?
首先 contextClassLoader 是那种需要显示使用的类加载器,如果你没有显示使用它,也就永远不会在任何地方用到它。你可以使用下面这种方式来显示使用它

Thread.currentThread().getContextClassLoader().loadClass(name);

这意味着如果你使用 forName(string name) 方法加载目标类,它不会自动使用 contextClassLoader。那些因为代码上的依赖关系而懒惰加载的类也不会自动使用 contextClassLoader来加载。

其次线程的 contextClassLoader 默认是从父线程那里继承过来的,所谓父线程就是创建了当前线程的线程。程序启动时的 main 线程的 contextClassLoader 就是 AppClassLoader。这意味着如果没有人工去设置,那么所有的线程的 contextClassLoader 都是 AppClassLoader。

那这个 contextClassLoader 究竟是做什么用的?

它可以做到跨线程共享类,只要它们共享同一个 contextClassLoader。父子线程之间会自动传递 contextClassLoader,所以共享起来将是自动化的。

如果不同的线程使用不同的 contextClassLoader,那么不同的线程使用的类就可以隔离开来。

如果我们对业务进行划分,不同的业务使用不同的线程池,线程池内部共享同一个 contextClassLoader,线程池之间使用不同的 contextClassLoader,就可以很好的起到隔离保护的作用,避免类版本冲突。tomcat和spring就用到了这个

如果我们不去定制 contextClassLoader,那么所有的线程将会默认使用 AppClassLoader,所有的类都将会是共享的。

相关文章

  • java ClassLoader 总结

    开始正式我们的ClassLoader总结分析。 ClassLoader的分类(Hot Spot JVM): Boo...

  • ClassLoader总结

    通读这篇文章你会知道如何回答以下问题: Java自带的三大加载器加载的jar位置都是在哪里? 三大加载器之间的关系...

  • ClassLoader总结

    它的作用 它是用来加载 Class 的。它负责将 Class 的字节码形式转换成内存形式的 Class 对象。字节...

  • Classloader总结

    ClassLoader classloader的最终目标是将我们的class文件变成字节,然后加载进来 BootS...

  • Android进阶解密⑤—热修复

    在此之前已经总结过ClassLoader的原理,以及通过ClassLoader方式实现的热修复思路,实现热修复的方...

  • Java ClassLoader总结

    Java ClassLoader总结 从一个题目开始 这段结果的输出如下: 23a=11,b=014 原因在于:S...

  • Android ClassLoader要点总结

    1.java classloader要点 1.1 类的加载流程 1.2 类的加载机制之双亲委派模型 避免重复加载:...

  • ClassLoader:Java Android 总结

    前言 无论是 Java 还是 Android,学习它们的类加载机制都非常重要的。本文统一记录两个平台下 Class...

  • 关于OSGI(Karaf) Classloader的几点说明

    1. Java ClassLoader Java通过Classloader加载Class,Classloader之...

  • ClassLoader,了解一下

    ClassLoader 类图: Android中ClassLoader的介绍 ClassLoader介绍:Clas...

网友评论

      本文标题:ClassLoader总结

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