类加载分类

类加载器:
  Java虚拟机自带的加载器:
      根类加载器(Boostrap Classloader)
      扩展类加载器(Extension Classloader)
      系统(应用)类加载器(System Classloader/ app ClassLoader)
  用户自定义的类加载器:
      java.class.ClassLoader的子类
      用户可以定制类的加载方式

根类加载器(Bootstrap ClassLoader)

该加载器没有父加载器, 它负责加载虚拟机的核心类库, 如java.lang.*等, 根类加载器从系统属性
sun.boot.class.path所指定的目录中加载类库, 根类加载器的实现依赖于底层操作系统, 属于虚拟机实现的
一部分, 它并没有继承于java.lang.ClassLoader

扩展类加载器(Extension ClassLoader / Platform ClassLoader)

它的父加载器为根类加载器, 它从java.ext.dirs系统属性所指定的目录中加载类库, 或者从JDK的安装目录
的jre/lib/ext扩展目录下加载类库, 如果把用户创建的jar文件放在这个目录下, 也会自动由扩展类加载器
加载。扩展类加载器是纯java类, 是java.lang.ClassLoader类的子类

系统类加载器(System ClassLoader / Application ClassLoader)

它的父类加载器是扩展类加载器, 它从环境变量classpath或者系统属性java.class.path所指定的目录中加
载类, 它是用户自定义的类加载器的默认父加载器, 系统类加载器是纯java类, 是java.lang.ClassLoader
类的子类

注意: 虽然这里说是父加载器, 但它们在类的表现上不是继承关系

预加载概念

<1> 类加载器并不需要等到某个类被"首次主动使用"时再加载它
<2> JVM规范运行类加载器在预料某个类将要被使用时就预先加载它, 如果在预先加载的过程中遇到了class文件
    缺失或存在错误, 类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)
<3> 如果这个类一直没有被程序主动使用, 那么类加载器就不会报告错误

类加载的双亲委派机制

概念:在父亲委托机制中, 各个加载器按照父子关系形成了树形结构, 除了根类加载器之外, 其余的类加载器都
    有且只有一个父加载器, 自底向上检查是否类已经被加载, 然后自顶向下尝试加载类   

好处: (下面的类加载器可以认为是类加载器的实例)
  <1> 可以确保Java核心库的类型安全: 所有的Java应用都至少会引用java.lang.Object类, 也就是说在运行
      期, java.lang.Object这个类会被加载到Java虚拟机中; 如果这个加载过程是由Java应用自己的类加载
      器所完成的, 那么很可能就会在JVM中存在多个版本的java.lang.Object类的Class对象, 而且这些类还
      是不兼容的, 相互不可见的(命名空间发挥着作用)
  <2> 借助于双亲委托机制, Java核心库中类的加载工作都是由启动类加载器来统一完成的, 从而确保Java应用
      所使用的都是同一个版本的Java核心类库, 它们之间是相互兼容的
  <3> 可以确保Java核心类库所提供的类不会被自定义的类所替代
  <4> 不同的类加载器可以为相同名称(binary name)的类创建不同的命名空间, 相同名称的类可以并存在Java
      虚拟机中, 只需要用不同的类加载器来加载它们即可, 不同类加载器所加载的类之间是不兼容的, 这就相
      当于在Java虚拟机内部创建了一个又一个相互隔离的Java类空间

类加载的具体过程

我们在调用类ClassLoader的实例的loadClass方法的时候, 会经历下面几个阶段
<1> 调用该类加载器的findLoadedClass方法, 查询该类加载器加载的所有类中是否存在需要被加载的类, 存在
    则返回, 不存在则进行下一步
<2> 调用parent.loadClass方法来委派给父类加载器, 父类加载器同样会执行第一步, 如果没有找到, 则继续
    委派父类的父类加载器, 直到委派到根类加载器, 根类加载器的代码是native的, 如果根类加载器能够加载
    该类(即在根类加载器中规定的加载路径下存在该类class文件), 则直接返回该加载的Class对象到最初的
    加载器, 如果不能加载就让上一个加载器加载
<3> 如果父类加载器没有加载该类或者不能加载该类, 那么就调用本次的类加载器的findClass方法来进行加载

实现自定义的ClassLoader

