转载

面试高频:深入理解Java虚拟机之—JVM类加载过程和类加载器

深入理解Java虚拟机之—JVM类加载过程和类加载器

不仅是为了面试,还为了从根本上学习和理解Java代码的执行过程,提高自己对Java的理解

面试高频:深入理解Java虚拟机之—JVM类加载过程和类加载器

Java虚拟机生命周期:

  1. 程序正常结束
  2. 程序异常终止
  3. 操作系统错误
  4. System.exit()

类加载

添加idea属性打印加载的类 -XX:+TraceClassLoading

在Java代码中,类的加载、连接和初始化都是在运行时完后的,每一个类都通过类加载器加入加载到JVM中(堆中),形成一个虚拟机可以直接使用的Java类型

1.加载

把Java字节码加载成一段二进制流,读取到内存,放在运行时数据区的方法区内;创建一个java.lang.Class对象描述该类的数据结构

可以从磁盘、jar、war、网络、自己编写的class文件中加载class文件

2.连接

分为验证、准备、解析三个阶段

(1)验证

确保类加载的正确性,保证Class文件的字节流不会影响虚拟机的安全(因为class文件可以从任何途径生成),验证失败抛出 VerifyError ,验证通过就把内存中的二进制流存放到JVM的运行时数据区的方法区中

  1. 文件格式验证

文件开头魔数代表JDK版本号等信息;常量池中是否有不支持的常量

只有验证通过,二进制字节流才会进入内存的方法区存储

  1. 元数据验证

验证该类是否有父类,父类是否继承了不允许继承的类(final类);是否实现了父类或者接口中要求实现的方法;类中方法字段是否与父类或者接口匹配(参数类型、返回值类型)

  1. 字节码验证

对类的方法体进行验证,保证类型转换是安全的。

通过字节码验证也不一定是安全的, Halting Problem ,没有任何一个程序可以校验所有程序的合法性(比如while true是无法校验的)

  1. 符号引用验证

发生在符号引用转换为直接引用的时候

确保该符号引用可以找到对应类。

(2)准备

为类的静态变量分配内存(内存中方法区),并将其初始化为默认值(不是自己设置的值,例如 int a=1; 将a赋值为0)

(3)解析

将虚拟机常量池中的符号引用(一组符号描述目标引用,也就是JVM中的Reference)转换为直接引用(指向目标的实际内存地址)

3.初始化

  • 被动使用不会导致类的初始化

为静态变量赋初始值,执行static块

以下情况将触发初始化:

  1. 遇到 new , getstatic , putstatic , invokestatic 指令时,如果没有初始化将进行初始化
  2. 反射调用 reflect 包中,将初始化调用类
  3. 虚拟机启动时需要制定一个执行的主类,main函数类将进行初始化
  4. 初始化一个类时,父类没有被初始化,则将进行父类初始化
  5. JDK7中MethodHandler

对于静态字段,只有直接定义的地方才会被初始化

public class Test8 {
    public static void main(String[] args) {
        System.out.println(Son2.s);
    }
}
class Father2{
    public static int s = 1;
    static{
        System.out.println("hello i am father");
    }
}
class Son2 extends Father2{
    //不会打印这句 没有对Son2的主动使用
    static {
        System.out.println("hello i am son");
    }
}
复制代码

在初始化一个类时,要求其父类已经被初始化

在初始化一个接口时,不要求其父接口被初始化

在初始化一个类时,不要求其实现接口被初始化

接口变量不需要使用public static final修饰 默认是常量

案例:加载静态变量和常量

public class Test1 {
    public static void main(String[] args) {
        System.out.println(MyChild.s);
    }
}
class MyParent{
    /**
     * 当s申明为static时 会加载父类和子类,但是只会调用父类的static块
     * 当s加上final时,表示常量,不会加载任何一个类,编译阶段被放入该Test1类的常量池中
     */
    public static final String s = "dx";
    static {
        System.out.println("hello i am my parent");
    }
}
class MyChild extends MyParent{
    static {
        System.out.println("i am my child");
    }
}复制代码

面试高频:深入理解Java虚拟机之—JVM类加载过程和类加载器

案例:接口初始化

