天天看点

【JVM】说说双亲委派源码、SPI、线程上下文类加载器TCCL

说明:

本文虽然借鉴了网络的部分内容,但是由于网络上内容基本都是摘抄JVM书来的,有的地方很晦涩难懂,尤其是SPI和线程上下文类加载器TCCL那里。

本文通过根据jdbc的驱动加载过程,跟踪

双亲委派源码

rt.jar

Class.forName

SPI

TCCL

的执行顺序,分析了它们之间的关系。总结完收获很大,希望你也有所收获

各小节没有标序号,但大章节序号是有的,慢慢看即可,我花了一整天整理学习。不说了,去睡觉了

类的使用流程:

  • 是否加载了该类
    • 没有加载:使用类加载器加载该类
    • 加载了:链接–初始化—调用main方法

一、简述

类加载归纳为有三个阶段:

先编译java为class,启动程序后开始进行类装载

1) 、加载:

从文件系统或者网络中查找并加载类的二进制数据,利用二进制数据创建class对象

2) 、连接:

2.1) 、验证 : 确保被加载的类的正确性,确保class文件的字节流中信息符合JVM的要求,不会危害JVM的安全,使得JVM免受恶意代码的攻击。主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。

2.2) 、准备:为类的static静态变量分配内存,并将其初始化为默认值,但是到达初始化之前类变量都没有初始化为真正的初始值。(这里不包含final修饰的static变量,因为final在编译时候就会分配了,准备阶段会显示初始化。)(这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量会随着对象一起分配到java堆中)这些内存都将在方法区中进行分配。

2.3) 解析

2.3、解析:把类中的符号引用转换为直接引用,就是在类的常量池中寻找类、接口、字段和方法的符号引用,把这些符号引用替换成直接引用的过程。虚拟机规范之中并未规定解析阶段发生的具体时间,只要求了在执行anewarray、checkcast, getfield, getstatic, instanceof, invokeinterface, invokespecial, invokestatic、invokevirtual, multianewarray、new、 putfield和 putstatIc这13个用于操作符号引用的字节码指令之前,先对它们所使用的符号引用进行解析。所以虚拟机实现会根据需要来判断,到底是在类被加载器加载时就对常量池中的符号引用进行解析,还是等到一个符号引用将要被使用前才去解析它。

解析动作主要针对类或接口、字段、类方法、接口方法四类符号引用进行,分别对应于常量池的 CONSTANT Class info、 CONSTANT_Fieldref_info、 CONSTANT_Methodref_info及 CONSTANT_InterfaceMethodref_info四种常量类型。

  • 符号引用( Symbolic References):符号引用以一组符号来描述所引用的目标,符号引用可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,无用的且标并不一定已经加载到内存中。
    • 符号引用在 Class文件中它以 CONSTANT_Class_info、CONSTANT_Fieldref_info、 CONSTANT_Methodref_info等类型的常量出现
  • 直接引用 (Direct Referenc):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在内存中存在。

2.3.1、解析阶段:

对同一个符号引用进行多次解析请求是很常见的事情,虚拟机实现可能会对第一次解析的结果进行缓存(在运行时常量池中记录直接引用,并把常量标识为已解析状态)从而避免解析动作重复进行。无论是否真正执行了多次解析动作,虚拟机需要保证的都是在同一个实体中,如果一个符号引用之前已经被成功解析过,那么后续的引用解析请求就应当一直成功;同样地,如果第一次解析失败了,其他指令对这个符号的解析请求也应该收到相同的异常。

下面将讲解这四种引用的解析过程。

  • 1.类或接口的解析:

假设当前代码所处的类为D,如果要把一个从未解析过的符号引用N解析为一个类或接口C的直接引用,那虚拟机完成整个解析的过程需要包括以下3个步骤:

1)如果C不是一个数组类型,那虚拟机将会把代表N的全限定名传递给D的类加载器去加载这个类C。在加载过程中,由于无数据验证、字节码验证的需要,又将可能触发其他相关类的加载动作,例如加载这个类的父类或实现的接口。一旦这个加载过程出现了任何异常,解析过程就将宣告失败

2)如果C是一个数组类型,并且数组的元素类型为对象,也就是N的描述符会是类似“[ Ljava. ang Integer”的形式,那将会按照第1点的规则加载数组元素类型。如果N的描述符如前面所假设的形式,需要加载的元素类型就是“ java.lang.Integer”,接着由虚拟机生成一个代表此数组维度和元素的数组对象

3)如果上面的步骤没有出现任何异常,那么C在虚拟机中实际上已经成为一个有效的类或接口了,但在解析完成之前还要进行符号引用验证,确认C是否具备对D的访问权限。如果发现不具备访问权限,将抛出 java. lang. IllegalAccessError异常

  • 2.字段解析:

要解析一个未被解析过的字段符号引用,首先将会对字段表内 class index项中索引的CONSTANT_Class_info符号引用进行解解析,也就是字段所属的类成接口的符号引用。如果在解析这个类或接口符号引用的过程中出现了任何异常,都会导致字段符号引用解析的失败。如果解析成功完成,那将这个字段所属的类或接口用C表示,虚拟机规范要求按照如下步骤对C进行后续字段的搜索

1)如果C本身就包含了简单名称和字段描述符都与目标相匹配的字段,则返这个字段的直接引用,查找结束

2)否则,如果在C中实现了接口,将会按照继承关系从上往下递归搜索各个接口和它的父接口,如果接口中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束

3)否则,如果C不是 java. lang Object的话,将会按照继承关系从上往下递归搜索其父类)如果在父类中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束

4)否则,查找失败,抛出 java. lang NoSuch Field Error异常

如果查找过程成功返回了引用,将会对这个字段进行权限验证,如果发现不具备对字段的访问权限,将抛出 java. lang. IllegalAccess Error异常

在实际应用中,虚拟机的编译器实现可能会比上述规范要求得更加严格一些,如果有一个同名字段同时出现在C的接口和父类中,或者同时在自己或父类的多个接口中出现,那编译器将可能拒绝编译。在代码清单7-4中,如果注释了Sub类中的“ public static int A=4;”,接口与父类同时存在字段A,那编译器将提示“ The field Sub.A is ambiguous" ,并且会拒绝编译这段代码

  • 3.类方法解析

类方法解析的第一个步骤与字段解析一样,也是需要先解析出类方法表的 class index项中索引的方法所属的类或接口的符号引用,如果解析成功,我们依然用C表示这个类,接下来虚拟机将会按照如下步骤进行后续的类方法搜索:

1)类方法和接口方法符号引用的常量类型定义是分开的,如果在类方法表中发现 class_index中索引的C是个接口,那就直接抛出java.lang.IncompatibleClassChangeError异常。

2)如果通过了第(1)步,在类C中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。

3)否则,在类C的父类中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。

4)否则,在类C实现的接口列表及它们的父接口之中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果存在匹配的方法,说明类C是一个抽象类,这时候查找结束,抛出java.lang.AbstractMethodError异常

5)否则,宣告方法查找失败,抛出java.lang.NoSuchMethodError

最后,如果查找过程成功返回了直接引用,将会对这个方法进行权限验证;如果发现不具备对此方法的访问权限,将抛出 java .lang.IllegalAccessError异常

  • 4.接口方法解析

接口方法也是需要先解析出接口方法表的 class index项中索引的方法所属的类或接口的符号引用,如果解析成功,依然用C表示这个接口,接下来虚拟机将会按照如下步骤进行后续的接口方法搜索:

1)与类方法解析相反,如果在接口方法表中发现 class index中的索引C是个类而不是接口,那就直接抛出java.lang. IncompatibleClassChangeError异常。

2)否则,在接口C中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。

3)否则,在接口C的父接口中递归查找,直到 javalang Object类(查找范围会包括 Object类)为止,看是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。

