3.6_3 【JVM原理】 P2 类加载器(ClassLoader)

相关链接

  • Excel
  • 【JVM原理目录】 类加载全流程详解
    • 【JVM原理】 P1 字节码文件(Java Class)
    • 【JVM原理】 P2 类加载器(ClassLoader)

目录

  • 2 类加载器(ClassLoader)
    • 2.1 类加载的过程
    • 2.2 类加载器的分类
      • 2.2.1 启动(BootStrap)类加载器
      • 2.2.2 扩展(Extension)类加载器
      • 2.2.3 应用程序(Application)类加载器
      • 2.2.4 自定义(Custom)类加载器
      • 2.2.5 线程上下文(Thread Context)类加载器
    • 2.3 双亲委派模型(Parent-Delegation Mode)
    • 2.4 破坏双亲委派
    • 2.5 SPI机制


JVM虚拟机 + 执行引擎 + 本地方法接口



概述

3.6_3 【JVM原理】 P2 类加载器(ClassLoader)_第1张图片

  • 2 类加载器(ClassLoader):虚拟机把Class文件加载到内存,最终形成虚拟机可以直接使用的Java类型。
    • 2.1 启动(BootStrap)类加载器:JAVA_HOME/lib下面的核心类库及一些其他包加载到内存中。(底层使用C++实现)
    • 2.2 扩展(Extension)类加载器:JAVA_HOME /lib/ext类库及一些其他包加载到内存中。(ClassLoader的子类)
    • 2.3 应用程序(Application)类加载器:当前类所在路径及其引用的第三方类库的路径下的类库加载到内存中。(ClassLoader子类)
    • 2.4 线程上下文(Thread Context)类加载器:破坏"双亲委派模型",解决了spi接口通过系统加载类找不到实现类的问题。
    • 自定义类加载器:以上三个属于系统类加载器,系统的ClassLoader只会加载指定目录下的class文件,如果想加载自己的class文件,就需要自定义一个ClassLoader。

2 类加载器(ClassLoader)

  • 2 类加载器(ClassLoader):虚拟机把Class文件加载到内存,最终形成虚拟机可以直接使用的Java类型。
    • 2.1 启动(BootStrap)类加载器:JAVA_HOME/lib下面的核心类库及一些其他包加载到内存中。(底层使用C++实现)
    • 2.2 扩展(Extension)类加载器:JAVA_HOME /lib/ext类库及一些其他包加载到内存中。(ClassLoader的子类)
    • 2.3 应用程序(Application)类加载器:当前类所在路径及其引用的第三方类库的路径下的类库加载到内存中。(ClassLoader子类)
    • 2.4 线程上下文(Thread Context)类加载器:破坏"双亲委派模型",解决了spi接口通过系统加载类找不到实现类的问题。
    • 自定义类加载器:以上三个属于系统类加载器,系统的ClassLoader只会加载指定目录下的class文件,如果想加载自己的class文件,就需要自定义一个ClassLoader。

  • Source:图解类加载器和双亲委派机制

2.1 类加载的过程

  • 类加载:我们都知道Java代码会被编译class文件,在class文件中描述了该类的各种信息,class类最终需要被加载到虚拟机中才能运行使用

  • 类加载机制:虚拟机把Class文件加载到内存,并对数据进行校验转换解析初始化,最终形成虚拟机可以直接使用的Java类型。

    3.6_3 【JVM原理】 P2 类加载器(ClassLoader)_第2张图片

  • 加载来源

    • 1、本地磁盘
    • 2、网络下载的.class文件
    • 3、war,jar下加载.class文件
    • 4、从专门的数据库中读取.class文件(少见)
    • 5、将java源文件动态编译成class文件,典型的就是动态代理,通过运行时生成class文件

2.2 类加载器的分类

  • JVM启动时并不会一次性加载所有的class文件(内存会爆),而是根据需要去动态加载。因此根据不同功能场景将类加载器分为5种。