/**
 * 接口初始化时,不要求父接口被初始化完成
 * 常量如果编译时确定,就不会去加载
 * 如果时运行时才可以确定的常量,需要加载
 */
public class Test4 {
    public static void main(String[] args) {
        System.out.println(MyInterfaceSon.b);
    }
}
//一直不加载
interface MyInterface{
    public static final int  a = 5;
}
interface MyInterfaceSon extends MyInterface{
    //会加载,运行时确定
    public static final int  b = new Random().nextInt(10);
    //不会加载,编译时就已经确定
    //public static final int  b = 10;

}复制代码

案例:对象数组不被加载

public class Test3 {
    public static void main(String[] args) {
        /*
         * 不会加载MyParen4,数组类型不会导致加载,只会创建数组引用分配空间
         */
        MyParent3[] myParent = new MyParent3[10];
        //class [Ltop.dzou.jvm.MyParent3;
        //数组类型标志 [L 全限定名
        System.out.println(myParent.getClass());
    }
}
class MyParent3{

    static{
        System.out.println("i am my parent3");
    }
}复制代码

案例:静态常量的初始化

public class Test5 {
    public static void main(String[] args) {
        /**
         * 调用了getInstance方法 主动进行加载Singleton类
         * 准备阶段:初始化count1为0 singleton为null count2为0
         * 初始化完成后,按照顺序调用,执行了invokespecial执行了构造函数,执行完count1=1 count2=1
         * 调用完后执行了自己的putstatic指令 把count2设置为0
         * 最终结果:count1=0 count2=0
         */
        Singleton singleton = Singleton.getInstance();
        System.out.println(singleton.count1);
        System.out.println(singleton.count2);
    }
}
class Singleton{
    public static int count1;
    private static Singleton singleton = new Singleton();
    private Singleton(){
        count1++;count2++;
        System.out.println(count1);
        System.out.println(count2);
    }
    public static int count2 = 0;

    public static Singleton getInstance(){
        return singleton;
    }
}复制代码

面试高频:深入理解Java虚拟机之—JVM类加载过程和类加载器

双亲委托机制

加载一个类时,会由自底向上检查一个类是否被加载,如果没有被加载过,会尝试从顶向下加载,首先会由启动器加载器rt.jar加载Object,所有类被加载时都要保证Object类已经被加载

面试高频:深入理解Java虚拟机之—JVM类加载过程和类加载器

包含关系:

子加载器包含一个父亲加载器的引用,即使两个加载器属于一种类型的加载器(例如:同一种自定义加载器)

利用的是ClassLoader中构造方法可以传入一个parent也就是指向父类的类加载器的引用,加载时会优先委托给父类

面试题:

是否可以自定义一个java.lang.System类?

答:不行,因为自定义System在加载时会被委托到启动器类加载器加载,根据全限定名找到真正的System类加载后在执行main函数时会报找不到main方法,原因是自定义的System类不会被加载

public class System {
    public static void main(String[] args) {

    }
}

output:
错误: 在类 java.lang.System 中找不到 main 方法, 请将 main 方法定义为:
   public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application复制代码

双亲委派模型优点:

  1. 保证核心库的安全:如果都有自己的加载器加载,那么会存在很多命名空间,会存在很多相同的类,但是 无法相互兼容 使用(命名空间不同), 确保核心类被优先加载
  2. JVM相同的类可以存在的,通过命名空间相互隔离,可以一同存在,在不同命名空间中可以使用。

类加载器剖析

类加载器

JVM虚拟机类加载器: 启动器加载器扩展类加载器系统加载器

类加载器就是根据一个全限定名加载class生成二进制流并转换为一个java.lang.Class对象实例

  • 真正类的加载过程是由 defineClass 完成的,根据Java Doc
Converts an array of bytes into an instance of class Class. Before the Class can be used it must be resolved.复制代码

它将一个二进制流转换为一个java.lang.Class对象返回

命名空间

自己
同一个命名空间内的类是相互可见
有可能

扩展类加载器加载的class文件需要打成jar包

更改系统类加载器目录:修改java.system.class.loader为自定义

命令: java -Djava.system.class.loader /自定义加载器class文件路径