4)否则,宣告方法查找失败,抛出java.lang.NoSuchMethodError异常

由于接口中的所有方法都默认是 public的,所以不存在访问权限的问题,因此接口方法的符号解析应当不会抛出java.lang. IllegalAccess Error异常。

3) 、初始化:

为类的静态变量赋予正确的初始值。为新的对象分配内存,为实例变量赋默认值,为实例变量赋正确的初始值。初始化阶段就是指向类构造器方法

<clinit>()

【意思是class init】的过程,此方法不需定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。

<clinit>()

不同于类的构造器,若该类具有父类,JVM会保证子类的

<clinit>

执行前,父类的

<clinit>

已经执行完毕。JVM必须保证一个类的

<clinit>()

方法在多线程下被同步加锁。

类加载最后阶段,若该类具有超类,则对其进行初始化,执行静态初始化器和静态初始化成员变量(如前面只初始化了默认值的static变量将会在这个阶段赋值,成员变量也将被初始化)。

3.1、

<clinit>()

是由编译器自动收集类中的所有类变量的赋值动作(static变量)和静态语句块(static代码块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块中可以赋值,但是不能访问。

3.2、

<clinit>()

方法与类的构造函数(或者说实例构造器

<init>

方法)不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的

<clinit>()

方法执行之前,父类的

<clinit>()

方法已经执行完毕。因此在虚拟机中第一个被执行的

<clinit>()

方法的类肯定是java.lang.Object

3.3、由于父类的

<clinit>()

方法先执行,也就意味着父类中定义的静态语块要优先于子类的变量赋值操作,如代码清单7-5中,字段B的值将会是2而不是1

static class Parent{
    public static int A=1;
    static{
        A=2;
    }
}
public class Sub extends Parent{
    public static int B=A;
}
public static void main(String[] args){
    Sub.B.sout;
}
           

3.4、

<clinit>()

方法对于类和接口来说并不是必须的,如果一个类中没有静态代码块,也没有对变量的赋值操作,那么编译器可以不为这个类生成

<clinit>()

方法方法

3.5、 接口中不能使用静态代码块,但如果有变量初始化的赋值操作,接口与类一样都会生成

<clinit>()

方法。但接口与类不同的是,执行接口的

<clinit>()

方法不需要先执行父接口的

<clinit>()

方法。只有父接口定义的变量被使用时,父接口才回被初始化。另外,接口的实现类在初始化时也一样不会执行接口的

<clinit>()

方法

3.6、 虚拟机会保证一个类的

<clinit>()

方法在多线程环境中被正确地加锁和同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的

<clinit>()

方法,其他线程都需要阻塞等待,直到活动线程执行

<clinit>()

方法完毕。如果一个类的

<clinit>()

方法有耗时很长的操作,那就可能造成多个进程阻塞,在实际应用中这种阻塞往往是很隐蔽的。

实例初始化是在实例的构造函数中,而他相应的父类是调用super()完成的,
  • 如果没有显示写super(),那么将加在第一句。
  • 当父类没有无参构造函数时,在子类构造方法中必须显示指定,如super(“hello”)
有父类有无参构造时,super可以省略,此时子类可以使用this调用构造方法,this在构造方法的作用是调用子类的其他构造方法;父类没有无参构造时,子类不能使用this调用构造方法

非静态成员的赋值,是在自己的构造调用之后,并且是在自己的构造调用完父类的构造super之后,

在非静态成员全部赋值完成,才会继续执行自己构造内,剩余代码。

以final关键字为例先体会一下类加载流程

// 常量都是用final来修饰的,所以只要在包含它类实例化对象完成之前初始化就行了,什么都不影响。但是如果前面加个static表明类装载时这个常量必须是有个状态的(被赋予了值,初始化了),所以如果用static就必须类加载时初始化。

// 只被final关键字修饰的常量,可以在其类加载时就初始化,也可以到类的构造方法里面再对它进行初始化:例如
class A{
    final int i;//或者final int i=10; // 有没有值无所谓,实例化的构造方法完成之前有值就可
    public A(){
        i=10;
    }
}
//用static和final关键字同时修饰的常量就必须得在定义时初始化,例如:
class A{
    static final int i=10;//编译时候就赋值了,它是常量
}

// 基本类型,是值不能被改变  //引用类型,是地址值不能被改变,对象中的属性可以改变
public static void method(final int x) { //此处的final修饰的 x随着方法使用完毕后回收,当再次调用时,重新分配空间
	System.out.println(x);
}
           

说明:

  • 在java代码中,类的加载、连接和初始化过程都是在程序运行期间完成的。(类从磁盘加载到内存中经历的三个阶段)
  • 类从磁盘上加载到内存中要经历五个阶段:加载、连接、初始化、使用、卸载

二、1)类加载器的分类

类加载定义:将类的.class文件中的二进制数据(字节流)读入到内存中,将其放在运行时数据区的方法区内,然后在内存中创建一个java.lang.Class对象(规范并未说明Class对象位于哪里,HotSpot虚拟机将其放在方法区中)用来封装内在方法区内的数据结构。

类的加载时机:类并不需要等到某个类被“首次主动使用”时再加载它:JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类才报告错误(LinkageError错误),如果这个类没有被程序主动使用,那么类加载器就不会报告错误。

将二进制字节流锁代表的静态存储结构转化为方法区的运行时数据结构
【JVM】说说双亲委派源码、SPI、线程上下文类加载器TCCL

注:

  • class文件在文件开头有特定的文件标示
  • ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定
  • 加载.class文件的方式

    (1)从本地系统中直接加载

    (2)通过网络下载.class文件

    (3)从zip,jar等归档文件中加载.class文件

    (4)从专用数据库中提取.class文件

    (5)将java源文件动态编译为.class文件

① 启动类加载器

① 启动类加载器/根加载器/引导类加载器(Bootstrap):

  • C++编写。默认加载路径

    $JAVAHOME/$jre/lib/rt.jar

    ,或者被-Xbootclasspath参数所指定的路径。里面有如rt.jar/sun/misc/Launcher.class,Object.class等。该加载器没有父加载器,它负责加载虚拟机中的核心类库。根类加载器从系统属性sun.boot.class.path所指定的目录中加载类库。类加载器的实现依赖于底层操作系统,属于虚拟机的实现的一部分,它并没有集成java.lang.ClassLoader类。出于安全考虑,根加载器只加载包名为java,javax,sun等开头的类,意味着及时将你自己的jar放到该目录下也不一定被加载,因为在JVM内已经按照文件名识别。
//通过java对象.object.getClass().getClassLoader();获取该类的载器
Object object = new Object(); // object类的类加载器是根加载器
object.getClass().getClassLoader();//null。Bootstrap根加载器是c++写的,java查不出来
try {
    object.getClass().getClassLoader().getParent();//报错,根加载器是最初级的了
} catch (Exception e) {
    System.out.println(object.getClass().getClassLoader() + "没有父加载器了");
}



//-----------------------------------------
Object object2 = new Main2();//自己写的类
System.out.println(object2.getClass().getClassLoader());//[email protected]
//自定义类默认的加载器是应用加载器AppClassloader//sun.misc.launcher$AppClassLoader$18b4aac2。位于rt.jar包中

ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();//获取系统加载器 /
System.out.println(systemClassLoader);//[email protected]
System.out.println(systemClassLoader.getParent());// [email protected]
System.out.println(systemClassLoader.getParent().getParent());//null
           