2.2.1 启动(BootStrap)类加载器

  • 首先我们来看看启动类加载器加载了哪些类,启动类加载器负责加载sun.boot.class.path:
    • 启动(Bootstrap)类加载器是用本地代码实现的类加载器。属于虚拟机的一部分,它是用C++写的,看不到源码
    • 负责将JAVA_HOME/lib下面的核心类库或-Xbootclasspath选项指定的jar包等虚拟机识别的类库加载到内存中。
    • 也叫引导类加载器
package com.groupies.base.JVM;
import java.util.Arrays;
import java.util.List;

/**
 * @author GroupiesM
 * @date 2021/1/29
 * 类加载器的分类:1.启动(Bootstrap)类加载器
 */
public class bootClassLoaderLoadingPath {
    public static void main(String[] args) {
        //获取启动列加载器加载的目录
        String bootStrapLoadingPath=System.getProperty("sun.boot.class.path");
        //把加载的目录转为集合
        List<String> bootLoadingPathList= Arrays.asList(bootStrapLoadingPath.split(";"));
        for (String bootPath:bootLoadingPathList){
            /*
            启动类加载器加载的目录:D:\develop\Java\JAVA_HOME\jdk1.8\jre\lib\resources.jar
            启动类加载器加载的目录:D:\develop\Java\JAVA_HOME\jdk1.8\jre\lib\rt.jar
            启动类加载器加载的目录:D:\develop\Java\JAVA_HOME\jdk1.8\jre\lib\sunrsasign.jar
            启动类加载器加载的目录:D:\develop\Java\JAVA_HOME\jdk1.8\jre\lib\jsse.jar
            启动类加载器加载的目录:D:\develop\Java\JAVA_HOME\jdk1.8\jre\lib\jce.jar
            启动类加载器加载的目录:D:\develop\Java\JAVA_HOME\jdk1.8\jre\lib\charsets.jar
            启动类加载器加载的目录:D:\develop\Java\JAVA_HOME\jdk1.8\jre\lib\jfr.jar
            启动类加载器加载的目录:D:\develop\Java\JAVA_HOME\jdk1.8\jre\classes
             */
            System.out.println("启动类加载器加载的目录:"+bootPath);
        }
    }
}
  • 启动(Bootstrap)类加载器是用 {JRE_HOME}/lib加载类的,不过,你也可以使用参数 -Xbootclasspath 或 系统变sun.boot.class.path来指定的目录来加载类。
  • 一般而言,{JRE_HOME}/lib下存放着JVM正常工作所需要的系统类,如下表所示:
文件名 描述
rt.jar 运行环境包,rt即runtime,J2SE 的类定义都在这个包内
charsets.jar 字符集支持包
jce.jar 是一组包,它们提供用于加密、密钥生成和协商以及 Message Authentication Code(MAC)算法的框架和实现
jsse.jar 安全套接字拓展包Java™ Secure Socket Extension
classlist 该文件内表示是引导类加载器应该加载的类的清单
net.properties JVM 网络配置信息
  • 启动类加载器(Bootstrap ClassLoader) 加载系统类后,JVM内存会呈现如下格局:
    • 引导类加载器将类信息加载到方法区中,以特定方式组织,对于某一个特定的类而言,在方法区中它应该有 运行时常量池类型信息字段信息方法信息类加载器的引用对应class实例的引用等信息。
    • 类加载器的引用,由于这些类是由引导类加载器(Bootstrap Classloader)进行加载的,而 引导类加载器是有C++语言实现的,所以是无法访问的,故而该引用为NULL
    • 对应class实例的引用, 类加载器在加载类信息放到方法区中后,会创建一个对应的Class 类型的实例放到堆(Heap)中, 作为开发人员访问方法区中类定义的入口和切入点。

      3.6_3 【JVM原理】 P2 类加载器(ClassLoader)_第3张图片


2.2.2 扩展(Extension)类加载器

  • 扩展(Extension)类加载器加载负责加载java.ext.dirs,我们同样写一段代码去加载它:
    • 扩展(Extension)类加载器是由Sun的ExtClassLoadersun.misc.Launcher$ExtClassLoader)实现的。Java类,继承自URLClassLoader 扩展类加载器。
    • 负责将JAVA_HOME /lib/ext或者由系统变量-Djava.ext.dir指定位置中的类库加载到内存中。