loadClass(String name) 加载名称为 name 的类,返回的结果是 java.lang.Class 类的实例。
findClass(String name) 查找名称为 name 的类,返回的结果是 java.lang.Class 类的实例。
findLoadedClass(String name) 查找名称为 name 的已经被加载过的类,返回的结果是 java.lang.Class 类的实例。
defineClass(String name, byte[] b, int off, int len) 把字节数组 b 中的内容转换成 Java 类,返回的结果是 java.lang.Class 类的实例。这个方法被声明为 final 的。
resolveClass(Class c) 链接指定的 Java 类。

{% qnimg jvm/4.png %}

案例:反射不导致类的初始化

public class Test9 {
    public static void main(String[] args) throws ClassNotFoundException {
        ClassLoader classLoader = ClassLoader.getSystemClassLoader();
        //classloader不会导致类的初始化
        Class<?> c = classLoader.loadClass("top.dzou.jvm.class_load.D");
        System.out.println("---------");
        //使用反射加载类会导致类的主动使用,从而初始化该类
        Class.forName("top.dzou.jvm.class_load.D");
        System.out.println(c);;
    }
}
class D{
    static {
        System.out.println("hello i am d");
    }
}复制代码

案例:实现一个类加载器

对于自定义的类加载器,我们通过继承ClassLoader类调用子类的loadClass方法加载类,loadClass方法会为我们自动调用findClass方法,其中需要实现自定义的加载类以及实现defineClass方法

public class Test10 extends ClassLoader{
    private String fileExt = ".class";
    private String path = null;
    public void setPath(String path) {
        this.path = path;
    }
    public Test10(){
        super();//super方法会使用系统加载器作为默认类加载器
    }
    @Override
    protected Class<?> findClass(String s) throws ClassNotFoundException {
        byte[] data = loadClassData(s);
        //找到class调用核心defineClass方法返回一个Class对象
        return defineClass(s,data,0,data.length);
    }
    //自己实现的加载类方法,把文件读取到二进制流中返回
    public byte[] loadClassData(String fileName){
        InputStream in = null;
        ByteArrayOutputStream baos = null;
        byte[] data = null;
        try {
            fileName = fileName.replace(".","/");
            in = new FileInputStream(new File(path+fileName+this.fileExt));
            baos = new ByteArrayOutputStream();
            int c = 0;
            while((c=in.read())!=-1){
                baos.write(c);
            }
            data = baos.toByteArray();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                in.close();
                baos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return data;
    }

    public static void main(String[] args) throws IllegalAccessException, InstantiationException, ClassNotFoundException {
        Test10 loader = new Test10();
        //调用ClassLoader的loadClass方法
        loader.setPath("/home/dzou/java/jvm-learning/target/classes/");
        Class<?> c = loader.loadClass("top.dzou.jvm.class_load.Test9");
        System.out.println("class:"+c);
        Object o = c.newInstance();
        System.out.println(o);
        System.out.println(o.getClass().getClassLoader());
    }
}复制代码

注意:根据双亲委托机制,会先交给父类去加载,也就是系统类加载器加载,系统类加载器能加载成功的话,就不会使用我们自定义的类加载器,所以我们需要把target中的.class文件删除,使用我们自定义的.class文件路径才会让系统类加载器加载失败,从而使用我们自定义的类加载器

命名空间使用

两个不同实例的加载器加载不同path下的class

public class Test13 {
    public static void main(String[] args) throws Exception {
        Test10 loader1 = new Test10();
        Test10 loader2 = new Test10();
        loader1.setPath("/home/dzou/Downloads/j/classes/");
        loader2.setPath("/home/dzou/Downloads/a/");
        Class<?> clazz2 = loader2.loadClass("top.dzou.jvm.class_load.Test1");
        Class<?> clazz1 = loader1.loadClass("top.dzou.jvm.class_load.Test1");
        Object o1 = clazz1.newInstance();
        Object o2 = clazz2.newInstance();
        System.out.println(o1.getClass().getClassLoader());
        System.out.println(o2.getClass().getClassLoader());
        System.out.println(o1==o2);
    }
}

输出:
top.dzou.jvm.class_load.Test10@6f94fa3e
top.dzou.jvm.class_load.Test10@1d44bcfa
false复制代码

继承关系

Launcher 系统和扩展类加载类-> ExtClassLoader/AppClassLoader 内部类-> URLClassLoader 支持通过路径和jar包加载-> SecureClassLoader 支持提供保护permissions权限(具体没有了解)-> ClassLoader

任意两个加载器都可以通过构造方法创建父子关系,即使是同一个类的类加载器

上下文类加载器

ContextClassLoader就是为了破坏Java双亲委派模型

我们了解了类加载器,现在看一下一个核心的加载器,就是 上下文类加载器ContextClassLoader

我们可以通过 Thread.currentThread().getContextClassLoader() 获取当前上下文类加载器

通过 Thread.currentThread().setContextClassLoader(ClassLoader cl); 来设置上下文类加载器

依赖规则:我们知道每一个类都会使用自己的类加载器加载该类中依赖的类,比如A类中引用了B类,那么加载A类的时候就会使用加载A的加载器加载B,而且每一个我们编写的类都是由 系统类加载器(AppClassLoader) 加载的,那

