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

47次阅读

共计 18729 个字符,预计需要花费 47 分钟才能阅读完成。

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

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

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");
    }
}

案例:接口初始化

/**
 * 接口初始化时,不要求父接口被初始化完成
 * 常量如果编译时确定,就不会去加载
 * 如果时运行时才可以确定的常量,需要加载
 */
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;}
}

双亲委托机制

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

包含关系:

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

利用的是 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 开发者发现了这一点,想到了一个 线程中的类加载器,就可以通过线程的上下文类加载器来让父加载器可以访问子加载器所加载的类,就相当于把系统类加载器放在当前线程的上下文类加载器中,当父加载器需要获取子类加载器加载的类时,就可以通过这种方式获取

由此我们可以想到 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 类作为该接口的实现

同一个服务的不同提供商将根据 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;
                    }
        }

  • 我们看一下最根本的 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 驱动

现在我们再来看这张图片就能会容易理解了,配置文件的内容你可能也已经想到了,就是 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 创建了自己的一套加载模型,如下:

  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 加载,不委托给父类加载器,然后通过舞弊的 上下文类加载器 来实现父加载器对子类加载器加载的类的访问与可见性

正文完
 0