如果我们要实现一个自定义的ClassLoader, 根据类加载的过程以及源代码, 我们需要重写findClass方法来进行
类的加载, 以及添加一个方法能够将class文件读取成一个字节数组, 下面先用代码描述:

public class MyClassLoader extends ClassLoader {
  // findClass方法通过loadClassData来将一个class文件读取进内存并以字节数组的形式保存
  // 然后调用native方法defineClass来构建一个Class对象
  @Override
  public Class<?> findClass (String name) {
        byte[] data = loadClassData( name );
        return defineClass( name, data, 0, data.length );
  }

  private byte[] loadClassData (String name) {
    // 根据name找到class文件并通过输入流读取进内存以字节数组的形式保存
  }
}

查看自定义的ClassLoader代码

命名空间

<1> 每个类加载器都有自己的命名空间, 命名空间由该加载器(更直观的可以认为是该加载器类对象的实例loader)
    及所有父加载器(系统类加载器以及以上的加载器都是单例的)所加载的类组成
<2> 在同一个命名空间中, 不会出现类的完整名字(包括类的包名)相同的两个类
<3> 在不同的命名空间中, 有可能会出现类的完整名字(包括类的包名)相同的两个类
<4> 子加载器所加载的类能够访问到父加载器所加载的类
<5> 父加载器所加载的类无法访问到子加载器所加载的类

同一个命名空间内的类是相互可见的, 子加载器的命名空间包含所有父加载器的命名空间。因此由子加载器加载
的类能看见父加载器加载的类,例如系统类加载器加载的类能够看见根类加载器加载的类

由父加载器加载的类不能看见子加载器加载的类, 如果两个加载器之间没有直接或间接地父子关系, 那么它们各自
加载地类相互不可见

例子:
    自定义的MyClassLoader, 创建一个该类的实例A和B, A去加载test.class和B去加载test.class得出的
    Class对象是不同的, 而父类加载器AppClassLoader的实例是单例的, 所以说任何一个AppClassLoader加
    载的类Class对象都是相同的, 所以A + AppClassLoader + 父类 + 父类加载的类为一个命名空间, 而B
    自身也会形成一个这样的命名空间, 两个命名空间中系统自带的类加载器加载的类是一样的, 而A和B加载的
    是不一样的Class对象