  • 为何出现上下文类加载器?

知道SPI的同学可能就知道JDBC、JAXP,不了解的下面一节会讲到,他们都是基于SPI实现的,基本上说就是JDK提供接口,服务商提供不同的实现(jar包),当我们使用这些SPI接口时,我们都要导入相应的jar包到classpath下的指定目录可能为lib,mysql-connectorJ等,但是我们的SPI接口是在rt.jar中的,是由启动器类为我们加载的,那么如果根据 依赖规则和双亲委派模型 ,JVM会使用加载该接口类的启动器加载器来加载我们的接口实现类,但是我们的SPI的不同实现类却在classpath下,这里是启动器类加载器加载不到的, classpath只能由系统类加载器或者自定义加载器加载 ,那么这样就会导致无法加载SPI接口实现类,所以 双亲委派模型 就不能在这起到合适的作用,我们就只能想办法去让 系统加载器来支持加载SPI实现类 ,于是出现了上下文类加载器

可能有人会说直接把各个厂商的实现放入对应的接口类所在包里不就好了,乍一看这么做是可以解决问题,但是你要知道的是无论在设计模式还是JDK中都是 面向扩展,对修改关闭的 ,这样做不仅违背了设计模式还会让JDK包变的务必庞大

  • 上下文类加载器的作用?

它改变了父加载器的加载方式,也就是破坏了双亲委托模型,它让父加载器可以使用当前线程的 Thread.currentThread().getContextClassLoader()`类加载器获取到加载classpath下类的加载器,使用该加载器去加载类,这就改变了父加载器不能使用子加载器加载的类的情况

根据双亲委派模型传递顺序,父类加载器加载不了才会交给子类加载器,所以它自然看不到并无法加载子类加载器加载的类,智慧的JDK开发者发现了这一点,想到了一个 线程中的类加载器 ,就可以通过线程的上下文类加载器来让父加载器可以访问子加载器所加载的类,就相当于把系统类加载器放在当前线程的上下文类加载器中,当父加载器需要获取子类加载器加载的类时,就可以通过这种方式获取

面试高频:深入理解Java虚拟机之—JVM类加载过程和类加载器

由此我们可以想到ThreadLocal类的实现,也是利用每个线程的独立性把需要的信息放入ThreadLocal,思想就是一种以空间换时间的策略(多个线程都有自己独立的ThreadLocal存储区,消耗了一定的空间,但是我们就不需要通过其他方式去存储需要的信息并获取,时间上有很大的优化)

源码文档写道:

If not set, the default is the ClassLoader context of the parent Thread. The context ClassLoader of the primordial thread is typically set to the class loader used to load the application.复制代码

告诉我们如果的上下文类加载器没有被设置,那么默认值就是加载当前线程的类加载器,加载当前线程的类加载器就是加载该应用的类加载器,一般为系统类加载器

我们后面就根据一些源码分析和案例使用来看一看上下文类加载器到底有多么强大的功能,竟然可以破坏双亲委派模型

SPI加载以及破坏双亲委派模型

SPI—Service Provider Interface,服务提供接口,像JDBC加载就是使用了spi,服务提供商使用spi扩展接口功能,类似根据jdk提供的一个接口不同服务提供商实现不同的接口实现,封装成一个jar包,我们通过导入这个jar包就可以使用服务提供商提供的该不同接口实现对应功能,通过ServiceLoader类加载不同服务提供商的实现—你可以简单理解为 策略模式

ServiceLoader

官方文档写的: 是一个加载服务提供商提供的服务实现的设备

A simple service-provider loading facility.复制代码

使用:官方文档写到:

A service provider is identified by placing a provider-configuration file in the resource directory META-INF/services. The file's name is the fully-qualified binary name of the service's type. The file contains a list of fully-qualified binary names of concrete provider classes, one per line. 复制代码

就是说服务提供商需要在提供的服务实现所在的resource目录中编写配置文件,指定文件目录为META-INF/services,文件名是服务类型的全限定名(也就是jdk中服务接口的接口全限定名),用于寻找服务接口,文件内容应该保存服务接口实现类的全限定名,也就是该类在jar包中的包名+类名

如: JDBC->文件名:java.sql.Driver 文件内容:com.mysql.cj.jdbc.Driver

JDK就会去找到java.sql.Driver这个接口,然后找到文件内容中的在jar包中对应的com.mysql.cj.jdbc.Driver类作为该接口的实现

面试高频:深入理解Java虚拟机之—JVM类加载过程和类加载器

同一个服务的不同提供商将根据jdk SPI规范编写符合规范的实现类(对类没有要求,只需要实现接口就好了,但是需要添加 META-INF/services/服务限定名 文件,在其中每一行写服务提供商提供的类相应的在jar包目录下的全限定名)

自定义SPI服务

下面我们自己实现一个spi服务看一下它到底是如何运作的,写完之后我们再看源码

  • 首先我们编写一个服务接口,接口包路径全限定名 top.dzou.jvm.spi
package top.dzou.jvm.spi;

public interface TestInterface {
    void saySomething();
}
复制代码
  • 再编写两个不同的接口服务实现,模拟不同服务提供商提供的不同实现,包路径为 top.dzou.jvm.spi.impl
package top.dzou.jvm.spi.impl;
public class ConcreteImpl1 implements TestInterface {
    @Override
    public void saySomething() {
        System.out.println("I am first service provider interface impl;");
    }
}复制代码
package top.dzou.jvm.spi.impl;
public class ConcreteImpl2 implements TestInterface {
    @Override
    public void saySomething() {
        System.out.println("I am second service provider interface impl;");
    }
}复制代码
  • 我们还需要编写配置文件,在classpath下的创建配置文件目录 META-INF/services ,配置文件名为接口包路径全限定名top.dzou.jvm.spi.TestInterface`
top.dzou.jvm.spi.impl.ConcreteImpl1
top.dzou.jvm.spi.impl.ConcreteImpl2复制代码
  • 编写一个测试类,使用 ServiceLoader
public class TestSpi {
    public static void main(String[] args) {
        //Thread.currentThread().setContextClassLoader(ClassLoader.getSystemClassLoader().getParent());
        ServiceLoader<TestInterface> loader = ServiceLoader.load(TestInterface.class);
        Iterator<TestInterface> iterator = loader.iterator();
        System.out.println("current class loaded by :"+TestSpi.class.getClassLoader());
        System.out.println("current thread loader :"+Thread.currentThread().getContextClassLoader());
        System.out.println("service interface loader :"+loader.getClass().getClassLoader());
        while(iterator.hasNext()){
            TestInterface next = iterator.next();
            next.saySomething();
        }
    }
}

输出:
current class loaded by :sun.misc.Launcher$AppClassLoader@18b4aac2
current thread loader :sun.misc.Launcher$AppClassLoader@18b4aac2
service interface loader :null
I am first service provider interface impl;
I am second service provider interface impl;复制代码

如果我们把main函数第一行之前加上一行

Thread.currentThread().setContextClassLoader(ClassLoader.getSystemClassLoader().getParent());复制代码

输出为

current class loaded by :sun.misc.Launcher$AppClassLoader@18b4aac2
current thread loader :sun.misc.Launcher$ExtClassLoader@266474c2
service interface loader :null复制代码

解释:

你可以把我们写的接口实现看成是某个服务商提供者编写的jar包的类,把接口看成是JDK提供的服务接口,然后在jar包中的resource目录下的 META-INF/services 中编写了一个与JDK提供服务接口全限定名相同的配置文件,在其中配置了两个具体实现类的类全限定名,就可以通过ServiceLoader去使用这两个类作为JDK接口的实现类,我们在测试类中测试的结果可以看到除了ServiceLoader类由启动类加载器加载,线程和测试类都是通过系统类加载器加载的;

但是 当我们设置了扩展类为线程上文文类加载器的时候,可以看到打印结果是我们自己编写的服务接口实现没有被加载,那这是为什么?

答:很简单,因为ServiceLoader是通过上下文类加载器获取到系统类加载器的引用,通过系统类加载器来帮助我们实现访问服务实现的类,但是现在我们的上下文类加载器为扩展类加载器,显然扩展类加载器是加载和访问不了我们自己编写的服务实现类,所以自然没有打印处加载的信息,更没有去调用方法

SPI原理以及ServiceLoader源码分析

我们通过上下文类加载器和自定义SPI实现大致已经知道SPI是怎么运作的了,我们下面看一下它的源码

因为sun公司源码有些是不对外开放的,所以我们看一下反编译的源码就好了,大致都能理解