package com.groupies.base.JVM;

import java.util.Arrays;
import java.util.List;

/**
 * @author GroupiesM
 * @date 2021/1/29
 * 类加载器的分类:2.启动(Bootstrap)类加载器
 */
public class extClassLoaderLoadingPath {
    public static void main(String[] args) {
        //获取启动列加载器加载的目录
        String bootStrapLoadingPath = System.getProperty("java.ext.dirs");
        //把加载的目录转为集合
        List<String> bootLoadingPathList = Arrays.asList(bootStrapLoadingPath.split(";"));
        for (String bootPath : bootLoadingPathList) {
            /*
                拓展类加载器加载的目录:D:\develop\Java\JAVA_HOME\jdk1.8\jre\lib\ext
                拓展类加载器加载的目录:C:\WINDOWS\Sun\Java\lib\ext
             */
            System.out.println("拓展类加载器加载的目录:" + bootPath);
        }
    }
}

2.2.3 应用程序(Application)类加载器

  • 应用(Application)类加载器,负责加载java.class.path
    • 负责加载工程目录下classpath下的class以及jar包。Java类,继承自URLClassLoader 系统类加载器。
    • 也叫系统类加载器
package com.groupies.base.JVM;

import java.util.Arrays;
import java.util.List;

/**
 * @author GroupiesM
 * @date 2021/1/29
 * 类加载器的分类:3.应用程序(Application)类加载器
 */
public class appClassLoaderLoadingPath {
    public static void main(String[] args) {
        //获取启动列加载器加载的目录
        String bootStrapLoadingPath=System.getProperty("java.class.path");
        //把加载的目录转为集合
        List<String> bootLoadingPathList= Arrays.asList(bootStrapLoadingPath.split(";"));
        for (String bootPath:bootLoadingPathList){
            /*
            应用程序类加载器加载的目录:D:\develop\Java\JAVA_HOME\jdk1.8\jre\lib\charsets.jar
            应用程序类加载器加载的目录:D:\develop\Java\JAVA_HOME\jdk1.8\jre\lib\deploy.jar
            应用程序类加载器加载的目录:D:\develop\Java\JAVA_HOME\jdk1.8\jre\lib\ext\access-bridge-64.jar
            应用程序类加载器加载的目录:D:\develop\Java\JAVA_HOME\jdk1.8\jre\lib\ext\cldrdata.jar
            应用程序类加载器加载的目录:D:\develop\Java\JAVA_HOME\jdk1.8\jre\lib\ext\dnsns.jar
            应用程序类加载器加载的目录:D:\develop\Java\JAVA_HOME\jdk1.8\jre\lib\ext\jaccess.jar
            应用程序类加载器加载的目录:D:\develop\Java\JAVA_HOME\jdk1.8\jre\lib\ext\jfxrt.jar
            应用程序类加载器加载的目录:D:\develop\Java\JAVA_HOME\jdk1.8\jre\lib\ext\localedata.jar
            应用程序类加载器加载的目录:D:\develop\Java\JAVA_HOME\jdk1.8\jre\lib\ext\nashorn.jar
            应用程序类加载器加载的目录:D:\develop\Java\JAVA_HOME\jdk1.8\jre\lib\ext\sunec.jar
            应用程序类加载器加载的目录:D:\develop\Java\JAVA_HOME\jdk1.8\jre\lib\ext\sunjce_provider.jar
            应用程序类加载器加载的目录:D:\develop\Java\JAVA_HOME\jdk1.8\jre\lib\ext\sunmscapi.jar
            应用程序类加载器加载的目录:D:\develop\Java\JAVA_HOME\jdk1.8\jre\lib\ext\sunpkcs11.jar
            应用程序类加载器加载的目录:D:\develop\Java\JAVA_HOME\jdk1.8\jre\lib\ext\zipfs.jar
            应用程序类加载器加载的目录:D:\develop\Java\JAVA_HOME\jdk1.8\jre\lib\javaws.jar
            应用程序类加载器加载的目录:D:\develop\Java\JAVA_HOME\jdk1.8\jre\lib\jce.jar
            应用程序类加载器加载的目录:D:\develop\Java\JAVA_HOME\jdk1.8\jre\lib\jfr.jar
            应用程序类加载器加载的目录:D:\develop\Java\JAVA_HOME\jdk1.8\jre\lib\jfxswt.jar
            应用程序类加载器加载的目录:D:\develop\Java\JAVA_HOME\jdk1.8\jre\lib\jsse.jar
            应用程序类加载器加载的目录:D:\develop\Java\JAVA_HOME\jdk1.8\jre\lib\management-agent.jar
            应用程序类加载器加载的目录:D:\develop\Java\JAVA_HOME\jdk1.8\jre\lib\plugin.jar
            应用程序类加载器加载的目录:D:\develop\Java\JAVA_HOME\jdk1.8\jre\lib\resources.jar
            应用程序类加载器加载的目录:D:\develop\Java\JAVA_HOME\jdk1.8\jre\lib\rt.jar
            应用程序类加载器加载的目录:F:\bakup\JAVA_PRACTICE\base01\target\classes
            应用程序类加载器加载的目录:D:\develop\Java\Maven\maven_repository\org\springframework\spring-test\4.1.3.RELEASE\spring-test-4.1.3.RELEASE.jar
            应用程序类加载器加载的目录:D:\develop\Java\Maven\maven_repository\org\springframework\spring-core\4.1.3.RELEASE\spring-core-4.1.3.RELEASE.jar
            应用程序类加载器加载的目录:D:\develop\Java\Maven\maven_repository\commons-logging\commons-logging\1.2\commons-logging-1.2.jar
            应用程序类加载器加载的目录:D:\develop\Java\Maven\maven_repository\junit\junit\4.12\junit-4.12.jar
            应用程序类加载器加载的目录:D:\develop\Java\Maven\maven_repository\org\hamcrest\hamcrest-core\1.3\hamcrest-core-1.3.jar
            应用程序类加载器加载的目录:D:\develop\Java\IntelliJ IDEA 2020.3.1\lib\idea_rt.jar
             */
            System.out.println("应用程序类加载器加载的目录:"+bootPath);
        }
    }
}


