美文网首页
【JAVA】类加载器

【JAVA】类加载器

作者: Y了个J | 来源:发表于2019-02-13 15:35 被阅读41次

    ClassLoader的简单介绍

    Class的装载大体上可以分为加载类、连接类和初始化三个阶段,在这三个阶段中,所有的Class都是由ClassLoader进行加载的,然后Java虚拟机负责连接、初始化等操作.也就是说,无法通过ClassLoader去改变类的连接和初始化行为.
    Java虚拟机会创建三类ClassLoader,分别是

    BootStrap ClassLoader(启动类加载器)
    Extension ClassLoader(扩展类加载器)
    APP ClassLoader(应用类加载器,也称为系统类加载器)

    此外,每个应用还可以自定义ClassLoader

    ClassLoader的双亲委托模式

    在ClassLoader的结构中,还有一个重要的字段parent,它也是一个ClassLoader的实例,这个字段字段表示的ClassLoader也成为这个ClassLoader的双亲,在类加载的过程中,可能会将某些请求交于自己的双亲处理.
    如图,应用类加载器的双亲为扩展类加载器,扩展类加载器的双亲为启动类加载器.

    屏幕快照 2019-01-25 下午12.30.52.png

    系统中的ClassLoader在协同工作时,默认会使用双亲委托模式.即在类加载的时候,系统会判断当前类是否已经被加载,如果被加载,就会直接返回可用的类,否则就会尝试加载,在尝试加载时,会先请求双亲处理,如果双亲请求失败,则会自己加载.

    双亲委托模式的弊端

    判断类是否加载的时候,应用类加载器会顺着双亲路径往上判断,直到启动类加载器.但是启动类加载器不会往下询问,这个委托路线是单向的,即顶层的类加载器,无法访问底层的类加载器所加载的类,如图

    屏幕快照 2019-01-25 下午12.32.32.png

    启动类加载器中的类为系统的核心类,比如,在系统类中,提供了一个接口,并且该接口还提供了一个工厂方法用于创建该接口的实例,但是该接口的实现类在应用层中,接口和工厂方法在启动类加载器中,就会出现工厂方法无法创建由应用类加载器加载的应用实例问题.
    拥有这样问题的组件有很多,比如JDBC、Xml parser等.JDBC本身是java连接数据库的一个标准,是进行数据库连接的抽象层,由java编写的一组类和接口组成,接口的实现由各个数据库厂商来完成

    双亲委托模式的补充

    在Java中,把核心类(rt.jar)中提供外部服务,可由应用层自行实现的接口,这种方式成为spi.那我们看一下,在启动类加载器中,访问由应用类加载器实现spi接口的原理

    Thread类中有两个方法

    public ClassLoader getContextClassLoader()//获取线程中的上下文加载器
    public void setContextClassLoader(ClassLoader cl)//设置线程中的上下文加载器
    

    通过这两个方法,可以把一个ClassLoader置于一个线程的实例之中,使该ClassLoader成为一个相对共享的实例.这样即使是启动类加载器中的代码也可以通过这种方式访问应用类加载器中的类了.如下图


    屏幕快照 2019-01-25 下午12.36.48.png
    1. Java 的类加载器的种类都有哪些?

    1、根类加载器(Bootstrap) --C++写的 ,看不到源码
    2、扩展类加载器(Extension) --加载位置 :jre\lib\ext 中
    3、系统(应用)类加载器(System\App) --加载位置 :classpath 中
    4、自定义加载器(必须继承 ClassLoader)

    1. 类什么时候被初始化?

    1)创建类的实例,也就是 new 一个对象
    2)访问某个类或接口的静态变量,或者对该静态变量赋值
    3)调用类的静态方法
    4)反射(Class.forName("com.lyj.load"))
    5)初始化一个类的子类(会首先初始化子类的父类)
    6)JVM 启动时标明的启动类,即文件名和类名相同的那个类

    只有这 6 中情况才会导致类的类的初始化。

    类的初始化步骤:

    1)如果这个类还没有被加载和链接,那先进行加载和链接
    2)假如这个类存在直接父类,并且这个类还没有被初始化(注意:在一个类加载器中,类只能初始化一 次),那就初始化直接的父类(不适用于接口)
    3)加入类中存在初始化语句(如 static 变量和 static 块),那就依次执行这些初始化语句。

    1. Java 类加载体系之 ClassLoader 双亲委托机制
      java 是一种类型安全的语言,它有四类称为安全沙箱机制的安全机制来保证语言的安全性,这四类安全 沙箱分别是:
    1. 类加载体系
    2. .class 文件检验器
    3. 内置于 Java 虚拟机(及语言)的安全特性
    4. 安全管理器及 Java API

    主要讲解类的加载体系:
    java 程序中的 .java 文件编译完会生成 .class 文件,而 .class 文件就是通过被称为类加载器的 ClassLoader加载的,而 ClassLoder 在加载过程中会使用“双亲委派机制”来加载 .class 文件,先上图:

    屏幕快照 2019-02-13 下午3.08.22.png

    BootStrapClassLoader:启动类加载器,一般用本地代码实现,负责加载 JVM 基础核心类库(rt.jar);该 ClassLoader 是 jvm 在启动时创建的,用于加 载 $JAVA_HOME$/jre/lib 下面的类库(或者通过参数-Xbootclasspath 指定)。由于启动类加载器涉及到虚拟机 本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不能直接通过引用进行操作。

    ExtClassLoader:扩展类加载器,从 java.ext.dirs 系统属性所指定的目录中加载类库,它的父加载器是 Bootstrap; 该 ClassLoader 是在 sun.misc.Launcher 里作为一个内部类 ExtClassLoader 定义的(即 sun.misc.Launcher$ExtClassLoader),ExtClassLoader 会加载 $JAVA_HOME/jre/lib/ext 下的类库(或者通过参数-Djava.ext.dirs 指定)。

    AppClassLoader:应用程序类加载器,该 ClassLoader 同样是在 sun.misc.Launcher 里作为一个内部类
    AppClassLoader 定义的(即 sun.misc.Launcher$AppClassLoader),AppClassLoader 会加载 java 环境变量 CLASSPATH 所指定的路径下的类库,而 CLASSPATH 所指定的路径可以通过 System.getProperty("java.class.path")获取;当然,该变量也可以覆盖,可以使用参数-cp,例如:java -cp 路 径 (可以指定要执行的 class 目录)。

    CustomClassLoader:自定义类加载器,该 ClassLoader 是指我们自定义的 ClassLoader,比如 tomcat 的 StandardClassLoader 属于这一类;当然,大部分情况下使用 AppClassLoader 就足够了。

    前面谈到了 ClassLoader 的几类加载器,而 ClassLoader 使用双亲委派机制来加载 class 文件的。ClassLoader 的双亲委派机制是这样的(这里先忽略掉自定义类加载器 CustomClassLoader):

    1)当 AppClassLoader 加载一个 class 时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父 类加载器 ExtClassLoader 去完成。
    2)当 ExtClassLoader 加载一个 class 时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给 BootStrapClassLoader 去完成。
    3)如果 BootStrapClassLoader 加载失败(例如在$JAVA_HOME$/jre/lib 里未查找到该 class),会使用 ExtClassLoader 来尝试加载;
    4)若 ExtClassLoader 也加载失败,则会使用 AppClassLoader 来加载,如果 AppClassLoader 也加载失败, 则会报出异常 ClassNotFoundException。

    下面贴下 ClassLoader 的 loadClass(String name, boolean resolve)的源码:

    protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException
        {
            synchronized (getClassLoadingLock(name)) {
                // First, check if the class has already been loaded,首先找缓存是否有class
                Class<?> c = findLoadedClass(name);
                if (c == null) {
                    long t0 = System.nanoTime();
                    try {//没有判断有没有父类
                        if (parent != null) {//有的话,用父类递归获取 class
                            c = parent.loadClass(name, false);
                        } else {//没有父类。通过这个方法来加载
                            c = findBootstrapClassOrNull(name);
                        }
                    } catch (ClassNotFoundException e) {
                    }
    
                    if (c == null) {
                        // If still not found, then invoke findClass in order, 如果还是没有找到,调用findClass(name)去找这个类
                        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;
            }
        }
    

    代码很明朗:首先找缓存(findLoadedClass),没有的话就判断有没有 parent,有的话就用 parent 来递归 的 loadClass,然而 ExtClassLoader 并没有设置 parent,则会通过 findBootstrapClassOrNull 来加载 class,而 findBootstrapClassOrNull 则会通过 JNI 方法”private native Class findBootstrapClass(String name)“来使用 BootStrapClassLoader 来加载 class。
    然后如果 parent 未找到 class,则会调用 findClass 来加载 class,findClass 是一个 protected 的空方法, 可以覆盖它以便自定义 class 加载过程。
    另外,虽然 ClassLoader 加载类是使用 loadClass 方法,但是鼓励用 ClassLoader 的子类重写 findClass(String),而不是重写 loadClass,这样就不会覆盖了类加载默认的双亲委派机制。

    双亲委派托机制为什么安全
    举个例子,ClassLoader 加载的 class 文件来源很多,比如编译器编译生成的 class、或者网络下载的字节码。
    而一些来源的 class 文件是不可靠的,比如我可以自定义一个 java.lang.Integer 类来覆盖 jdk 中默认的 Integer 类,例如下面这样:

    public class Integer {
        public Integer(int value) {
            System.exit(0);
        }
    }
    

    初始化这个 Integer 的构造器是会退出 JVM,破坏应用程序的正常进行,如果使用双亲委派机制的话该 Integer 类永远不会被调用,以为委托 BootStrapClassLoader 加载后会加载 JDK 中的 Integer 类而不会加载自定义 的这个,可以看下下面这测试个用例:

    public static void main(String... args) {
            Integer i = new Integer(1);
            System.err.println(i);
        }
    

    执行时 JVM 并未在 new Integer(1)时退出,说明未使用自定义的 Integer,于是就保证了安全性。

    描述一下 JVM 加载 class
    JVM 中类的装载是由类加载器(ClassLoader)和它的子类来实现的,Java 中的类加载器是一个重要的 Java 运 行时系统组件,它负责在运行时查找和装入类文件中的类。

    由于 Java 的跨平台性,经过编译的 Java 源程序并不是一个可执行程序,而是一个或多个类文件。当 Java 程序 需要使用某个类时,JVM 会确保这个类已经被加载、连接(验证、准备和解析)和初始化。类的加载是指把类的.class 文件中的数据读入到内存中,通常是创建一个字节数组读入.class 文件,然后产生与所加载类对应的 Class 对象。加 载完成后,Class 对象还不完整,所以此时的类还不可用。当类被加载后就进入连接阶段,这一阶段包括验证、准备 (为静态变量分配内存并设置默认的初始值)和解析(将符号引用替换为直接引用)三个步骤。最后 JVM 对类进行 初始化,包括:
    如果类存在直接的父类并且这个类还没有被初始化,那么就先初始化父类;
    如果类中存在初始化语句,就依次执行这些初始化语句。类的加载是由类加载器完成的,类加载器包括:根加载器 (BootStrap)、扩展加载器(Extension)、系统加载器(System)和用户自定义类加载器(java.lang.ClassLoader 的子类)。
    从 Java 2(JDK 1.2)开始,类加载过程采取了父亲委托机制(PDM)。PDM 更好的保证了 Java 平台的安全 性,在该机制中,JVM 自带的 Bootstrap 是根加载器,其他的加载器都有且仅有一个父类加载器。类的加载首先请求 父类加载器加载,父类加载器无能为力时才由其子类加载器自行加载。JVM 不会向 Java 程序提供对 Bootstrap 的引 用。

    相关文章

      网友评论

          本文标题:【JAVA】类加载器

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