//获取根加载器所能加载的路径
URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
for (URL element : urLs) {
    System.out.println(element.toExternalForm());
}
/*
file:/E:/Java/jdk1.8.0_231/jre/lib/resources.jar
file:/E:/Java/jdk1.8.0_231/jre/lib/rt.jar
file:/E:/Java/jdk1.8.0_231/jre/lib/sunrsasign.jar
file:/E:/Java/jdk1.8.0_231/jre/lib/jsse.jar
file:/E:/Java/jdk1.8.0_231/jre/lib/jce.jar
file:/E:/Java/jdk1.8.0_231/jre/lib/charsets.jar
file:/E:/Java/jdk1.8.0_231/jre/lib/jfr.jar
file:/E:/Java/jdk1.8.0_231/jre/classes
*/
System.out.println(System.getProperty("sun.boot.class.path"));//获取根加载器路径 // 结果和上面的遍历结果一致

           

② 扩展类加载器

  • Java编写 ,由sun.misc.Launcher$ExtClassLoader(意思是说ExtClassLoader是Launcher的静态内部类)
  • 默认加载路径

    JDK安装目录/jre/lib/ext/*.jar

    (或通过

    -Djava.ext.dirs

    系统属性重新指定)如果用户创建的JAR放在此目录下,也会由拓展类加载器自动加载。
类结构

继承了ClassLoader,并且是Launcher的静态内部类

public class Launcher {
    static class ExtClassLoader extends URLClassLoader {
        public class URLClassLoader extends SecureClassLoader implements Closeable {
            public class SecureClassLoader extends ClassLoader {
           
加载路径

默认加载路径

JDK安装目录/jre/lib/ext/*.jar

(或通过

-Djava.ext.dirs

系统属性重新指定)如果用户创建的JAR放在此目录下,也会由拓展类加载器自动加载。

System.out.println(System.getProperty("java.ext.dirs"));//获取扩展类加载器路径
// E:\Java\jdk1.8.0_231\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext
           

③ 应用程序类加载器

应用程序类加载器(AppClassLoader,也叫系统类加载器):

  • Java编写,它的父加载器为扩展类加载器,他是用户自定义的类加载器的默认父加载器。sun.misc.Launcher$AppClassLoader,也是静态内部类
  • 加载当前应用的classpath的所有类。默认加载路径为:

    环境变量$classpath

    (可以通过

    -Djava.class.path

    重新指定)。
  • 可以通过

    ClassLoader.getSystemClassLoader()

    获取到应用类加载器。
类结构

继承了ClassLoader,并且是Launcher的静态内部类

// 
public class Launcher {
    static class AppClassLoader extends URLClassLoader {
        public class URLClassLoader extends SecureClassLoader implements Closeable {
            public class SecureClassLoader extends ClassLoader {
           
加载路径
加载路径
System.out.println(System.getProperty("java.class.path"));//获取应用类加载器路径
/*
E:\Java\jdk1.8.0_231\jre\lib\charsets.jar;E:\Java\jdk1.8.0_231\jre\lib\deploy.jar;。。。E:\Java\jdk1.8.0_231\jre\lib\management-agent.jar;E:\Java\jdk1.8.0_231\jre\lib\plugin.jar;E:\Java\jdk1.8.0_231\jre\lib\resources.jar;E:\Java\jdk1.8.0_231\jre\lib\rt.jar;F:\springbootTest\testBoot\target\classes;D:\MavenRepository\org\springframework\boot\spring-boot-starter-web\2.3.1.RELEASE\spring-boot-starter-web-2.3.1.RELEASE.jar;D:\MavenRepository\org\springframework\boot\spring-boot-starter\2.3.1.RELEASE\spring-boot-starter-2.3.1.RELEASE.jar;
。。。省略
D:\MavenRepository\org\springframework\spring-jcl\5.2.7.RELEASE\spring-jcl-5.2.7.RELEASE.jar;D:\MavenRepository\org\projectlombok\lombok\1.18.12\lombok-1.18.12.jar;E:\JetBrains\IntelliJ IDEA 2019.3.3\lib\idea_rt.jar

*/
System.out.println(System.getProperty("sun.boot.class.path"));//打印跟加载路径
// 输出样式为E:\Java\jdk1.8.0_231\jre\lib\resources.jar;E:\Java\jdk1.8.0_231\jre\lib\rt.jar;E:\Java\jdk1.8.0_231\jre\lib\sunrsasign.jar;E:\Java\jdk1.8.0_231\jre\lib\jsse.jar;E:\Java\jdk1.8.0_231\jre\lib\jce.jar;E:\Java\jdk1.8.0_231\jre\lib\charsets.jar;E:\Java\jdk1.8.0_231\jre\lib\jfr.jar;E:\Java\jdk1.8.0_231\jre\classes
           

④用户自定义加载器

④用户自定义加载器:Java.lang.ClassLoader的子类,用户可以定制类的加载方式。默认加载路径

\$CLASSPATH

  • 为什么要使用自定义类加载器:隔离加载类、修改类加载的方式、拓展加载源、防止源码泄漏
  • 实现步骤:继承抽象类Java.lang.ClassLoader,重写findClass()方法

层次为:根类加载器–>扩展类加载器–>系统应用类加载器–>自定义类加载器

三、各加载器的源码

前面我们稍微提过了,ExtClassLoader和AppClassLoader是Launcher的静态内部类。

【JVM】说说双亲委派源码、SPI、线程上下文类加载器TCCL

什么时候初始化呢?

Launcher

的构造与2个类加载器

下面说明了在执行Launcher()无参构造时就构造了系统类加载器,赋值到Launcher的属性中

赋值系统类加载器的时候,因为它的父加载器是扩展类加载器,所以先实例化了一个扩展类加载器,然后在实例化扩展类加载器的时候传进去

public class Launcher {
    // 自己把自己实例化了
    private static Launcher launcher = new Launcher();
    // 
    private static String bootClassPath =
        System.getProperty("sun.boot.class.path");

    private ClassLoader loader;

    public Launcher() {
        // Create the extension class loader
        ClassLoader extcl;
        extcl = ExtClassLoader.getExtClassLoader();

        // Now create the class loader to use to launch the application
        loader = AppClassLoader.getAppClassLoader(extcl);
        //设置AppClassLoader为线程上下文类加载器,这个文章后面部分讲解
        Thread.currentThread().setContextClassLoader(loader);
    }
           
上面那个

System.getProperty("sun.boot.class.path")

结果为

C:\Program Files\Java\jre1.8.0_91\lib\resources.jar;

C:\Program Files\Java\jre1.8.0_91\lib\rt.jar;

C:\Program Files\Java\jre1.8.0_91\lib\sunrsasign.jar;

C:\Program Files\Java\jre1.8.0_91\lib\jsse.jar;

C:\Program Files\Java\jre1.8.0_91\lib\jce.jar;

C:\Program Files\Java\jre1.8.0_91\lib\charsets.jar;

C:\Program Files\Java\jre1.8.0_91\lib\jfr.jar;

C:\Program Files\Java\jre1.8.0_91\classes

AppClassLoader源码:

/**
     * The class loader used for loading from java.class.path.
     * runs in a restricted security context.
     */
static class AppClassLoader extends URLClassLoader {


    public static ClassLoader getAppClassLoader(final ClassLoader extcl){
        final String s = System.getProperty("java.class.path");
        final File[] path = (s == null) ? new File[0] : getClassPath(s);


        return AccessController.doPrivileged(
            new PrivilegedAction<AppClassLoader>() {
                public AppClassLoader run() {
                    URL[] urls =
                        (s == null) ? new URL[0] : pathToURLs(path);
                    return new AppClassLoader(urls, extcl);
                }
            });
    }