2.2.4 自定义(Custom)类加载器

=> ClassLoader详解

  • 自定义(Custom)类加载器,通过 java.lang.ClassLoader的子类自定义加载class,系统JVM自带的3个ClassLoader只会加载指定目录下的class文件,如果某个情况下,我们需要加载应用程序之外的类文件呢?比如本地D盘下的,或者去加载网络上的某个类文件,这种情况就可以自定义一个ClassLoader。而且我们可以根据自己的需求,对class文件进行加密和解密。
  • 如何自定义ClassLoader
    • 新建一个类继承自java.lang.ClassLoader,重写它的findClass方法。
    • 将class字节码数组转换为Class类的实例
    • 调用loadClass方法即可
  • 类加载过程的重要方法loadClass:JDK文档中是这样写的,通过指定的全限定类名加载class,它通过同名的loadClass(String,boolean)方法。
protected Class<?> loadClass(String name,
                             boolean resolve)
                      throws ClassNotFoundException
  • 上面是方法原型,一般实现这个方法的步骤是
    • 执行findLoadedClass(String)去检测这个class是不是已经加载过了。如果加载过了,直接加载,如果没有,进行下一步。
    • 执行父加载器的loadClass方法。如果父加载器为null,则jvm内置的加载器去替代,也就是Bootstrap ClassLoader。这也解释了ExtClassLoader的parent为null,但仍然说Bootstrap ClassLoader是它的父加载器。
    • 如果向上委托父加载器没有加载成功,则通过findClass(String)查找。
    • 如果class在上面的步骤中找到了,参数resolve又是true的话,那么loadClass()又会调用resolveClass(Class)这个方法来生成最终的Class对象。 我们可以从源代码看出这个步骤。
protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 首先,检查是否已经加载过
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        //父加载器不为空,调用父加载器的loadClass
                        c = parent.loadClass(name, false);
                    } else {
                        //父加载器为空则,调用Bootstrap Classloader
                        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();
                    //父加载器没有找到,则调用findclass
                    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()
                resolveClass(c);
            }
            return c;
        }
    }
  • 代码解释了双亲委托。
  • 另外,要注意的是如果要编写一个classLoader的子类,也就是自定义一个classloader,建议覆盖findClass()方法,而不要直接改写loadClass()方法。

  • 下面写一个自定义类加载器:指定类加载路径在D盘下的lib文件夹下。
    • 新建一个Test.class类,代码如下:
package com.test;

public class Test {
    public void say() {
        System.out.println("Hello MyClassLoader");
    }
}
  • 在Test.java所在目录打开dos窗口

    3.6_3 【JVM原理】 P2 类加载器(ClassLoader)_第4张图片

  • 编译Test.java,生成.class字节码文件

    3.6_3 【JVM原理】 P2 类加载器(ClassLoader)_第5张图片

  • 放入指定目录 D:/lib/com/Test
    3.6_3 【JVM原理】 P2 类加载器(ClassLoader)_第6张图片

  • 自定义加载类 MyClassLoader

    • a.继承ClassLoader
    • b.重写findClass()方法
    • c.在findClass()方法中调用defineClass()方法:我们在findClass()方法中定义了查找class的方法,然后数据通过defineClass()生成了Class对象。
    • 注意:一个ClassLoader创建时如果没有指定parent,那么它的parent默认就是AppClassLoader,因为这样就能够保证它能访问系统内置加载器加载成功的class文件。
package com.groupies.base.JVM.CustomClassLoader;

import java.io.*;

/**
 * @author GroupiesM
 * @date 2021/2/1
 * 自定义(Custom)类加载器
 *      通过 java.lang.ClassLoader的子类自定义加载class,系统的ClassLoader只会加载指定目录下的class文件,如果你想加载自己的class文件,那么就可以自定义一个ClassLoader。而且我们可以根据自己的需求,对class文件进行加密和解密。
 *
 * 如何自定义ClassLoader:
 *      新建一个类继承自java.lang.ClassLoader,重写它的findClass方法。
 *      将class字节码数组转换为Class类的实例
 *      调用loadClass方法即可
 */
public class MyClassLoader extends ClassLoader {

    private String classpath;

    public MyClassLoader(String classpath) {
        this.classpath = classpath;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            byte[] classDate = getDate(name);
            if (classDate == null) {
            } else {
                //defineClass方法将字节码转化为类
                return defineClass(name, classDate, 0, classDate.length);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return super.findClass(name);
    }

    /**
     返回类的字节码
     */
    private byte[] getDate(String className) throws IOException {
        InputStream in = null;
        ByteArrayOutputStream out = null;
        String path = classpath + File.separatorChar + className.replace('.', File.separatorChar) + ".class";
        try {
            in = new FileInputStream(path);
            out = new ByteArrayOutputStream();
            byte[] buffer = new byte[2048];
            int len = 0;
            while ((len = in.read(buffer)) != -1) {
                out.write(buffer, 0, len);
            }
            return out.toByteArray();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } finally {
            in.close();
            out.close();
        }
        return null;
    }
}
  • 测试类TestMyClassLoader
    • 现在如果调用一个Test对象的say方法,它会输出"Hello MyClassLoader"这条字符串。但现在是我们把Test.class放置在应用工程所有的目录之外,我们需要加载它,然后执行它的方法。
package com.groupies.base.JVM.CustomClassLoader;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * @author GroupiesM
 * @date 2021/2/1
 */
public class TestMyClassLoader {
    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, SecurityException, IllegalArgumentException, InvocationTargetException {
        //自定义类加载器的加载路径
        MyClassLoader myClassLoader = new MyClassLoader("D:\\lib");
        //包名+类名
        Class c = myClassLoader.loadClass("com.test.Test");

        if (c != null) {
            Object obj = c.newInstance();
            Method method = c.getMethod("say", null);
            method.invoke(obj, null);
            //输出结果:
            //Hello MyClassLoader
            //com.groupies.base.JVM.CustomClassLoader.MyClassLoader@5cad8086
            System.out.println(c.getClassLoader().toString());
        }
    }
}

2.2.5 线程上下文(Thread Context)类加载器