  • 首先在ServiceLoader中有这样一段代码
private static final String PREFIX = "META-INF/services/";复制代码

现在我们就可以看懂这是什么了,为什么服务提供商都要在jar包中在classpath目录下编写这么一个目录,就是一个绝对路径,系统类加载器就是通过这个路径去寻找jar包中的服务接口实现类

  • 我们再看一下自定义SPI实现的ServiceLoader.load()方法
public static <S> ServiceLoader<S> load(Class<S> var0) {
    ClassLoader var1 = Thread.currentThread().getContextClassLoader();//核心方法
    return load(var0, var1);
}复制代码

在load中ServiceLoader拿到了上下文类加载器,作为参数传入load方法

private ServiceLoader(Class<S> var1, ClassLoader var2) {
        this.service = (Class)Objects.requireNonNull(var1, "Service interface cannot be null");
        this.loader = var2 == null ? ClassLoader.getSystemClassLoader() : var2;
        this.acc = System.getSecurityManager() != null ? AccessController.getContext() : null;
        this.reload();
    }复制代码

load方法返回了一个ServiceLoader对象,构造方法把loader设置为了刚刚拿到的当前线程上下文类加载器

  • 我们看一下使用 loader 的地方

ServiceLoader维护了一个内部类 LazyIterator 实现了 Iterator 接口作为使用服务提供商在配置文件中编写的所有服务实现类的迭代器,看一下 hasNextService 方法,我把关键部分留了下来

private boolean hasNextService() {
    //关键是这里,反编译把常量直接加载过来了
    if (this.configs == null) {
        try {
            String var1 = "META-INF/services/" + this.service.getName();//这里service就是
            if (this.loader == null) {
                this.configs = ClassLoader.getSystemResources(var1);//一般不会来到这,如果出现异常来到这也要把loader设置为系统类加载器
            } else {
                this.configs = this.loader.getResources(var1);//使用系统类加载器根据jar包中路径获取资源,也就是使用服务实现
            }
        } catch (IOException var2) {
            ServiceLoader.fail(this.service, "Error locating configuration files", var2);
        }
           
//下面使用迭代器,负责判断是否有其他服务实现
                while(this.pending == null || !this.pending.hasNext()) {
                    if (!this.configs.hasMoreElements()) {
                        return false;
                    }

                    this.pending = ServiceLoader.this.parse(this.service, (URL)this.configs.nextElement());
                }

                this.nextName = (String)this.pending.next();
                return true;
            }
        }复制代码

再看一下nextService()方法

private S nextService() {
                String var1 = this.nextName;//拿到下一个服务类的类全限定名
                this.nextName = null;
                Class var2 = null;
                try {
                    var2 = Class.forName(var1, false, this.loader);//使用反射加载服务实现,loader为系统类加载器,var1为nextName就是服务类全限定名
                    
                    Object var3 = this.service.cast(var2.newInstance());
                    ServiceLoader.this.providers.put(var1, var3);//加载成功放入Maop中
                    return var3;
                    }
        }复制代码

面试高频:深入理解Java虚拟机之—JVM类加载过程和类加载器