    ......
}
           
/**
用指定的父类加载创建一个新的类加载器
如果有安全管理器,将调用它的SecurityManager#checkCreateClassLoader()方法,它可能引发安全异常
*/
protected ClassLoader(ClassLoader parent) {// 传入父类加载器
    this(checkCreateClassLoader(), parent);
}
protected ClassLoader(){
    // 默认会传入系统类加载器作为父类加载器
    this(checkCreateClassLoader(), getSystemClassLoader());
}
           
【JVM】说说双亲委派源码、SPI、线程上下文类加载器TCCL

四、双亲委派模型

如果自定义了一个java.lang.String类,是不起作用的。为什么呢?

类加载器用来把类加载到java虚拟机中。从JDK1.2版本开始,类的加载过程采用父亲委托机制,这种机制能更好地保证Java平台的安全。在此委托机制中,除了java虚拟机自带的根类加载器以外,其余的类加载器都有且只有一个父加载器。当java程序请求加载器loader1加载A类时,loader1首先委托自己的父加载器去加载A类,若父加载器能加载,则由父加载器完成加载任务,否则才由加载器loader1本身加载A类。

当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类类加载去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。

【JVM】说说双亲委派源码、SPI、线程上下文类加载器TCCL

若有一个类能够成功加载Test类,那么这个类加载器被称为定义类加载器,所有能成功返回Class对象引用的类加载器(包括定义类加载器)称为初始类加载器。

比如一个自定义类,可以由自定义类加载器和系统加载器加载,但是类路径下没有class文件,所以系统加载器加载不到,只有自定义加载器能加载到。此时,系统加载器和自定义加载器都是定义类加载器;而自定义加载器才是初始类加载器。详情可以看Test16

采用双亲委派的一个好处是比如加载位于 rt.jar 包中的类 java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个 Object对象。

双亲委托模型的好处:

  • (1)可以确保Java和核心库的安全:所有的Java应用都会引用java.lang中的类,也就是说在运行期java.lang中的类会被加载到虚拟机中,如果这个加载过程如果是由自己的类加载器所加载,那么很可能就会在JVM中存在多个版本的java.lang中的类,而且这些类是相互不可见的(命名空间的作用)。借助于双亲委托机制,Java核心类库中的类的加载工作都是由启动根加载器去加载,从而确保了Java应用所使用的的都是同一个版本的Java核心类库,他们之间是相互兼容的;
  • (2)确保Java核心类库中的类不会被自定义的类所替代;
  • (3)不同的类加载器可以为相同名称的类(binary name)创建额外的命名空间。相同名称的类可以并存在Java虚拟机中,只需要用不同的类加载器去加载即可。相当于在Java虚拟机内部建立了一个又一个相互隔离的Java类空间。
沙箱安全机制:自定义String类,但是在加载自定义String类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载jdk自带的文件(rt.jar包中java/lang/String.class),报错信息说没有main方法就是因为加载的是rt.jar包中的String类,这样可以保证对java核心源代码的保护,这就是沙箱安全。

命名空间与类的隔离

两个class相同条件:类名相同,且要同一个类加载器加载的。只有两者同时满足的情况下,JVM才认为这两个class是相同的

命名空间:命名空间由该加载器及所有父加载器所加载的类构成。每个类加载器都有自己的命名空间

  • 在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类;
  • 在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类;
  • 同一命名空间内的类是互相可见的,非同一命名空间内的类是不可见的;
  • 子加载器可以见到父加载器加载的类,父加载器也不能见到子加载器加载的类。

同样不同的类加载器加载的类生产的对象也是不能类型转换的

类的隔离
// 这部分的代码借鉴了https://blog.csdn.net/a745233700/article/details/89245847
package classloader;

public class NewworkClassLoaderTest {