  • 概述

    • 从 JDK 1.2 开始引入。Java.lang.Thread中的方法 getContextClassLoader()setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。
    • 如果没有通过 setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程线程上下文(Thread Context)类加载器
    • Java 应用运行的初始线程的线程上下文(Thread Context)类加载器系统类(包括启动、扩展、应用程序、线程上下文类)加载器,在线程中运行的代码可以通过此类加载器来加载类和资源。
  • 作用

    • 线程上下文类加载器从根本解决了一般应用不能违背双亲委派模式的问题,使得java类加载体系显得更灵活。 上面所提到的问题正是线程上下文类加载器的拿手好菜。如果不做任何的设置,Java应用的线程上下文类加载器默认就是系统类加载器。因此,在 SPI 接口的代码中使用线程上下文类加载器,就可以成功的加载到 SPI 实现的类。

2.3 双亲委派模型(Parent-Delegation Mode)

  所谓双亲委派模型,就是指一个类接收到类加载请求后,会把这个请求依次传递给父类加载器(如果还有的话),如果顶层的父类加载器可以加载,就成功返回,如果无法加载,再依次给子加载器去加载。
  双亲委派模型保证了Java程序的稳定运行,可以避免类的重复加载,也保证了 Java 的核心 API 不被篡改

3.6_3 【JVM原理】 P2 类加载器(ClassLoader)_第7张图片

  • 我们先通过代码来看一下类加载器的层级结构:
    • a.应用程序类加载器AppClassLoader
    • b.父类是扩展类加载器ExtClassLoader
    • c.扩展类加载器的父类输出了一个null;
    • d.这个null会去调用启动(BootStrap)类加载器。(根据ClassLoader类源码)
package com.groupies.base.JVM;

/**
 * @author GroupiesM
 * @date 2021/1/29
 * 类加载器:双亲委派机制
 * 目标:编写一个类,依次输出这个类的类加载器,父类加载器,父类的父类加载器
 */
public class ClassLoaderPath {
    public static void main(String[] args) {
        //sun.misc.Launcher$AppClassLoader@18b4aac2
        System.out.println(ClassLoaderPath.class.getClassLoader());
        //sun.misc.Launcher$ExtClassLoader@2503dbd3
        System.out.println(ClassLoaderPath.class.getClassLoader().getParent());
        //null
        System.out.println(ClassLoaderPath.class.getClassLoader().getParent().getParent());
    }
}
  • ClassLoader类源码

    3.6_3 【JVM原理】 P2 类加载器(ClassLoader)_第8张图片

  • 双亲委派模型流程

    • 当一个类加载器接收到类加载任务时,先查缓存里有没有,如果没有,将任务委托给它的父加载器去执行。
    • 父加载器也做同样的事情,一层一层往上委托,直到最顶层的启动(BootStrap)类加载器为止。
    • 如果启动(BootStrap)类加载器没有找到所需加载的类,便将此加载任务退回给下一级类加载器去执行,而下一级的类加载器也做同样的事情
    • 如果最底层类加载器仍然没有找到所需要的class文件,则抛出异常
  • 双亲委派模型流程图

    3.6_3 【JVM原理】 P2 类加载器(ClassLoader)_第9张图片

  • 为什么要双亲委派

    • 确保类的全局唯一性,避免核心API库被随意篡改。
    • 如果你自己写的一个类与核心类库中的类重名,会发现这个类可以被正常编译,但永远无法被加载运行。因为任何一个类第一次加载,首先会被委托到顶层,由启动(BootStrap)类加载器核心类库中寻找。如果没有双亲委托机制来确保类的全局唯一性,谁都可以编写一个java.lang.Object类放在classpath下,那应用程序就乱套了。
    • 从安全的角度讲,通过双亲委托机制,Java虚拟机总是先从最可信的Java核心API(启动类加载器)查找类型,可以防止不可信的类假扮被信任的类对系统造成危害。

双亲委派模型并不是绝对的,spi机制就可以打破双亲委派模型


2.4 破坏双亲委派