  • 我们看一下最根本的Launcher中的初始化方法,我们知道Launcher就是负责类加载器的加载,相当于应用的主启动类

里面有这样一段代码

try {
        this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
            throw new InternalError("Could not create application class loader", var9);
        }

        Thread.currentThread().setContextClassLoader(this.loader);复制代码

它首先就是获取系统类加载器作为Launcher中把保存的loader引用,因为它是JDK最下面的类加载器。可以通过getParent方法获取上册加载器;并且调用了 Thread.currentThread().setContextClassLoader方法把系统类加载器设置为当前线程的上下文类加载器

SPI原理和ServiceLoader的源码讲完我们下面看一下SPI对服务接口的实际使用

SPI—JDBC加载分析

我们一般通过 Class.forName("com.mysql.cj.jdbc.Driver"); 先使用加载当前类的加载器(也就是系统类加载器)加载该classpath下的mysql驱动

面试高频:深入理解Java虚拟机之—JVM类加载过程和类加载器

现在我们再来看这张图片就能会容易理解了,配置文件的内容你可能也已经想到了,就是JDBC的mysql驱动

com.mysql.cj.jdbc.Driver 或者 com.mysql.jdbc.Driver

  • 我们看一下这个mysql的Driver类
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws SQLException {
    }
    static {
        try {
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can't register driver!");
        }
    }
}复制代码