    public static void main(String[] args) {
        try {
            //测试加载网络中的class文件
            String rootUrl = "http://localhost:8080/httpweb/classes";
            String className = "org.classloader.simple.NetClassLoaderSimple";
            
            // 不同的类加载器
            NetworkClassLoader ncl1 = new NetworkClassLoader(rootUrl);
            Class<?> clazz1 = ncl1.loadClass(className);
             Object obj1 = clazz1.newInstance();
            // 类加载器2
            NetworkClassLoader ncl2 = new NetworkClassLoader(rootUrl);
            Class<?> clazz2 = ncl2.loadClass(className);
            Object obj2 = clazz2.newInstance();
            
            clazz1.getMethod("setNetClassLoaderSimple", Object.class).invoke(obj1, obj2);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}


package org.classloader.simple;

public class NetClassLoaderSimple {

    private NetClassLoaderSimple instance;

    public void setNetClassLoaderSimple(Object obj) {
        this.instance = (NetClassLoaderSimple)obj;
    }
}
首先获得网络上一个class文件的二进制名称,然后通过自定义的类加载器NetworkClassLoader创建两个实例,并根据网络地址分别加载这份class,并得到这两个ClassLoader实例加载后生成的Class实例clazz1和clazz2,最后将这两个Class实例分别生成具体的实例对象obj1和obj2,再通过反射调用clazz1中的setNetClassLoaderSimple方法。
    
           

自定义加载器

为什么要自定义类加载器:
  • 隔离加载类
  • 修改类加载的方式
  • 扩展加载源
  • 防止源码泄漏
  • 自定义路径下查找自定义的class类文件,也许我们需要的class文件并不总是在已经设置好的Classpath下面,那么我们必须想办法来找到这个类,在这种清理下我们需要自己实现一个ClassLoader。
  • 确保安全性:Java字节码很容易被反编译,对我们自己的要加载的类做特殊处理,如保证通过网络传输的类的安全性,可以将类经过加密后再传输,在加密到JVM之前需要对类的字节码在解密,这个过程就可以在自定义的ClassLoader中实现。
  • 实现类的热部署:可以定义类的实现机制,如果我们可以检查已经加载的class文件是否被修改,如果修改了,可以重新加载这个类。
自定义类加载器方法:
  • 继承抽象类java.lang.ClassLoader
  • 在JDK1.2之前,需要重写loadClass();JDK1.2之后,不需要重写loadClass()了,建议重写

    findClass()

    • findClass()

      的功能是找到class文件并把字节码加载到内存中。自定义的ClassLoader一般覆盖该方法,以便使用不同的加载路径,然后调用defineClass()解析字节码。
    • defineClass()

      方法用来将byte字节流解析成JVM能够识别的Class对象。有了这个方法意味着我们不仅仅可以通过class文件实例化对象,还可以通过其他方式实例化对象,如我们通过网络接收到一个类的字节码,拿这个字节码流直接创建类的Class对象形式实例化对象。
    • 自定义的加载器可以覆盖方法loadClass()以便定义不同的加载机制。
    • 如果自定义的加载器仅覆盖了findClass(),而未覆盖loadClass(即加载规则一样,但加载路径不同);则调用getClass().getClassLoader()返回的仍然是AppClassLoader!因为真正的load类,还是AppClassLoader
  • 如果没有太多复杂的需求,可以直接继承URLCloassLoader类,就可以避免自己编写findClass()及其获取字节码流的方式,使自定义类加载器编写更加简洁。可以看test16

JVM中表示两个class对象是否为同一个类存在两个必要条件:

  • 类的完整类名必须一致,包括包名
  • 加载这个类的ClassLoader(指CLassLoader实例对象)必须相同

    换句话说,在JVM中,即使这两个类对象(class对象)来源同一个class文件,被同一个虚拟机所加载,但只要加载它们的ClassLoader实例对象不同,那么这两个类对象也是不相等的。

应用:

  • 加载自定义格式的class文件

    如果我们从网路上下载一个class文件的字节码,但是为了安全性在传输之前对这个字节码进行了简单的加密处理,然后再通过网络传输。当客户端接收到这个类的字节码后需要经过解密才能还原成原始的类格式,然后再通过ClassLoader的defineClass()方法创建这个类的实例,最后完成类的加载工作。

      比如上面的代码中,在获取到字节码(byte[] classData = getData(name);)之后再通过一个类似以下的代码:

    private byte[] deCode(byte[] src){
        byte[] decode = null;
        //do something niubility! 精密解码过程
        return decode; 
    }
               
hello-world
/**
     * 自定义类加载器
     */
public class MyClassLoader extends ClassLoader {

    private String rootDir;/*自定义类加载的查找class的路径*/

    /*指定该类加载器会查找的rootDir目录,和父加载器*/
    public MyClassLoader(String rootDir, ClassLoader parent){
        // 父类无参构造默认会加载系统类加载器
        super(parent);
        this.rootDir = rootDir;

    }

    /*指定该类加载器会查找的rootDir目录*/
    public MyClassLoader(String rootDir){
        this.rootDir = rootDir;
    }


    /**
         * 自定义自己的类加载器,如没有要改变类加载顺序的必要的话,则重写findClass方法,因为这个方法是JDK预留了给我们实现的,
         * 否则就需要修改loadClass的实现。
         * @param name
         * @return
         * @throws ClassNotFoundException
         */
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        //<1>.根据类的全路径(包含包名)类名和放置的目录确定类文件的路径
        String className = name.substring(name.lastIndexOf(".")+1)+ ".class";
        String classFile = rootDir + File.separator + className;
        FileInputStream fileInputStream = null;
        byte[] classData = null;
        try {
            //<2>.将class文件读取到字节数组
            fileInputStream = new FileInputStream(new File(classFile));
            classData = new byte[fileInputStream.available()];
            fileInputStream.read(classData,0,classData.length);

            //<3>.将字节数据创建一个class
            return defineClass(name,classData,0,classData.length);

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if (fileInputStream != null){
                try {
                    fileInputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        //<4>如果父类加载器不是自定义的,上面的加载过程没加载成功,则此调用会throw ClassNotFoundException
        return super.findClass(name);
    }
}
           
public class CustomClassLoaderTest {

    /*定义了一个目录存放class文件,这个其实可以修改为可配置参数*/
    private static final String rootDir = "D:/class/";

    public static void main(String[] args) throws Exception {

        /*<1> 从指定的目录下查找对应的class文件,进行加载,然后创建该对象,如果加载存在则加载成功,则类加载器应为MyClassLoader*/
        MyClassLoader classLoader = new MyClassLoader(rootDir);

        Class c = classLoader.loadClass("com.learn.classloader.custom.Person");

        Object object = c.newInstance();

        Method getNameMethod = c.getMethod("getName");
        Method getAgeMethod = c.getMethod("getAge");
        System.out.println("name:" + getNameMethod.invoke(object) + ",age:" + getAgeMethod.invoke(object));
        System.out.println("类加载器为:" + object.getClass().getClassLoader());
    }
}
           

因为该Person类,在IDE中编译后放在了classpath,而classpath默认是由ApplicationClassLoader进行加载的,而MyClassLoader的parent为ApplicationClassLoader,所以如果在IDE中执行测试程序,根据双亲委派机制,Person类的类加载器将一直是ApplicationClassLoader,下图是运行的结果:

【JVM】说说双亲委派源码、SPI、线程上下文类加载器TCCL

显然,直接在IDE中执行测试类是没有办法使用我们自定义的类加载器实现类加载的,这一切的原因就是Person类在classpath中,所以解决方法就是将编译后的Person类,放到"D:/class/"目录下,这个是例子中定义的目录,根据具体情况自己指定。如下图所示:

【JVM】说说双亲委派源码、SPI、线程上下文类加载器TCCL

接下来,我们先将项目打包成Jar包,此时Jar中含有编译后的Person类,然后需要将该类从中删除掉,然后再运行Jar程序,如果不删除jar包中的Peson类,则会仍会是ApplicationClassLoader,如下所示:

【JVM】说说双亲委派源码、SPI、线程上下文类加载器TCCL

下面删除jar包中的Person类,那么就会从"D:/class"里进行加载,也就是自定义的类加载器去加载的,如下所示,显然类加载器已经是MyClassLoader了:

【JVM】说说双亲委派源码、SPI、线程上下文类加载器TCCL

实现类的热部署

JVM默认不能热部署类,因为加载类时会去调用findLoadedClass(),如果类已被加载,就不会再次加载。

  JVM判断类是否被加载有两个条件:完整类名是否一样,ClasssLoader是否是同一个

  所以要实现热部署的话,只需要使用ClassLoader的不同实例来加载。

  如果用同一个ClassLoader实例来加载同一个类,则会抛出LinkageError.

  Jsp就是一个热部署的例子。

所谓的热部署就是利用同一个class文件,不同的类加载器在内存创建出两个不同的class对象(关于这点的原因前面已分析过,即利用不同的类加载实例),由于JVM在加载类之前会检测请求的类是否已加载过(即在loadClass()方法中调用findLoadedClass()方法),如果被加载过,则直接从缓存获取,不会重新加载。注意同一个类加载器的实例和同一个class文件只能被加载器一次,多次加载将报错,因此我们实现的热部署必须让同一个class文件可以根据不同的类加载器重复加载,以实现所谓的热部署。实际上前面的实现的FileClassLoader和FileUrlClassLoader已具备这个功能,但前提是直接调用findClass()方法,而不是调用loadClass()方法,因为ClassLoader中loadClass()方法体中调用findLoadedClass()方法进行了检测是否已被加载,因此我们直接调用findClass()方法就可以绕过这个问题,当然也可以重载loadClass方法,但强烈不建议这么干。利用FileClassLoader类测试代码如下:

public static void main(String[] args) throws ClassNotFoundException {
    String rootDir="/Users/zejian/Downloads/Java8_Action/src/main/java/";
    //创建自定义文件类加载器
    FileClassLoader loader = new FileClassLoader(rootDir);
    FileClassLoader loader2 = new FileClassLoader(rootDir);

    try {
        //加载指定的class文件,调用loadClass()
        Class<?> object1=loader.loadClass("com.zejian.classloader.DemoObj");
        Class<?> object2=loader2.loadClass("com.zejian.classloader.DemoObj");

        System.out.println("loadClass->obj1:"+object1.hashCode());
        System.out.println("loadClass->obj2:"+object2.hashCode());

        //加载指定的class文件,直接调用findClass(),绕过检测机制,创建不同class对象。
        Class<?> object3=loader.findClass("com.zejian.classloader.DemoObj");
        Class<?> object4=loader2.findClass("com.zejian.classloader.DemoObj");

        System.out.println("loadClass->obj3:"+object3.hashCode());
        System.out.println("loadClass->obj4:"+object4.hashCode());

        /**
             * 输出结果:
             * loadClass->obj1:644117698
               loadClass->obj2:644117698
               findClass->obj3:723074861
               findClass->obj4:895328852
             */

    } catch (Exception e) {
        e.printStackTrace();
    }
}
           

五、双亲委派源码浅析

Class.forName

通常用法:

Class<A> a = Class.forName("A类的全类名");
           

两个重载方法:

public static Class<?> forName(String className);
public static Class<?> forName(String name, boolean initialize,ClassLoader loader);
           

第二个构造函数指定了父类加载器,这儿可能要有疑问了,第一个方法默认使用哪个类加载器来加载的呢?(答案是得看它在哪个对象中执行的)

我们来看下具体实现:

public static Class<?> forName(String className)
    throws ClassNotFoundException {
    // 使用native方法获取调用类的Class对象
    Class<?> caller = Reflection.getCallerClass();
    // 传入的是调用者的类加载器,比如在A类里执行了forName,那么类加载器就是加载A类的类加载器
    return forName0(className, 
                    true, 
                    ClassLoader.getClassLoader(caller),
                    caller);
    // native方法,看不了了
}

// 获取类的加载器
static ClassLoader getClassLoader(Class<?> caller) {
    if (caller == null) {
        return null;
    }
    return caller.getClassLoader0();
}


@CallerSensitive
public static Class<?> forName(String name, 
                               boolean initialize,
                               ClassLoader loader){
    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);
}
           

好了,我们知道了,要通过Class.forName加载器一个类,你可以自己传入一个类加载器,你不传默认使用的是调用Class.forName处所用的类加载器

上面的逻辑重要的是如何获取一个类的类加载器

ClassLoader#loadClass

步骤:

  • 1.加锁,防止重复加载
  • 2.查找是否已经被加载过,如果已经被加载过了则直接返回
  • 3.判断是否有父类加载器,如果有则交给父类加载器进行加载,直到Bootstrap ClassLoader
  • 4.如果父类加载器都没有加载成功,则会调用子类加载器的findClass,尝试用字类加载器加载.
//java.lang.ClassLoader#loadClass(java.lang.String, boolean)
protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    //<1> 加锁
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        //<2> 查找是否已经被加载过
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                //<3>.判断是否有父类加载器
                if (parent != null) {
                    //<3.1> 有父类加载器,先使用父类加载器进行加载
                    c = parent.loadClass(name, false);
                } else {
                    //<3.2> 如果没父类加载器,则表明类加载器已经是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();
                //<4> 如果该类还没有加载,则会调用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(c);
        }
        return c;
    }
}

           
【JVM】说说双亲委派源码、SPI、线程上下文类加载器TCCL

上面的原理就是为什么推荐重写findClass()而不是loadClass()。重写findClass()不仅简单,而且还能都避免覆盖默认加载器的父类委托、缓存机制等,一举两得。

findClass

方法含义:给定一个全类名字符串,找能加载这个地址class二进制文件的类加载器

返回值:加载好的class对象

接下来我们来查看一下ClassLoader#findClass(String name),会发现该方法是空实现,直接抛出了异常,如下所示的代码,这是什么原因了;其实在签名提到了,所有类加载器,独立于虚拟机外部,并且完全继承自抽象类java.lang.ClassLoader,所以该findClass(String name)方法由具体的的实现类来实现,也就是如果我们自己定义类加载器,在不打破双亲委派机制的前提下,则只需要重写这个findClass(String name)方法即可。

//java.lang.ClassLoader#findClass
protected Class<?> findClass(String name) throws ClassNotFoundException {
    //直接抛出了ClassNotFoundException异常,显然父类没有找到则会抛出该异常,看回上面的loadClass就可以知道,子类是由try-catch该异常,然后再有子类加载的findClass尝试加载
    throw new ClassNotFoundException(name);
}
           

那么接下来就看看系统、扩展类加载器的findClass吧

其实,扩展、系统类加载器都没有复写findClass()方法,他们的实现都在他们的共同父类

URLClassLoader

//java.net.URLClassLoader#findClass
protected Class<?> findClass(final String name){
    final Class<?> result;


    result = AccessController.doPrivileged(
        // 传入lambda函数式接口的实现类
        new PrivilegedExceptionAction<Class<?>>() {
            public Class<?> run() throws ClassNotFoundException {
                //将带包名的全限定的类名,转为文件路径,例如将com.learn.Student转为/com/learn/Student.class
                String path = name.replace('.', '/').concat(".class");
                //加载class资源
                Resource res = ucp.getResource(path, false);
                if (res != null) {
                    //开始解析class
                    return defineClass(name, res);
                } else { return null; }
            }
        }, 
        acc
    );


    //如果加载失败,则会抛出ClassNotFoundException
    if (result == null) { throw new ClassNotFoundException(name); }
    return result;
}
           

ClassLoader

再介绍一下所有类加载器(除了根)的父类ClassLoader

它是一个抽象类,其后所有的类加载器都继承自ClassLoader(不包括启动类加载器)

getParent();
loadClass(String);
findClass();
findLoadedClass();
defineClass();
resolveClass();
           
