美文网首页JVM
深入理解JVM 2 - 类加载器

深入理解JVM 2 - 类加载器

作者: 冬狮郎 | 来源:发表于2019-12-25 22:20 被阅读0次

类加载器类型

Java虚拟机自带的加载器

  • 根类加载器(Bootstrap)
    该类加载器没有父类加载器。它负责加载虚拟机的核心类库(jre/lib/rt.jar里所有的class,由c++实现),如java.lang.* 等。Object类就是由根类加载器加载的。根类加载器从系统属性System.out.println(System.getProperty("sun.boot.class.path"));所指定的目录中加载类库。根类加载器的实现依赖于底层操作系统,属于虚拟机的实现的一部分,它并没有继承java.lang.ClassLoader类。
    java -Dsun.boot.class.path=./ com.ethan.jvm.classload.MyTest23
  • 扩展类加载器(Extension)
    它的父类加载器为根类加载器,它从System.out.println(System.getProperty("java.ext.dirs"));系统属性所指定的目录中加载类库,或者从JDK的安装目录的jre\lib\ext子目录(扩展目录)下加载类库,如果把用户创建的JAR文件放在这个目录下,也会自动由扩展类加载器加载(只加载jar包),扩展类加载器是纯Java类,是java.lang.ClassLoader类的子类。通过-Djava.ext.dirs指定。
    java -Djava.ext.dirs=./ com.ethan.jvm.classload.MyTest23
  • 系统(应用)类加载器(System)
    也称为应用类加载器,它的父类加载器为扩展类加载,它从环境变量classpath或者系统属性System.out.println(System.getProperty("java.class.path"));所指定的目录中加载类,它是用户自定义的类加载器的默认父加载器,系统类加载器是纯Java类,是java.lang.ClassLoader类的子类。
    java -Djava.system.class.loader=com.ethan.jvm.classload.MyTest16 com.ethan.jvm.classload.MyTest23(自己指定类加载器)

用户自定义的类加载器

  • java.lang.ClassLoader的子类
  • 用户可以定制类的加载方法(需要继承ClassLoader类)

类加载器执行顺序

类加载器的顺序
内建于JVM中的启动类加载器会加载java.lang.ClassLoader以及其他的Java平台类。
当JVM启动时,一块特殊的机器码会运行,它会加载扩展类加载器与系统类加载器,这块特殊的机器码叫:启动类加载器(Bootstrap)
启动类加载器并不是Java类,而其他的加载器则都是Java类。
启动类加载器是特定与平台的机器指令,它负责开启整个加载过程。
所有的类加载器(除了启动类加载器)都被实现为Java类。不过总归要由一个组件来加载第一个Java类加载器,从而让整个加载过程能够顺利进行下去。
加载第一个纯Java类加载器就是启动类加载器的职责。
启动类加载器还会负责加载供JRE正常运行所需要的基本组件,这包括java.util与java.lang包中的类。

类加载器用来把类加载到Java虚拟机中。从JDK1.2版本开始,类的加载过程采用父亲委托机制,这种机制能更好的保证Java平台的安全,在此委托机制中,除了Java虚拟机自带的根类加载器以外,其余的类加载器都有且只有一个父加载器。

类加载器的顺序

当JAVA程序请求加载器loader1加载Sample类时,loader1首先委托自己的父加载器去加载Sample类,若父加载器能加载,则由父加载器完成加载任务,否则才由加载器loader1本身加载Sample类。最终,是由系统类加载器将Sample类加载成功。这也就是双亲委托机制:在双亲委托机制中,各个加载器按照父子关系形成树形结构,除了根加载器之外,其余的类加载器都有且只有一个父加载器。

类的加载器并不需要等到某个类被"首次主动使用"时再加载它。

  • JVM规范允许类加载器在预料某个类将要被使用时就预先加载它。如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)。
  • 如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误

概念

其他方式的类加载器定义

定义类加载器:有一个类加载器能够成功加载了Test类,那么这个类加载器被称为定义类加载器;
初始类加载器:所有能成功返回Class对象引用的类加载器(包括定义类加载器)都被称为初始类加载器。

获得ClassLoader的途径

// 获得当前类的ClassLoader
class.getClassLoader();

// 获得当前线程上下文的ClassLoader
Thread.currentThread().getContextClassLoader();

// 获得系统的ClassLoader
ClassLoader.getSystemClassLoader();

// 获得调用者的ClassLoader
DriverManager.getCallerClassLoader();

