• 09 Java的类加载以及类加载器


    1 Java类加载的阶段(区分类与实例)

    1-1概述

    类的生命周期:加载,链接(验证,准备,解析),初始化,使用,卸载

    1-1-1 类加载的三个基本阶段

    加载过程的基本步骤:JVM会动态的加载,链接,初始化class和interface,主要分为类文件的加载,链接,初始化。在链接阶段可以细分为验证,准备和解析

    • 加载:加载就是寻找类/接口的二进制标识,并且根据二进制表示创建类/接口
     Loading is the process of finding the binary representation of a class or interface type with a particular name and creating a class or interface from that binary representation
    
    • 链接:组合类/接口让JVM执行
    Linking is the process of taking a class or interface and combining it into the run-time state of the Java Virtual Machine so that it can be executed.
    
    • 初始化:更多参考 init
    Initialization of a class or interface consists of executing the class or interface initialization method <clinit>
    

    1-1-2 Java的类卸载

    类卸载必须满足的条件

    1. 该类所有的实例已经被回收
    2. 加载该类的ClassLoder已经被回收
    3. 该类对应的java.lang.Class对象没有任何对方被引用
    

    总结:.由java虚拟机自带的三种类加载加载的类在虚拟机的整个生命周期中是不会被卸载的,由用户自定义的类加载器所加载的类才可以被卸载

    Java 类何时会被卸载

    Unloading classes in java?

    1-2 类加载的阶段1:加载

    1-2-0 加载阶段的流程

    加载是类加载的一个阶段,注意不要混淆

    加载过程完成以下三件事:

    • 通过类的完全限定名称获取定义该类的二进制字节流。
    • 将该字节流表示的静态存储结构转换为方法区的运行时存储结构。
    • 在内存中生成一个代表该类的 Class 对象,作为方法区中该类各种数据的访问入口。

    其中二进制字节流可以从以下方式中获取:

    • 从 ZIP 包读取,成为 JAR、EAR、WAR 格式的基础。
    • 从网络中获取,最典型的应用是 Applet。
    • 运行时计算生成,例如动态代理技术,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 的代理类的二进制字节流。
    • 由其他文件生成,例如由 JSP 文件生成对应的 Class 类

    1-2-1 加载的内容

    类文件加载到方法区后,采用 C++ 的 instanceKlass 描述 java 类 ,这个类中有以下的field:

    field(成员名称) 说明 备注
    _java_mirror java 的类镜像,作用是将 klass 暴露给 java 使用 例如对 String 来说,就是通过String.class的类对象访问_java_mirror
    _super 该Java类的父类
    fields 该Java类的成员变量
    _methods 该Java类的成员方法
    _constants 该Java类的常量池信息
    _class_loader 类加载器
    _vtable 该Java类的虚方法表 实现多态的关键
    _itable 该Java类相关联的接口表

    注意点:

    • 类加载过程中如果这个类还有父类没有加载,先加载父类
    • 加载和链接可能是交替运行

    1-2-2 问题:Java的类对象与实例对象的区别?

    类对象: 类对象是由JVM(类加载器)创建Class对象,通过getClass()方法获取到Class对象,通过类对象可以查看当前class的一系列信息,注意类对象也位于堆中。

    实例对象: 通过new关键在创建的对象,一般也位于堆中。

    1-2-3 class文件加载到内存后的总体视图

    从上图中可以看到

    • instanceKlass 这样的【元数据】是存储在方法区(1.8 后的元空间内),_java_mirror有Person.class地址,同时类对象Person.class也拥有方法区的instanceKlass的地址
    • JDK1.8中类对象位于堆中,方法区中存储的是类的结构信息instanceKlass。

    场景:使用new关键字创建一系列Person的实例对象如何调用方法?

    关键点:实例对象中对象头包含class对象的地址以及mark word.

    1)获取实例对象的对象头中class对象在堆内存的地址。
    2)从class对象中获取元空间中instanceKlass地址。
    3)通过instanceKlass调用相关方法。
    

    对象头与对象头大小的计算

    1-3 类加载的阶段2:链接

    类加载过程中链接阶段的可以大致分为三个阶段:1)验证 2)准备 3)解析

    1-3-1 链接阶段的验证

    主要看class文件是否符合格式要求,比如class文件的魔术不是类文件的数值就会报错。

    1-3-2 链接阶段的准备(为静态变量分配空间/赋值)

    主要是为 static 变量分配空间,设置默认值

    • static 变量在 JDK 7 之前存储于 instanceKlass 末尾,从 JDK 7 开始,存储于 _java_mirror 末尾
    • static 变量分配空间和赋值是两个步骤,分配空间在准备阶段完成赋值在初始化阶段完成
      • 特例1:static 变量是 final 的基本类型/字符串常量,编译阶段(转化为字节码)即赋值,赋值在准备阶
        段完成
    • 如果 static 变量是 final 的,但属于引用类型,那么赋值在初始化阶段完成 ,初始化阶段需要调用构造函数。
    Java的静态变量存储位置?
    • 在JDK1.7前与类的结构存储在一起,即位于方法区。
    • 从JDK7开始与class对象存储在一起,比如JDK8中静态变量就位于class对象所在的堆中。

    静态变量准备阶段即可确定值的实例

    public class test10 {
        static int a;                                 // 1) 准备阶段仅仅进行空间分配
        static int b = 10;                            // 2) 准备阶段完成赋值并初始化
        static final int c = 20;                      // 3) 准备阶段完成赋值并初始化
        static final String d = "hello";              // 4) 准备阶段完成赋值并初始化
        static final Object e = new Object();         // 5) 准备阶段仅仅进行空间分配,初始化阶段完成赋值,通过构造函数实现
        public static void main(String[] args) {
        }
    }
    
    

    字节码

      Last modified 2021-4-14; size 616 bytes
      MD5 checksum b791c66694977da49daa6b77040df897
      Compiled from "test10.java"
    public class part3.test10
      minor version: 0
      major version: 52
      flags: ACC_PUBLIC, ACC_SUPER
    Constant pool:
       #1 = Methodref          #3.#31         // java/lang/Object."<init>":()V
       #2 = Fieldref           #5.#32         // part3/test10.b:I
       #3 = Class              #33            // java/lang/Object
       #4 = Fieldref           #5.#34         // part3/test10.e:Ljava/lang/Object;
       #5 = Class              #35            // part3/test10
       #6 = Utf8               a
       #7 = Utf8               I
       #8 = Utf8               b
       #9 = Utf8               c
      #10 = Utf8               ConstantValue
      #11 = Integer            20
      #12 = Utf8               d
      #13 = Utf8               Ljava/lang/String;
      #14 = String             #36            // hello
      #15 = Utf8               e
      #16 = Utf8               Ljava/lang/Object;
      #17 = Utf8               <init>
      #18 = Utf8               ()V
      #19 = Utf8               Code
      #20 = Utf8               LineNumberTable
      #21 = Utf8               LocalVariableTable
      #22 = Utf8               this
      #23 = Utf8               Lpart3/test10;
      #24 = Utf8               main
      #25 = Utf8               ([Ljava/lang/String;)V
      #26 = Utf8               args
      #27 = Utf8               [Ljava/lang/String;
      #28 = Utf8               <clinit>
      #29 = Utf8               SourceFile
      #30 = Utf8               test10.java
      #31 = NameAndType        #17:#18        // "<init>":()V
      #32 = NameAndType        #8:#7          // b:I
      #33 = Utf8               java/lang/Object
      #34 = NameAndType        #15:#16        // e:Ljava/lang/Object;
      #35 = Utf8               part3/test10
      #36 = Utf8               hello
    {
      static int a;
        descriptor: I
        flags: ACC_STATIC
    
      static int b;
        descriptor: I
        flags: ACC_STATIC
    
      static final int c;
        descriptor: I
        flags: ACC_STATIC, ACC_FINAL
        ConstantValue: int 20
    
      static final java.lang.String d;
        descriptor: Ljava/lang/String;
        flags: ACC_STATIC, ACC_FINAL
        ConstantValue: String hello
    
      static final java.lang.Object e;
        descriptor: Ljava/lang/Object;
        flags: ACC_STATIC, ACC_FINAL
    
      public part3.test10();                      //  5) 准备阶段仅仅进行空间分配,初始化阶段完成赋值,通过构造函数实现
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=1, locals=1, args_size=1
             0: aload_0
             1: invokespecial #1                  // Method java/lang/Object."<init>":()V
             4: return
          LineNumberTable:
            line 3: 0
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0       5     0  this   Lpart3/test10;
    
      public static void main(java.lang.String[]);
        descriptor: ([Ljava/lang/String;)V
        flags: ACC_PUBLIC, ACC_STATIC
        Code:
          stack=0, locals=1, args_size=1
             0: return
          LineNumberTable:
            line 11: 0
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0       1     0  args   [Ljava/lang/String;
    
      static {};
        descriptor: ()V
        flags: ACC_STATIC
        Code:
          stack=2, locals=0, args_size=0
             0: bipush        10
             2: putstatic     #2                  // Field b:I
             5: new           #3                  // class java/lang/Object
             8: dup
             9: invokespecial #1                  // Method java/lang/Object."<init>":()V
            12: putstatic     #4                  // Field e:Ljava/lang/Object;
            15: return
          LineNumberTable:
            line 5: 0
            line 8: 5
    }
    SourceFile: "test10.java"
    

    1-3-2 链接阶段的解析(重要)

    解析的目的:将常量池中的符号引用解析为直接引用 (比如class A引用class B,如果class B不进行解析,那么class A找不到class B地址)

    • 虚拟机不会给未解析的类分配具体的地址,只有经过解析的类从拥有地址。

    1-4 类加载的阶段3:初始化)(重点掌握类初始化的时机)

    加载过程中初始化的概念:初始化即调用 ()V ,虚拟机会保证这个类的『构造方法』的线程安全

    • 在链接的准备阶段,类变量已赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员自己写的逻辑去初始化类变量和其他资源
    • 所有类变量初始化语句和静态代码块都会在编译时被编译器放在收集器里头,存放到一个特殊的方法中,这个方法就是方法,即类/接口初始化方法,该方法只能在类加载的过程中由JVM调用;
    • 编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量;
    • 如果超类还没有被初始化,那么优先对超类初始化,但在方法内部不会显示调用超类的方法,由JVM负责保证一个类的方法执行之前,它的超类方法已经被执行。
    • JVM必须确保一个类在初始化的过程中,如果是多线程需要同时初始化它,仅仅只能允许其中一个线程对其执行初始化操作,其余线程必须等待,只有在活动线程执行完对类的初始化操作之后,才会通知正在等待的其他线程。(所以可以利用静态内部类实现线程安全的单例模式)
    • 如果一个类没有声明任何的类变量,也没有静态代码块,那么可以没有类方法;
      关于类的初始化

    1-4-1 Java中类初始化的时机

    原则:类初始化是懒惰的

    • Class.forName:返回与给定的字符串名称相关联类或接口的Class对象。

    A: 会导致类初始化的情况

    1)首次访问类的静态变量或静态方法时,会初始化类。
    2)子类初始化,会确保该子类的所有父类初始化。
    3)子类访问父类的静态变量,只会触发父类的初始化。
    4)执行Class.forName默认情况下会导致类的初始化。 
    5)new 会导致类的初始化。
    

    B:不会导致类初始化的情况

    1)访问类的 static final 静态常量(基本类型和字符串)不会触发初始化(原因:初始化在准备阶段已完成)
    2)类对象.class不会触发初始化
    3)创建该类的数组不会触发初始化
    4)使用类加载器loadclass方法
    5)Class.forName方法的参数2为false情况。
    

    1-4-2 类初始化的练习

    练习1:确定类何时会初始化?

    package part3;
    public class test11 {
        public static void main(String[] args) {
            System.out.println(E.a);            // B 1):final修饰的基本类型,类加载的准备阶段已赋值,不会初始化 
            System.out.println(E.b);            // B 1):final修饰的String,类加载的准备阶段已赋值,不会初始化 
            System.out.println(E.c);            // 首次 class E 初始化
        }
    }
    
    class E {
        public static final int a = 10;
        public static final String b = "hello";
        public static final Integer c = 20;  // Integer.valueOf(20)
        static {
            System.out.println("init E");
        }
    }
    

    执行结果

    10
    hello
    init E
    20
    

    练习2:懒惰初始化的单例模式(类的懒惰初始化应用)

    package part3;
    final class Singleton {
        private Singleton() { }
        // 内部类中保存单例
        // 这里利用了 Singleton类加载过程中在初始化阶段的懒惰初始化的特性,当首次访问类的静态方法getInstance时,触发类的初始化。
        // 注意:单例模式实现是线程安全的,静态代码只在class的加载过程中执行一次,JVM提供的类加载器(C实现)需要确保多个线程进行初始化操作的时候,只有一个线程实现初始化
        // 双亲委派机制是推荐使用类加载器的方式,而类加载器是JVM底层实现用于加载类的代码,二者要注意区分。
     
        private static class LazyHolder {
            static final Singleton INSTANCE = new Singleton();
        }
        public static Singleton getInstance() {
            return LazyHolder.INSTANCE;
        }
    }
    public class test12 {
        public static void main(String[] args) {
        }
    }
    

    2 类加载器

    2-1 类加载器概述

    常见类加载器

    名称 加载的类(管理区域) 说明
    Bootstrap ClassLoader (启动类加载器) JAVA_HOME/jre/lib 无法直接访问,C++代码实现
    Extension ClassLoader (扩展类加载器) JAVA_HOME/jre/lib/ext 上级为 Bootstrap
    Application ClassLoader (应用类加载器,最常见) classpath 上级为 Extension
    自定义类加载器 自定义 上级为 Application

    注意点

    • 类加载器之间存在层级关系,所谓层级关系就是:当Application ClassLoader 加载类时候,首先询问上级类加载器Extension ClassLoader 是否已经加载过类,如果Extension ClassLoader没有加载,还会询问其上级的类加载器Bootstrap ClassLoader 是否已经加载过类,当得知所有上级类加载器都没有加载过类的时候,Application ClassLoader才会加载类
    层级关系(优先级): Bootstrap ClassLoader  > Extension ClassLoader > Application ClassLoader > 自定义类加载器 
    

    2-2 启动/扩展/应用类加载器的实例

    2-2-1 启动类加载器实例(通过JVM参数指定)

    -Xbootclasspath      // 设置启动类加载的路径
    java -Xbootclasspath:<new bootclasspath>
    java -Xbootclasspath/a:<追加路径>
    java -Xbootclasspath/p:<追加路径>
    
    package cn.itcast.jvm.t3.load;
    public class F {
        static {
            System.out.println("bootstrap F init");
        }
    }
    
    package cn.itcast.jvm.t3.load;
    public class Load5_1 {
        public static void main(String[] args) throws ClassNotFoundException {
            Class<?> aClass = Class.forName("cn.itcast.jvm.t3.load.F");    // 获取定义的类的名称即进行类的加载
            System.out.println(aClass.getClassLoader());                   // null
        }
    }
    

    执行结果:

    • 通过JVM参数将定义的F class的路径添加到启动类路径
    • 由于启动类加载器是采用C++编写的,因此无法直接获得启动类加载器,因此输出结果为null
    E:gitjvmoutproductionjvm>java -Xbootclasspath/a:.cn.itcast.jvm.t3.load.Load5
    bootstrap F init
    null
    

    2-2-3 应用和扩展类加载器实例

    package part3;
    public class test13 {
        public static void main(String[] args) throws ClassNotFoundException {
            Class<?> aClass = Class.forName("part3.test13");     // 参数是 包名+类名
            System.out.println(aClass.getClassLoader());
        }
    }
    

    执行结果

    • 可以看到test13 是通过应用类加载器加载的,其名称为AppClassLoader.
    sun.misc.Launcher$AppClassLoader@18b4aac2
    
    如何使用扩展类加载器?

    方法:将写好的类打包成Jar包并将其放到JAVA_HOME/jre/lib/ext

    如果扩展类加载器与应用程序加载器加载相同类名文件,扩展类加载器优先于应用类加载器。

    2-3 双亲(上级)委派模式

    2-3-1 loadClass源码分析

    双亲委派定义:调用类加载器的 loadClass 方法时,查找类的规则

    loadclass源码分析(ClassLoader.java)

    package part3;
    public class test13 {
        public static void main(String[] args) throws ClassNotFoundException {
            Class<?> aClass1 = test13.class.getClassLoader().loadClass("part3.test13");
            System.out.println(aClass1.getClassLoader());
        }
    }
    
        public Class<?> loadClass(String name) throws ClassNotFoundException {
            return loadClass(name, false);
        }
    
     protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
        {
            synchronized (getClassLoadingLock(name)) {       // 这里的类加载通过synchronized来保证安全性
                // 1. 检查这个类是否已经被加载。
                // First, check if the class has already been loaded
                Class<?> c = findLoadedClass(name);
                if (c == null) {
                    long t0 = System.nanoTime();
                    try {
                        if (parent != null) {
                            /*2. 有上级的话,委派上级 loadClass*/
                            c = parent.loadClass(name, false);
                        } else {
                            /* 3. 如果没有上级类加载器(ExtClassLoader),则委派BootstrapClassLoader*/
                            c = findBootstrapClassOrNull(name);
                        }
                    } catch (ClassNotFoundException e) {
                        // ClassNotFoundException thrown if class not found
                        // from the non-null parent class loader
                    }
    
                    if (c == null) {
                        // If still not found, then invoke findClass in order
                        // to find the class.
                        long t1 = System.nanoTime();
                        /*4. 上级都没有加载,调用 findClass 方法(每个类加载器自己扩展)来加载*/
                        c = findClass(name);
    
                        // this is the defining class loader; record the stats
                        /* 5. 记录耗时*/
                        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.loaderClass方法流程总结

    • 双亲委派机制是JVM提供的三种类加载器的使用顺序进行了约定,这种约定是推荐用户去使用的,其中ClassLoader.java的存在就是希望用户能够根据这个类定义自己的类加载器的,
      当然通常都是基于双亲委派模式,也可以不按照这个模式,使用Class.java中提供的方法自由的定义自己想要的类加载器。
      在JVM语言的C代码中应该也会实现。

    假设loadclass方法首次加载应用程序类

    • 整个过程经历二次调用findclass方法和一次异常捕获(非常巧妙)
    • 初始类加载器只有在启动的时候才会加载类,在代码中只是让该加载检查自己时候已经加载过这个类,没有让他尝试加载这个类。
    step1:首次调用loadclass方法,最初的类加载器是应用程序类加载器,该类加载器通过[findLoadedClass(name)]先检查自己的缓存中是否已经加载过这个类。
    step2:获取该类加载器的上级类加载器即扩展类加载,递归调用loadclass方法
    step3:扩展类加载同样检查自己是否已经加载,没有则判断启动类加载器是否已经加载过class
    step4:启动类加载没有加载过class,则扩展类加载器使用 findClass(name)自己加载这个类,显然加载失败(当前的类是应用程序类),这个时候会抛出ClassNotFoundException异常,
    该异常被应用类加载器给捕获,不做任何处理
    step5:此时应用程序类加载器使用findClass(name)加载到应用程序类并返回。
    

    引申问题:多线程环境下,是如何保证类加载的唯一性?

    • 双亲委派机制利用synchronized对加载类的名称作为key的object对象加锁
    synchronized (getClassLoadingLock(name))    
    // 这个加锁的过程用到了concurrentHashMap,key是类的名称,value是object对象,加载类的时候会对这个类的object对象进行加锁
    
    protected Object getClassLoadingLock(String className) {
        Object lock = this;
        if (parallelLockMap != null) {
            Object newLock = new Object();
            lock = parallelLockMap.putIfAbsent(className, newLock);
            if (lock == null) {
                lock = newLock;
            }
        }
        return lock;
    }
    

    2-3-2 JDBC中的双亲委派机制的打破

    实例:在使用 JDBC 时,都需要加载 Driver 驱动,com.mysql.jdbc.Driver 是如何被正确加载的

    Applications no longer need to explicitly load JDBC drivers using Class.forName(). Existing programs which currently load JDBC drivers using Class.forName() will continue to work without modification.
    // DriverManger.java文件中的注解,为什么之前需要 Class.forName()加载驱动,现在不需要呢?)
    
    package com;
    import java.sql.DriverManager;
    public class Test1 {
        public static void main(String[] args) {
            System.out.println(DriverManager.class.getClassLoader());
        }
    }
    

    代码执行结果:

    • 从打印结果上看DriverManger是启动类加载器加载的,然而启动类加载路径JAVE_HOME/jre/lib没有JDBC的驱动,因此程序是如何加载JDBC驱动类的?
    null
    

    DriverManger.java的部分源码

    step1:在静态代码块中调用loadInitialDrivers();

    public class DriverManager {
        // List of registered JDBC drivers
        private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();  
        ...
        static {
            loadInitialDrivers();
            println("JDBC DriverManager initialized");
        }
        ...
    

    step2: 调用loadInitialDrivers()方法

    关键点:1)使用SPI加载驱动类 2)使用应用程序类加载器加载驱动类

    private static void loadInitialDrivers() {
            String drivers;
            try {
                drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
                    public String run() {
                        return System.getProperty("jdbc.drivers");
                    }
                });
            } catch (Exception ex) {
                drivers = null;
            }
           /* 01 使用SPI机制加载相关类,具体来说按照SPI的约定,从而通过serviceLoader加载实现类并调用:*/
            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) {
                    // Do nothing
                    }
                    return null;
                }
            });
            /*使用JDBC drivers定义的驱动名加载驱动,注意这里使用的是应用程序类加载器
              这里没有遵循双亲委派机制,如果按照双亲委派机制应该采用启动类加载器,但是启动类加载器其
              路径下没有我们想要加载的东西,因此必须在代码中使用应用类加载器去加载我们想要的类。
            */
            println("DriverManager.initialize: jdbc.drivers = " + drivers);
            if (drivers == null || drivers.equals("")) {
                return;
            }
             /*这里就是String数组,里面每个String都是需要加载驱动类的名称*/
            String[] driversList = drivers.split(":");  
            println("number of Drivers:" + driversList.length);
            /*getSystemClassLoader()获取的是应用程序类加载器,该应用了加载器通过驱动类的名称加载类*/
            for (String aDriver : driversList) {
                try {
                    println("DriverManager.Initialize: loading " + aDriver);
                    Class.forName(aDriver, true,
                            ClassLoader.getSystemClassLoader());
                } catch (Exception ex) {
                    println("DriverManager.Initialize: load failed: " + ex);
                }
            }
        }
    
    JDBC的驱动类加载总结
    1) 实际加载过程中通过启动类加载器加载DriverManger这个类,DriverManager中loadInitialDrivers()没有采用双亲委派机制去获取驱动类。
    2)首先先尝试采用服务提供接口(SPI)的线程上下文加载器实现对驱动类的加载,本质上是采用应用程序类加载器进行加载(见2-3-3源码)
    3)然后通过上个步骤获取的驱动的名称(drivers),再次利用应用类加载器加载驱动类。
    
    备注:整个过程貌似是通过SPI获取到需要加载的类的类的名称,然后再调用应用程序类加载器加载(不是太确定)
    

    2-3-3 SPI的相关知识

    定义: SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的接口,它可以用来启用框架扩展和替换组件

    • SPI的作用就是为这些被扩展的API寻找服务实现

    mysql-connector-java在service目录下建立名称为java.sql.Driver的文件。

    • 文件名就是service provider interface(服务提供接口的名称)
    • 文件中的内容com.mysql.cj.jdbc.Driver是该“服务提供接口实现类"的名称

    使用SPI必须遵循上述约定

    按照上述约定并将接口类实现,可通过ServiceLoader来得到实现类(如下所示),体现的是【面向接口编程+解耦】的思想

    ServiceLoader<接口类型> allImpls = ServiceLoader.load(接口类型.class);
    Iterator<接口类型> iter = allImpls.iterator();
        while(iter.hasNext()) {
        iter.next();
    }
    

    SPI思想在其他框架的体现

    JDBC
    Servlet 初始化器
    Spring 容器
    Dubbo(对 SPI 进行了扩展)
    

    背景SPI实现扩展功能的类加载的时候,使用了线程上下文类加载器,分析源码可以发现线程上下文类加载器就是应用程序类加载器。

    线程上下文类加载器定义:当前线程使用的类加载器,默认就是应用程序类加载器 ,在每一个Thread启动的时候,JVM都会为该线程初始化一个类加载器

    private ClassLoader contextClassLoader;    // Thread源码中包含有contextClassLoader这个属性
    

    ServiceLoader.java的源码内容

    public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        // 传入两个参数: 1)class对象 2)类的加载器
        return ServiceLoader.load(service, cl);
    }
    
    public static <S> ServiceLoader<S> load(Class<S> service,ClassLoader loader){
        return new ServiceLoader<>(service, loader);
    }
    =========================================================================================================
    private ServiceLoader(Class<S> svc, ClassLoader cl) {
        service = Objects.requireNonNull(svc, "Service interface cannot be null");
        loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;           // 确保类加载器不为空
        acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
        reload();
    }
    
    public void reload() {
        providers.clear();
        lookupIterator = new LazyIterator(service, loader);  // 创建一个发现service的迭代器
    }
    ========================================================================================================
    /* nextService方法中会获取*/
    private S nextService() {
        if (!hasNextService())
            throw new NoSuchElementException();
        String cn = nextName;
        nextName = null;
        Class<?> c = null;
        try {
            c = Class.forName(cn, false, loader);  // loader就是传递进来的线程上下文加载器,本质上就是应用程序类加载。
        } catch (ClassNotFoundException x) {
            fail(service,
                 "Provider " + cn + " not found");
        }
        if (!service.isAssignableFrom(c)) {
            fail(service,
                 "Provider " + cn  + " not a subtype");
        }
        try {
            S p = service.cast(c.newInstance());
            providers.put(cn, p);
            return p;
        } catch (Throwable x) {
            fail(service,
                 "Provider " + cn + " could not be instantiated",
                 x);
        }
        throw new Error();          // This cannot happen
    }
    
    

    总结从上面的源码中看到SPI机制并没有遵循双亲委派机制去加载类即没有适用ClassLoader.loadClass方法加载类,而是通过Java的反射机制(@CallerSensitive)即使用Class.forName方法指定应用程序类加载器去加载class。


    2-3-4 Class.forName()加载类和使用ClassLoader.loadClass加载类的区别(重要)?

    /*方式1:使用ClassLoader.loadClass进行类加载*/
    Class<?> aClass1 = test13.class.getClassLoader().loadClass("part3.test13");
    /*方式2:使用Class.forName进行类加载*/
    Class.forName(String name, boolean initialize,ClassLoader loader)
    
    • 关于ClassLoader.loadClass见2-3-1的源码分析
    采用这种方式加载严格遵照双亲委派机制进行类的加载,对于大部分类加载的情况这个方法是适用的。
    
    • 关于Class.forName()加载
    有三个参数:
    1)加载类的名称
    2)是否进行类的初始化
    3)用于加载类的类加载器
    可以看出Class.forName这个方法加载类更加的灵活,不要遵循双亲委派机制,可以自由的去加载类。(也就是所谓的打破了双亲委派机制)
    

    Class.java文件中Class.forName的源码

      /*
          1)加载类的名称
          2)是否进行类的初始化
          3)用于加载类的类加载器
      */
       @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);
        }
    

    2种方法区别总结:

    1)Class.forName()加载类更加灵活,更加底层,可以指定类的加载器,通常用于打破双亲委派机制。(比如数据库驱动的加载本质上就是利用该方法打破双亲委派)
    2)ClassLoader.loadClass提供了基于双亲委派机制的类加载方法,适用于大多数类加载的情况,通过synchronized与双亲委派思想确保类加载的只加载一次。(我们也可以基于自定义类加载器重写loadClass方法实现不是基于双亲委派机制的类加载器)
    

    2-4 自定义类加载器的实现

    2-4-1 自定义类加载器的使用场景

    1)想加载非 classpath 随意路径中的类文件
    2)都是通过接口来使用实现,希望解耦时,常用在框架设计(SPI接口就是基于这个思想)
    3)这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器(同名同包的类的共同运行)
    

    2-4-2 自定义类加载器的实现(待补充)

    10、自定义类加载器
    11、类加载器的命名空间

    基本步骤(结合ClassLoader.java源码理解)

    1. 继承 ClassLoader 父类
    2. 要遵从双亲委派机制,重写 findClass 方法(不是重写 loadClass方法,否则不会走双亲委派机制)
    3. 读取类文件的字节码
    4. 调用父类的 defineClass 方法来加载类
    5. 使用者调用该类加载器的 loadClass 方法
    

    参考资料

    01 Java SPI 详解

    02 在Java的反射中,Class.forName和ClassLoader的区别

    03 JVM基础课程

    04 JDK8:Chapter 5. Loading, Linking, and Initializing

    05 类的生命周期

  • 相关阅读:
    前端大牛们都学过哪些东西?
    自适应网页设计
    CSS border实现三角形
    wampServer 设置
    javascript 基础
    Oracle 优化相关
    Java(数据类型)基础面试
    Java 反射机制
    【设计模式】——代理模式
    【设计模式】——单例模式
  • 原文地址:https://www.cnblogs.com/kfcuj/p/14813959.html
Copyright © 2020-2023  润新知