  • Java虚拟机与程序的生命周期
  • (1)执行了System.exit()方法

    (2)程序正常执行结束

    (3)程序在执行过程中遇到了异常或错误而异常终止

    (4)由于操作系统出现错误而导致虚拟机进程终止

获取类加载器的途径:

// 获取当前类的加载器
(1)clazz.getClassLoader(); 

// 获取当前线程上下文的加载器
(2)Thread.currentThread().getContextClassLoader(); 

// 获取系统的加载器
(3)ClassLoader.getSystemClassLoader(); 

// 获取调用者的加载器
(4)DriverManager.getCallerClassLoader(); 
           

六、双亲委派的打破

双亲委派模型并不是一个强制性的约束,而仅仅只是Java设计者推荐开发者的类加载器实现方式。故如果有需要,则可以破坏双亲委派模型,实现自己的类加载器。

双亲委派模型,解决了各个类加载器的基础类统一的问题,但是如果基础类又要回调用户的代码,也就是基础类的实现类,那要怎么办。为了解决这个问题,Java设计团队,只好引入:线程上下文类加载器

首先说下线程上下文类加载器的获取/设置方式

getContextClassLoader();
setContextClassLoader(ClassLoader cl); 
// 如果没有通过 setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器。而最原始的进程的类加载器是系统类加载器
// 即默认 系统类加载器

@CallerSensitive
public ClassLoader getContextClassLoader() {
    if (contextClassLoader == null)  return null;
    SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
        ClassLoader.checkClassLoaderPermission(contextClassLoader,
                                               Reflection.getCallerClass());
    }
    return contextClassLoader;
}

           

这样,在根加载器加载不了类的时候,就可以拿到根加载器的上下文加载器,比如让系统类加载器帮它加载一个类

双亲委派的打破–线程上下文类加载器

SPI

服务提供者接口(Service Provider Interface,SPI)

在Java应用中存在着很多服务提供者接口(Service Provider Interface,SPI),这些接口允许第三方为它们提供实现,如常见的 SPI 有 JDBC、JNDI等,这些 SPI 的接口属于 Java 核心库,一般存在

rt.jar

包中,由Bootstrap类加载器加载

而 SPI 的第三方实现代码则是作为Java应用所依赖的 jar 包被存放在

classpath

路径下,由于SPI接口中的代码经常需要加载具体的第三方实现类并调用其相关方法,但SPI的核心接口类是由引导类加载器来加载的,而Bootstrap类加载器无法直接加载SPI的实现类,同时由于双亲委派模式的存在,Bootstrap类加载器也无法反向委托AppClassLoader加载器SPI的实现类。在这种情况下,我们就需要一种特殊的类加载器来加载第三方的类库,而线程上下文类加载器就是很好的选择。

由于SPI中的类经常需要调用外部实现类的方法,而classpath路径下的jdbc.jar(包含外部实现类)无法通过Bootstrap类加载器加载,因此只能委派线程上下文类加载器把jdbc.jar中的实现类加载到内存以便SPI相关类使用。显然这种线程上下文类加载器的加载方式破坏了“双亲委派模型”,它在执行过程中抛弃双亲委派加载链模式,使程序可以逆向使用类加载器,当然这也使得Java类加载器变得更加灵活。

线程上下文类加载器
线程上下文类加载器(Thread Context ClassLoader), TCCL。

在JDBC4.0之前,我们开发有连接数据库的时候,通常会用

Class.forName("com.mysql.jdbc.Driver")

这句先加载数据库相关的驱动,然后再进行获取连接等的操作。而JDBC4.0之后不需要用Class.forName(“com.mysql.jdbc.Driver”)来加载驱动,直接获取连接就可以了,现在这种方式就是使用了Java的「SPI」扩展机制来实现。

// 加载Class到AppClassLoader(系统类加载器),然后注册驱动类
// Class.forName("com.mysql.jdbc.Driver").newInstance(); 
String url = "jdbc:mysql://localhost:3306/testdb";    
// 通过java库获取数据库连接
Connection conn = java.sql.DriverManager.getConnection(url, "name", "password"); 
           

为什么本来是系统类加载器帮着加载

Class.forName("com.mysql.jdbc.Driver")

,但却不用了呢?肯定是JVM帮我们的执行了类似于这句的语句

首先明确一下这条语句的目的:实例化一个Driver的class对象

那为什么这么做呢?我们也就多写一句java语句,原因在于,它想达到一个效果:只要你引入了我的包,那就自动加载我的那个实现类,不用你自己找这个实现类的全类名了

首先说明下是如何做到的:在META-INF/services/下新建文件,文件名为接口名,文件内容为实现类全类名

那么谁帮我们完成创建class对象到类装载器中呢?

其实就是

DriverManager.getConnection()

这个语句,在连接前实例化class对象

代码如下,但是要说明的是,谁去加载这些实现类呢?答案是 TCCL(当前线程的类加载器)

//DriverManager是Java核心包rt.jar的类
public class DriverManager {