证明命名空间中的4, 5

  • 子加载器所加载的类能够访问到父加载器所加载的类

    // 我们使用自定义类加载器MyClassLoader2(代码就不贴出来了, 在下一个文件中可以看到)
    TS1类: 
      public class TS1 {
        public TS1 () {
          new TS2();
          System.out.println( TS2.class );
        }
      }
    
    TS2类:
      public class TS2 {
        public TS2 () {
          System.out.println( "创建TS2的实例: " + this.getClass().getClassLoader() );
        }
      }
    
    测试代码:
      MyClassLoader2 loader = new MyClassLoader2("loader1", ClassLoader.getSystemClassLoader());
      loader.setPrefix( "C:\\Users\\zhongshenglong\\Desktop\\" );
      Class<?> cls = loader.loadClass( "com.fightzhong.jvm.classloader.TS1" );
      System.out.println( cls.getClassLoader() );
      cls.newInstance();
    
    前提准备:
      <1> 首先我们创建了一个自定义的ClassLoader类实例loader, 并且指定了其双亲为AppClassLoader(默认
          就是这个), 并且设置了该ClassLoader加载的类的路径为我的计算机的桌面
      <2> 整个项目被我放在了D盘的idea目录, src里面放的是我编写的代码, 其中上面所有类的源代码都放在那里,
          并且这些源代码编译后的class文件被放在了idea目录下的target/classes目录下, 这些都是Maven的配置
          了
      <3> 将编译后的所有的class文件复制一份放在桌面, 这样就能够通过自定义的loader来加载了
      <4> 删除源代码编译后的目录下的TS1.class文件, 执行测试代码
    
    代码执行分析:
      <1> loader.loadClass( "com.fightzhong.jvm.classloader.TS1" ), 委派给AppClassLoader去加载,
          由于系统类加载器加载的目录默认是在项目下的, 并且之前我们已经删除了项目下的TS1.class文件, 所
          以loader的双亲以上的加载器是不能找到文件去加载的, 此时就会轮到loader本身去加载, 而loader本身
          加载的路径在桌面, 刚才我们桌面放置了一份这些class文件的副本, 所以loader加载成功, 先输出如下
      <2> com.fightzhong.jvm.classloader.MyClassLoader2@511baa65
          创建TS1的实例: com.fightzhong.jvm.classloader.MyClassLoader2@511baa65    
      <3> 此时执行了TS1中的new TS2(), 根据类加载器的规范, 应当由loader去加载, 同时采用双亲委派机制,
          此时因为在项目中存在TS2.class文件, 所以AppClassLoader加载成功, 返回Class对象, 并输出如下
      <4> 创建TS2的实例: jdk.internal.loader.ClassLoaders$AppClassLoader@2437c6dc  
      <5> 执行TS1中的System.out.println( TS2.class ), 不会报错, 说明子类加载器所加载的类TS1能够访问
          到父类加载器所加载的类TS2
    
  • 父类加载器加载的类不能访问到子类加载器加载的类

    // 本次证明跟上面的类似, 只需要改一下代码即可
    TS1类: 
      public class TS1 {
        public TS1 () {
          System.out.println( "创建TS1的实例: " + this.getClass().getClassLoader() );
          new TS2();
        }
      }
    
    TS2类:
      public class TS2 {
        public TS2 () {
          System.out.println( "创建TS2的实例: " + this.getClass().getClassLoader() );
          System.out.println( TS1.class );
        }
      }
    
    测试代码(没变, 可跳过):
      MyClassLoader2 loader = new MyClassLoader2("loader1", ClassLoader.getSystemClassLoader());
      loader.setPrefix( "C:\\Users\\zhongshenglong\\Desktop\\" );
      Class<?> cls = loader.loadClass( "com.fightzhong.jvm.classloader.TS1" );
      System.out.println( cls.getClassLoader() );
      cls.newInstance();
    
    前提准备(没变, 可跳过):
      <1> 首先我们创建了一个自定义的ClassLoader类实例loader, 并且指定了其双亲为AppClassLoader(默认
          就是这个), 并且设置了该ClassLoader加载的类的路径为我的计算机的桌面
      <2> 整个项目被我放在了D盘的idea目录, src里面放的是我编写的代码, 其中上面所有类的源代码都放在那里,
          并且这些源代码编译后的class文件被放在了idea目录下的target/classes目录下, 这些都是Maven的配置
          了
      <3> 将编译后的所有的class文件复制一份放在桌面, 这样就能够通过自定义的loader来加载了
      <4> 删除源代码编译后的目录下的TS1.class文件, 执行测试代码
    
    代码执行分析:
      <1> loader.loadClass( "com.fightzhong.jvm.classloader.TS1" ), 委派给AppClassLoader去加载,
          由于系统类加载器加载的目录默认是在项目下的, 并且之前我们已经删除了项目下的TS1.class文件, 所
          以loader的双亲以上的加载器是不能找到文件去加载的, 此时就会轮到loader本身去加载, 而loader本身
          加载的路径在桌面, 刚才我们桌面放置了一份这些class文件的副本, 所以loader加载成功, 先输出如下
      <2> com.fightzhong.jvm.classloader.MyClassLoader2@511baa65
          创建TS1的实例: com.fightzhong.jvm.classloader.MyClassLoader2@511baa65    
      <3> 此时执行了TS1中的new TS2(), 根据类加载器的规范, 应当由loader去加载, 同时采用双亲委派机制,
          此时因为在项目中存在TS2.class文件, 所以AppClassLoader加载成功, 返回Class对象, 并输出如下
      <4> 创建TS2的实例: jdk.internal.loader.ClassLoaders$AppClassLoader@2437c6dc  
      <5> 执行TS2构造函数中的System.out.println( TS1.class ), 由于TS2由父类加载器AppClassLoader加
          载, 而TS1由loader加载, 此时会报类找不到错误, 所以证明了父类加载器不能访问子类加载器加载的类
    

类加载器加载路径

在jdk8中, 有这么几个系统属性, 分别代表了不同的类加载器加载的路径

"sun.boot.class.path": 该属性的值表示根类加载器加载类的路径
"java.ext.dirs": 该属性的值表示扩展类加载器加载类的路径
"java.class.path": 该属性的值表示系统类加载器加载类的路径
通过System.getProperty( 属性名 )可以依次获取上述的值