我们在通过Class.forName加载完该Driver时会自动初始化该类,就会执行static语句块,自然就会加载引用的DriverManger,根据双亲委托模型,把加载DriverManager的任务交给启动器类加载器

  • 加载完成后继续执行上面static块会执行registerDriver方法,自然就会先初始化DriverManager,执行下述DriverManager的static块
static {
        loadInitialDrivers();
    }复制代码
  • loadInitialDrivers

我们看一下它静态块中执行的初始化Driver的方法

private static void loadInitialDrivers() {
        String var0 = (String)AccessController.doPrivileged(new PrivilegedAction<String>() {
                public String run() {
                    return System.getProperty("jdbc.drivers");//如果存在系统的jdbc driver则返回,一般不存在,需要加载
                }
            });
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                ServiceLoader var1 = ServiceLoader.load(Driver.class);//ServiceLoader加载java.sql.Driver
                Iterator var2 = var1.iterator();
                while(var2.hasNext()) {//通过hasNext调用hasNextService方法拿取配置文件中指定的类的资源
                    var2.next();//调用nextService方法会通过Class.forName()加载这个类
                }
            } 
                return null;
            }
        });
        if (var0 != null && !var0.equals("")) {//如果System.getProperty("jdbc.drivers");中有驱动
            String[] var1 = var0.split(":");
            String[] var2 = var1;
            int var3 = var1.length;
            for(int var4 = 0; var4 < var3; ++var4) {
                String var5 = var2[var4];
                println("DriverManager.Initialize: loading " + var5);
                Class.forName(var5, true, ClassLoader.getSystemClassLoader());//尝试加载System.getProperty中的驱动
            }
        }
    }复制代码

这么一看进行了很多次Class.forName()加载驱动,那我们为什么还需要手动调用 Class.forName("com.mysql.cj.jdbc.Driver"); ?是不是可以不手动调用这一步?

答案是可以的,我们手动调用这步是因为JDK以前还不支持这种做法,需要调用,但是后面版本的JDK中可以不需在调用这一句了,因为只要在classpath中,它就会在loadInitialDrivers中调用next中调用nextService方法中调用了这句Class.forName()

  • 加载了驱动后,下面我们再看一下它的获取连接的方法,里面还有与类加载有关的过程

String var0: 驱动类全限定名

Properties var1: 包含数据库连接参数的配置信息

Class var2: 反射拿到的调用getConnetion方法的类

关键代码如下