    // 静态代码块
    static {
        loadInitialDrivers();//执行该方法
    }

    //loadInitialDrivers方法
    private static void loadInitialDrivers() {
        String drivers;

        // 先读取系统属性
        drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
            public String run() {
                return System.getProperty("jdbc.drivers");
            }
        });

        // 通过SPI加载驱动类,即加载Driver的实现类,生成class对象,然后再生成实例对象
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                // 获取该接口的加载器 // ServiceLoader是什么?它实现了Iterable接口,你可以认为它是个list即可
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
                while(driversIterator.hasNext()) {
                    driversIterator.next();
                }

                return null;
            }
        });
        // 继续加载系统属性中的驱动类
        String[] driversList = drivers.split(":");
        for (String aDriver : driversList) {
            // 使用AppClassloader加载
            Class.forName(aDriver, true,
                          ClassLoader.getSystemClassLoader());

        }
    }
           
private S nextService() {
    
    String cn = nextName;
    nextName = null;
    Class<?> c = null;

    // 这里传入cl,去获得Driver实现类的class对象
    c = Class.forName(cn, false, loader);

    S p = service.cast(c.newInstance());
    providers.put(cn, p);
    return p;

}
           

注意上面有个loader,从哪里来的?这里不啰嗦了,它是ServiceLoader实例的属性,那么是谁给它赋值的呢?

public static <S> ServiceLoader<S> load(Class<S> service) {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

public static <S> ServiceLoader<S> load(Class<S> service,
                                        ClassLoader loader){
    return new ServiceLoader<>(service, loader);
}
           

从上面代码中我们知道,传入的是当前线程的 线程上下文类加载器

前面说了,默认的上下文类加载器是【系统类加载器】,那么就用系统类加载器去加载Driver的实现类

总结

再啰嗦一下:

我们从DriverManager来,DriverManager是根加载器能加载的,但是它调用Class.forName的时候,前面说了,如果不指定classLoader,那么使用的就是执行Class.forName语句所在类的类加载器。

显然它是从

DriverManager

–>

ServiceLoader

类下来的,这两个类都在

rt.jar

包下,都是用根加载器加载的,而根加载器加载不了Driver的实现类,因为它不再rt.jar等包下,所以Class.forName时需要自己传入cl,那么传入哪个cl呢?就是从上下文类加载器中拿到的。

好了,我们加载好了Drvier的实现类了,得到对于的class对象了,看下Driver实现类干嘛了

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver()  {
    }

    static {
        // Driver已经加载到TCCL中了,此时可以直接实例化
        DriverManager.registerDriver(new Driver());
    }
}
           

就往驱动管理器DriverManager中注册了该驱动实例

Driver带不带cj的问题

我们自己写demo

Class.forName("com.mysql.jdbc.Driver")

的时候经常会碰到这个相关的报错,其实是不是报错,它就是提醒你,现在Driver实现类已经升级了,即使你自己Class.forName加载Driver的时候,也是加载的com.mysql.cj.jdbc.Driver

而com.mysql.jdbc.Driver继承类如下:

public class Driver extends com.mysql.cj.jdbc.Driver {
    public Driver() throws SQLException {
        super();
    }

    static {
        System.err.println("Loading class `com.mysql.jdbc.Driver'. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver'. "
                           + "The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.");
    }
}
           

七、回到getConnection

到此驱动注册基本完成,接下来我们回到最开始的那段样例代码:java.sql.DriverManager.getConnection()。它最终调用了以下方法:

private static Connection getConnection(
    String url, java.util.Properties info, Class<?> caller)  {
    /* 传入的caller由Reflection.getCallerClass()得到,该方法
      * 可获取到调用本方法的Class类,这儿获取到的是当前应用的类加载器
      */
    ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
    synchronized(DriverManager.class) {
        if (callerCL == null) {
            callerCL = Thread.currentThread().getContextClassLoader();
        }
    }


    SQLException reason = null;
    // 遍历注册到registeredDrivers里的Driver类
    for(DriverInfo aDriver : registeredDrivers) {
        // 检查Driver类有效性
        if(isDriverAllowed(aDriver.driver, callerCL)) {

            // 调用com.mysql.jdbc.Driver.connect方法获取连接
            Connection con = aDriver.driver.connect(url, info);
            if (con != null) {
                // Success!
                return (con);
            }
        } else {
            println("    skipping: " + aDriver.getClass().getName());
        }
    }
}
           

八、Tomcat与spring的类加载器案例

其实Tomcat、Spring、SpringBoot、SpringMVC都有类似于SPI的机制

你不用每个都去找了,我们猜猜它们的原理:

执行到某一句的时候,因为要创建一个类实例或者执行一个类静态方法,于是要求提前准备一些所需要的基础类实例,但是还是此时只有接口,额实现类因为每个jar包产商不一样,所以实现类不一样,怎么找实现类呢?还是去指定路径下找文件,得到实现类全类名,然后用Class.forName加载,这个过程中可能会涉及TCCL

然后说明一下Tomcat其实自己定义了几个自己的类加载器,用于加载不同路径下的class文件,比如下面几个自定义类加载器