特殊的数组类加载器

        /**
         * 数组类的class不是由类加载器生成,而是在JVM运行期间需要被创建的时候动态生成的。
         * <p> <tt>Class</tt> objects for array classes are not created by class
         * loaders, but are created automatically as required by the Java runtime.
         *
         * 数组类型的类加载器,由Class.getClassLoader()返回,和数组元素的类型是相同的。
         * The class loader for an array class, as returned by {@link
         * Class#getClassLoader()} is the same as the class loader for its element
         * type;
         *
         * 如果数组类型是原生类型,那么数组类就没有类加载器
         * if the element type is a primitive type, then the array class has no
         * class loader.
         *
         * @see ClassLoader
         */

命名空间

  • 每个类加载器都有自己的命名空间,命名空间由该类加载器及所有父加载器所加载的类组成

  • 在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类;

  • 在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类;

  • 同一个命名空间内的类时相互可见的。

  • 子加载器的命名空间包含所有父加载器的命名空间。因此由子类加载器加载的类能看见父类加载器加载的类。例如,系统类加载器AppClassLoader加载的类能看到根类加载器RootClassLoader加载的类。

  • 由父加载类加载的类不能看见子类加载器加载的类。

  • 如果两个加载器之间没有直接或间接的父子关系,那么他们各自加载的类相互不可见。

  • 在运行期,一个Java类是由该类的完全限定名(binary name二进制名)和用于加载该类的定义类加载器(defining loader)所共同决定的。如果同样名字(即同样的完全限定名)的类是由两个不同的类加载器所加载,那么这些类就是不同的,即便.class文件的字节码完全一样,并且从相同的位置加载也是如此。

类加载器的双亲委托模型的好处:

1、可以确保Java核心库的类型安全

所有的Java应用都至少会引用java.lang.Object类,也就是说在运行期,java.lang.Object这个类会被加载到Java虚拟机中。如果这个加载过程时由Java应用自己的类加载器所完成的,那么很可能就会在JVM中存在多个版本的java.lang.Object类,而且这些类之间还是不兼容的,相互不可见的。正是命名空间在发挥着作用。借助于双亲委托机制,Java核心类库中的类的加载工作都是由启动类加载器来统一完成,从而确保了Java应用所使用的都是同一个版本的Java核心类库,他们之间是相互兼容的。

2、可以确保Java核心类库所提供的类不会被自定义的类所替代。

3、不同的类加载器可以为相同名称(binary name)的类创建额外的命名空间

相同名称的类可以并存在Java虚拟机中,只需要用不同的类加载器来加载他们即可。不同的类加载器所加载的类之间是不兼容的。这就相当于在Java虚拟机内部创建了一个又一个相互隔离的Java类空间,这类技术在很多框架中都得到了实际使用。

当前类加载器:(Current Classloader)
每个类都会使用自己的类加载器(即加载自身的类加载器)去加载其他类(指的是所依赖的类)。如果ClassX引用了ClassY,那么ClassX的类加载器就会去加载ClassY(前提是ClassY尚未加载)

线程上下文类加载器(Context ClassLoader)
线程上下文类加载器是从JDK1.2开始引入的,类Thread中的getContextClassLoader()与setContextClassLoader(ClassLoader cl)分别用来获取和设置上下文类加载器。如果没有通过与setContextClassLoader(ClassLoader cl)进行设置的话,线程将继承其父线程的上下文类加载器。Java应用运行时的初始线程的上下文类加载器是系统类加载器,在线程中运行的代码可以通过该类加载器来加载类与资源。

线程上下文类加载器的重要性:
SPI(Service Provider Interface)
父ClassLoader可以使用当前线程Thread.currentThread().getContextClassLoader()所指定的classloader加载的类。这就改变了父ClassLoader不能使用子ClassLoader或是其他没有直接父子关系的ClassLoader加载的类的情况,即改变了双亲委托模型。

线程上下文类加载器就是当前线程的CurrentClassloader。

在双亲委托模型下,类加载时由下至上的,即下层的类加载器会委托上层进行加载。但是对于SPI来说,有些接口是Java核心库所提供的,而Java核心库是由启动类加载器来加载的,而这些接口的实现却来自于不同的jar包(厂商提供),Java的启动类加载器是不会加载其他来源的Jar包。这样传统的双亲委托模型就无法满足SPI的要求。而通过当前线程设置上下文类加载器,就可以由设置的上下文类加载器来实现对于接口实现类的加载。

相关文章

网友评论

    本文标题:深入理解JVM 2 - 类加载器

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