例子一:
  <1> 根据自己的项目文件, com.fightzhong.test下面创建了一个TestClass类
  <2> 将这个com文件夹整个copy到jdk/jre/classes中
  <3> 在项目中执行TestClass, 会发现TestClass是由根类加载器加载的

例子二:
  <1> 根据自己的项目文件, com.fightzhong.test下面创建了一个TestClass类
  <2> 将这个com文件夹执行jar cvf com.jar com命令打包成jar文件, 并放入jdk/jre/lib/ext中
  <3> 在项目中执行TestClass, 会发现TestClass是由扩展类加载器加载的

注意: 将一个class文件放入指定的类加载器加载目录下, 需要注意这个文件中的完整包名和在目录下的完整包名,
      扩展类加载器只能加载jar文件

线程上下文类加载器

  • 为什么要有线程上下文加载器

    我们知道, 不管是系统内置的类加载器还是自定义的类加载器, 都会遵守双亲委派机制, 由下向上查找类是否已
    经加载, 然后由上向下去加载类, 所以低层的类加载器能够看到高层的类加载器加载的类, 而高层的类加载器加
    载的类不能看到底层的类加载器加载的类, 并且还有一个特性就是如果类A依赖于类B, 类B依赖于类C, 那么最终
    类A,B都会由加载了类C的加载器去加载, 双亲委派机制在保护系统类的唯一性的同时也会引起一些麻烦。
    
    麻烦:
      当高层类加载器加载的类需要使用底层类加载器加载的类时, 由于双亲委派机制, 高层的命名空间看不见底
      层的命名空间, 所以就会导致使用不了, 比如jdbc, 我们获取一个连接数据库的对象是通过DriverManager
      来的,DriverManager.getConnection( url, username, password ), 通过这段代码就能获取到
      Connection对象,那么我们知道DriverManager是JDK内置的类, 是通过根类加载器加载的, getConnection
      方法获取到的类为数据库厂商提供的类, 根据双亲委派模型, 根类加载器是没办法加载我们引入的jar包的
      类的
    
    DriverManager静态代码块:
      static {
        loadInitialDrivers();
      }  
    
    loadInitialDrivers代码部分实现:
      AccessController.doPrivileged(new PrivilegedAction<Void>() {
          public Void run() {
              ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
              Iterator<Driver> driversIterator = loadedDrivers.iterator();
    
              try{
                  while(driversIterator.hasNext()) {
                      driversIterator.next();
                  }
              } catch(Throwable t) {
              }
              return null;
          }
      });
    
    分析:
      这个run方法执行的时候, 会去读取/METAINFO/service里面的文件名为java.sql.Driver的文件, 然后通过
      迭代的方式来获取里面的驱动类的全类名, 并加载这些类, 加载是通过反射Class.forName()方式去加载的,
      因为DriverManager类是由根类加载器加载的, 所以其没办法去加载文件中的类名代表的类, 那么为了能够去
      加载, 就利用了线程上下文类加载器(默认是系统类加载器AppClassLoader), 所以其能够在DriverManager
      里面用其它加载器去加载这些类
    
    总结: 线程上下文类加载器ContextClassLoader的作用是破坏双亲委派模型, 使得高层加载器加载的类如Driver
          Manager能够在内部去加载其它的类, 如数据库厂商提供的com.mysql.cj.jdbc.Driver, 它通过读取
          META-INF/services/下面的类的全类名, 然后利用线程上下文类加载器去加载, 从而实现了高层类加
          载器加载的类能够加载底层加载器加载的类
    
    注意: 线程上下文类加载器可以自己定义, 默认是AppClassLoader
    

类加载器总结

内建于JVM中的启动类加载器会加载java.lang.ClassLoader以及其它的Java平台类, 当JVM启动时, 一块特殊
的机器码会运行, 他会加载扩展类加载器与系统类加载器, 这块特殊的机器码叫做启动类加载器(Bootstrap)

启动类加载器并不是Java类, 而其它的加载器则都是Java类, 启动类加载器时特定与平台的机器指令, 他负责开
启整个加载过程, 总归要有一个组件来加载第一个Java类加载器, 从而让整个加载过程能够顺利进行下去, 加载
第一个纯Java类加载器就是启动类加载器的职责, 启动类加载器还会负责加载供JRE正常运行所需要的基本组件,
这包括java.util与java.lang包中类等等

results matching ""

    No results matching ""