  • SPI(Service Provider Interface)机制
    • SPI概述spi是一种为接口寻找服务实现的机制:Java在核心库中定义了许多接口,并且针对这些接口给出调用逻辑,但是并未给出具体的实现。开发者要做的就是定制一个实现类,在 META-INF/services注册实现类信息,以供核心类库使用。最典型的就是JDBC。在JDBC 4.0之后实际上我们不需要再调用Class.forName来加载驱动程序了,我们只需要把驱动的jar包放到工程的类加载路径里,那么驱动就会被自动加载。这个自动加载采用的技术叫做SPI
    • JDBC为什么要破坏双亲委派模型
        Java提供了一个Driver接口用于驱动各个厂商的数据库连接DriverManager类位于 JAVA_HOMEjre/lib/rt.jar包 中,由 启动(BootStrap)类加载器 进行加载。根据类加载机制,当被加载的类引用了另外一个类的时候,虚拟机就会装载第一个类的 类加载器(ClassLoader) 装载被引用的类。也就是说要使用 启动(BootStrap)类加载器 加载Driver接口的实现类,我们知道,启动(BootStrap)类加载器默认只负责加载 JAVA_HOME中jre/lib/rt.jar 里所有的class,所以需要由子类加载器去加载Driver实现,这就破坏了双亲委派模型
        查看DriverManager类的源码,看到在使用DriverManager的时候会触发其静态代码块,调用 loadInitialDrivers() 方法,并调用ServiceLoader.load(Driver.class) 加载所有在META-INF/services/java.sql.Driver 文件里边的类到JVM内存,完成驱动的自动加载。
    • 于是Java提供了spi机制,即使Driver启动(BootStap)类加载器 去加载,但是他可以让线程 上下文加载器(Thread Context ClassLoader) 去请求子类加载器去完成加载,默认是应用程序(Application)类加载器。但是这确实破坏了类加载机制

2.5 SPI机制

  • 概述
    • SPI(Service Provider Interface)机制:主要是应用于厂商自定义组件或插件中,在java.util.ServiceLoader的API文档里有比较详细的介绍。 SPI机制是一种破坏双亲委派模型自动加载技术,用于加载接口的具体实现类。
    • SPI机制的思想:面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。 Java SPI就是提供这样的一个机制:为某个接口寻找服务实现的机制。 有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。
    • SPI机制的实例:当服务的提供者提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件,该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。基于这样一个约定就能很好的找到服务接口的实现类,而不需要再代码里制定。jdk提供服务实现查找的一个工具类:java.util.ServiceLoader。JDBC SPI mysql的实现如下所示。

      3.6_3 【JVM原理】 P2 类加载器(ClassLoader)_第10张图片

    • SPI机制的问题
        Java 提供了很多服务SPI,允许第三方为这些接口提供实现。这些 SPI 的接口Java 核心库来提供,而这些 SPI 的实现则是由各供应商来完成。终端只需要将所需的实现作为 Java 应用所依赖的 jar 包包含进类路径(CLASSPATH)就可以了。问题在于SPI接口中的代码经常需要加载具体的实现类:SPI的接口是Java核心库的一部分,是由启动(BootStrap)类加载器来加载的;而SPI的实现类是由系统类加载器来加载的。启动类加载器是无法找到 SPI 的实现类的(因为它只加载 Java 的核心库),按照双亲委派模型,启动类加载器无法委派系统类加载器去加载类。也就是说,类加载器的双亲委派模式无法解决这个问题。
        线程上下文类加载器正好解决了这个问题。线程上下文类加载器破坏了“双亲委派模型”,可以在执行线程中抛弃双亲委派加载链模式,使程序可以逆向使用类加载器。

21/02/18

M

你可能感兴趣的:(三.Java,java)