【JVM】说说双亲委派源码、SPI、线程上下文类加载器TCCL
  • 加载java类库的
    • CommonClassLoader

      :加载放置在

      /common/*

      目录中:类库可被Tomcat和所有的Web应用程序共同使用。
    • CatalinaClassLoader

      :加载放置在

      /server/*

      目录中:类库可被Tomcat使用,但对所有的Web应用程序都不可见。
    • SharedClassLoader

      :加载放置在

      /shared/*

      目录中:类库可被所有的Web应用程序共同使用,但对Tomcat自己不可见。
  • 加载Web应用程序
    • WebAppClassLoader

      :加载放置在

      /WebApp/WEB-INF/*

      目录中:类库仅仅可以被此Web应用程序使用,对Tomcat和其他Web应用程序都不可见。其中 WebApp 类加载器和 Jsp 类加载器通常会存在多个实例,每一个 Web 应用程序对应一个 WebApp 类加载器,每一个 JSP 文件对应一个 Jsp 类加载器。

从图中的委派关系中可以看出,

CommonClassLoader

能加载的类都可以被

CatalinaClassLoader

SharedClassLoader

使用,而 CatalinaClassLoader 和 SharedClassLoader 自己能加载的类则与对方相互隔离。WebAppClassLoader 可以使用 SharedClassLoader 加载到的类,但各个 WebAppClassLoader 实例之间相互隔离。而 JasperLoader 的加载范围仅仅是这个 JSP 文件所编译出来的那一个 Class,它出现的目的就是为了被丢弃:当服务器检测到 JSP 文件被修改时,会替换掉目前的 JasperLoader 的实例,并通过再建立一个新的 Jsp 类加载器来实现 JSP 文件的 HotSwap 功能。

Spring加载问题

Tomcat 加载器的实现清晰易懂,并且采用了官方推荐的“正统”的使用类加载器的方式。这时作者提一个问题:如果有 10 个 Web 应用程序都用到了spring的话,可以把Spring的jar包放到

common

shared

目录下让这些程序共享。Spring 的作用是管理每个web应用程序的bean,getBean时自然要能访问到应用程序的类,而用户的程序显然是放在

/WebApp/WEB-INF

目录中的(由

WebAppClassLoader

加载),那么在

CommonClassLoader

SharedClassLoader

中的 Spring 容器如何去加载并不在其加载范围的用户程序(/WebApp/WEB-INF/)中的Class呢?

解答:

spring根本不会去管自己被放在哪里,它统统使用TCCL来加载类,而TCCL默认设置为了WebAppClassLoader,也就是说哪个WebApp应用调用了spring,spring就去取该应用自己的WebAppClassLoader来加载bean,简直完美~
源码分析

有兴趣的可以接着看看具体实现。在web.xml中定义的listener为org.springframework.web.context.ContextLoaderListener,它最终调用了org.springframework.web.context.ContextLoader类来装载bean,具体方法如下(删去了部分不相关内容):

public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {

    // 创建WebApplicationContext
    if (this.context == null) {
        this.context = createWebApplicationContext(servletContext);
    }
    // 将其保存到该webapp的servletContext中		
    servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
    // 获取线程上下文类加载器,默认为WebAppClassLoader
    ClassLoader ccl = Thread.currentThread().getContextClassLoader();
    // 如果spring的jar包放在每个webapp自己的目录中
    // 此时线程上下文类加载器会与本类的类加载器(加载spring的)相同,都是WebAppClassLoader
    if (ccl == ContextLoader.class.getClassLoader()) {
        currentContext = this.context;
    }
    else if (ccl != null) {
        // 如果不同,也就是上面说的那个问题的情况,那么用一个map把刚才创建的WebApplicationContext及对应的WebAppClassLoader存下来
        // 一个webapp对应一个记录,后续调用时直接根据WebAppClassLoader来取出
        currentContextPerThread.put(ccl, this.context);
    }

    return this.context;

}
           

具体说明都在注释中,spring考虑到了自己可能被放到其他位置,所以直接用TCCL来解决所有可能面临的情况。

总结

通过上面的两个案例分析,我们可以总结出线程上下文类加载器的适用场景:

  • 当高层提供了统一接口让低层去实现,同时又要是在高层加载(或实例化)低层的类时,必须通过线程上下文类加载器来帮助高层的ClassLoader找到并加载该类。
  • 当使用本类托管类加载,然而加载本类的ClassLoader未知时,为了隔离不同的调用者,可以取调用者各自的线程上下文类加载器代为托管。

九、链接

类被加载后,就进入连接阶段。连接阶段就是将已经读入到内存的类的二进制数据合并到虚拟机的运行时环境中去。

  • 类的连接-验证

    1)类文件的结构检查

    2)语义检查

    3)字节码验证

    4)二进制兼容性的验证

  • 类的连接-准备

    在准备阶段,java虚拟机为类的静态变量分配内存,并设置默认的初始值(不是真正的值)。例如对于以下Sample类,在准备阶段,将为int类型的静态变量a分配4个字节的内存空间,并且赋予默认值0,为long类型的静态变量b分配8个字节的内存空间,并且赋予默认值0;

public class Sample{
    private static int a=1;
    public  static long b;
    public  static long c;
    static { b=2; }
}
           

十、始化

首先明确是类的初始化而不是类实例的初始化。

首先idea安装jclasslib插件

验证、准备、解析

初始化阶段就是执行构造器方法

<clinit>

,他不是我们普通的构造器。而是类的初始化而且他是自动生成的,编译器收集类中所以类静态变量的赋值动作和静态代码块中的语句合并而来。没有静态变量和静态代码块就不生成clinit方法了。

构造器方法中指令按语句在源文件中出现的顺序执行。

JVM 会确保子类的clinit方法在父类 clinit已经执行结束。

虚拟机必须保证一个类的clinit方法在多线程下被同步加锁。

在链接阶段已经定义好了,在这个阶段只是覆盖。但是不能在定义前调用它,这是编译器决定的。

而init是我们真正的构造器。

在初始化阶段,Java虚拟机执行类的初始化语句,为类的静态变量赋予初始值。在程序中,静态变量的初始化有两种途径:

  • (1)在静态变量的声明处进行初始化;
  • (2)在静态代码块中进行初始化。

类的初始化步骤:

  • (1)假如这个类还没有被加载和连接,那就先进行加载和连接
  • (2)假如类存在直接父类,并且这个父类还没有被初始化,那就先初始化直接父类
    • 当java虚拟机初始化一个类时,要求它的所有父类都已经被初始化,但是这条规则不适用于接口。因此,一个父接口并不会因为它的子接口或者实现类的初始化而初始化。只有当程序首次使用特定的接口的静态变量时,才会导致该接口的初始化。
  • (3)假如类中存在初始化语句,那就依次执行这些初始化语句

Java程序对类的使用方式可分为两种

  • (1)主动使用
  • (2)被动使用

所有的Java虚拟机实现必须在每个类或接口被Java程序“首次主动使用”时才能初始化他们

  • 主动使用(七种)
    • (1)new创建类的实例
    • (2)访问某个类或接口的静态变量( getstatic(助记符)),或者对该静态变量赋值 putstatic
    • (3)调用类的静态方法 invokestatic
    • (4)反射

      Class.forName("com.Test")

    • (5)初始化一个类的子类
    • (6)Java虚拟机启动时被标明启动类的类
    • (7)JDK1.7开始提供的动态语言支持(了解)
  • 被动使用

    除了上面七种情况外,其他使用java类的方式都被看做是对类的被动使用,都不会导致类的初始化。如

    • 当访问一个静态域时,只有真正声明这个域的类才会被初始化。如:通过子类引用父类的静态变量,不会导致子类初始化
    • 通过数字定义类引用,不会触发此类的初始化
    • 引用常量不会触发此类的初始化(常量在链接阶段就存入调用类的常量池中了)
    • 调用ClassLoader类的loadClass方法加载一个类,并不是对类的主动使用,不会导致类的初始化。

执行顺序

  • 静态代码块: 静态代码块在类被加载的时候就运行了,而且只运行一次,并且优先于各种代码块以及构造函数。如果一个类中有多个静态代码块,会按照书写顺序依次执行。
    • 一般情况下,如果有些代码需要在项目启动的时候就执行,这时候就需要静态代码块。比如一个项目启动需要加载的很多配置文件等资源,我们就可以都放入静态代码块中。
    • 在类加载的时候,静态方法也已经加载了,但是我们必须要通过类名或者对象名才能访问,也就是说相比于静态代码块,静态代码块是主动运行的,而静态方法是被动运行的。
    • 静态变量要放在静态代码块前
  • 先初始化父类再初始化子类。先调用父类的构造函数再调用子类的构造函数
  • 构造代码块:在java类中使用{}声明的代码块(和静态代码块的区别是少了static关键字)。**构造代码块在创建对象时被调用,每次创建对象都会调用一次,但是优先于构造函数执行。**构造代码块依托于构造函数,也就是说,如果你不实例化对象,构造代码块是不会执行的

1、父类的静态变量和静态块赋值(按照声明顺序)

  2、自身的静态变量和静态块赋值(按照声明顺序)

  3、main方法

  3、父类的成员变量和块赋值(按照声明顺序)

  4、父类构造器赋值

  5、自身成员变量和块赋值(按照声明顺序)

  6、自身构造器赋值

  7、静态方法,实例方法只有在调用的时候才会去执行