private static Connection getConnection(String var0, Properties var1, Class<?> var2) throws SQLException {
        ClassLoader var3 = var2 != null ? var2.getClassLoader() : null;//拿到加载调用类的类加载器,一般为系统类加载器
        Class var4 = DriverManager.class;
        synchronized(DriverManager.class) {
            if (var3 == null) {
                var3 = Thread.currentThread().getContextClassLoader();//如果不是系统类加载器就设置为当前线程的1类加载器,也就是存储的系统类加载器的引用
            }
        } 
            Iterator var5 = registeredDrivers.iterator();
            while(true) {
                while(var5.hasNext()) {//调用迭代器来加载驱动
                    DriverInfo var6 = (DriverInfo)var5.next();
                    if (isDriverAllowed(var6.driver, var3)) {//关键在这里
                        Connection var7 = var6.driver.connect(var0, var1);
                        if (var7 != null) {
                            return var7;
                        }
                    }
                }
            }
    }复制代码
  • isDriverAllowed方法

就是为了辨别驱动var0是否有var1(当前线程的类加载器、加载当前调用类的类加载器)所加载,也就是var0是否在var1类加载器的命名空间中

出现这种情况的原因:

1.上下文类加载器被设置为了高层的类加载器而不是系统类加载器

2.线程被切换了,当前线程的上下文类加载器不是加载调用类的类加载器

不同的类加载器对应不同的命名空间,这样的话,上下文类加载器引用的类加载器无法加载该驱动,也就无法使用该驱动

private static boolean isDriverAllowed(Driver var0, ClassLoader var1) {
    boolean var2 = false;
    if (var0 != null) {
        Class var3 = null;
        try {
            var3 = Class.forName(var0.getClass().getName(), true, var1);
        } catch (Exception var5) {
            var2 = false;//如果异常发生,表示无法由var0加载var1,命名空间不同
        }
        var2 = var3 == var0.getClass();//否则只需要判断加载的类和var0驱动类是否是一个类
    }
    return var2;
}复制代码

Tomcat加载简要分析

Web服务器加载需求

  • 部署在同一个服务器的两个web应用程序使用的java类库相互隔离,两个不同的应用程序也可以依赖用一个第三方类库的不用版本,所以一个类库只能在一个应用程序中可见
  • 部署在一个服务器上的两个web应用可以共享Java类库,10个依赖Spring,那么10个应用都需要一个独立的Spring?显然是不需要的
  • 为了安全性,服务器所使用的类库应该与应用程序类库隔离
  • 像JSP这种文件,需要支持动态热更新,JSP修改后无需重启服务器,只需要刷新页面就可以了

tomcat加载模型

我们在上述情况下思考一下双亲委托模型可以实现吗?

显然不行,所以tomcat创建了自己的一套加载模型,如下:

面试高频:深入理解Java虚拟机之—JVM类加载过程和类加载器

  1. common类加载器 就是负责加载服务器和应用程序都可以共享的类库,如classpath下的lib目录
  2. catalina类加载器 负责加载服务器独立的类库,为了安全性不与应用程序共享的类库
  3. shared类加载器 就负责加载应用程序之间共享的类库,像是Spring这样的
  4. WebApp类加载器 加载单个应用程序独立的类库,对其他应用程序不可见,如webapp下类库
  5. jsp类加载器 负责jsp文件加载成servlet类,它需要解决 热更新 的问题

JSP文件的热更新加载

我们知道一般加载过程,创建一个JSP页面,启动服务器时由加载器加载成servlet类字节码文件,但是当你JSP内容修改了以后,就相当于类文件被修改了,这个时候我们只能重新启动应用程序来再次加载这个类来实现修改后的更新,但是如果是这样的话就没有人使用 JSP

tomcat考虑到了这一点,提出了一种 一个类加载器对应一个JSP文件 的实现方法

我们每次为JSP文件加载创建一个特定的加载器,每个JSP就有一个类加载器,当我们在运行时发现JSP被修改了的话,我们就丢弃那个加载出来的Class文件,通过重新建立一个新的JSP类加载器来加载更新的JSP文件

为了实现不同应用程序隔离,服务器和应用程序隔离,就不同在使用双亲委托模型,它会把所有加载交给父类,而保证每个类有且仅由一个,所以tomcat不得不 破坏双亲委托模型 ,但它只是没有遵循交给上层加载的规定,加载模型还是自上而下的

Tomcat决定把webapp目录下的类由自己的WebappClassLoader加载,不委托给父类加载器,然后通过舞弊的 上下文类加载器 来实现父加载器对子类加载器加载的类的访问与可见性

原文  https://juejin.im/post/5dce363e5188254ed649d8e0
正文到此结束
Loading...