一文理类加载相关知识:类加载器、双亲委派、SPI

思维导图

一文理类加载相关知识:类加载器、双亲委派、SPI_第1张图片

类加载的时机

一文理类加载相关知识:类加载器、双亲委派、SPI_第2张图片

类加载的流程

类从被加载到内存中开始,直到被从内存中卸载为止,它的整个生命周期包括:验证、准备、解析、初始化、使用和卸载7 个阶段。
其中验证、准备、解析 3 个部分统称为连接(Linking)
一文理类加载相关知识:类加载器、双亲委派、SPI_第3张图片

1.加载(重点)

类加载过程的第一步,主要完成下面 3 件事情:

  1. 通过全类名获取定义此类的二进制字节流
  2. 将字节流所代表的静态存储结构转换为方法区的运行时数据结构
  3. 在内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口

加载阶段(准确地说,是加载阶段中获取类的二进制字节流的动作)可以使用系统提供的类加载器(ClassLoader)来完成,也可以由用户自定义的类加载器完成,开发人员可以通过定义自己的类加载器去控制字节流的获取方式。

加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,方法区中的数据存储格式由虚拟机实现自行定义,虚拟机并未规定此区域的具体数据结构。然后在java堆中实例化一个java.lang.Class类的对象,这个对象作为程序访问方法区中的这些类型数据的外部接口。

2.验证

验证是链接阶段的第一步,这一步主要的目的是确保class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身安全。
验证阶段主要包括四个检验过程:文件格式验证、元数据验证、字节码验证和符号引用验证。

3.准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。这个阶段中有两个容易产生混淆的知识点,首先是这时候进行内存分配的仅包括类变量(static 修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在java堆中

4.解析

解析阶段是虚拟机常量池内的符号引用替换为直接引用的过程。

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

5.初始化

类的初始化阶段是类加载过程的最后一步,在准备阶段,类变量已赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员通过程序制定的主观计划去初始化类变量和其他资源,或者可以从另外一个角度来表达:初始化阶段是执行类构造器()方法的过程。

类加载器

类加载器主要分为四类:

BootStrap ClassLoader:启动类加载器,C++实现的,是Java类加载层次中最顶层的类加载器(JVM启动后初始化的),负责加载JDK中的核心类库,如:rt.jar、resources.jar、charsets.jar等;

ExtensionClassLoader:扩展类加载器,负责加载Java的扩展类库,默认加载JAVA_HOME/jre/lib/ext/目下的所有jar。该加载器是有java实现的,由Bootstrploader加载ExtClassLoader,并且将ExtClassLoader的父加载器设置为Bootstrp loader;

AppClassLoader:系统类加载器,负责加载应用程序classpath目录下的所有jar和class文件。

CustomLoader:自定义类加载器,负责加载指定的目录和文件

双亲委派

类加载器在加载类时,会先委托父类加载器去加载该类,如果父类加载器无法加载才会尝试自己加载。

当一个ClassLoader实例需要加载某个类时,它会先检查父类加载器(一直检查到Bootstrap ClassLoader)是否已经加载过该类,如果父类加载器已经加载该类则直接返回该类对象。然后由上至下依次加载类,首先由最顶层的类加载器BootstrapClassLoader试图加载,如果没加载到,则把任务转交给Extension ClassLoader试图加载,如果也没加载到,则转交给AppClassLoader进行加载,如果它也没有加载得到的话,则返回给委托的发起者,由它到指定的文件系统或网络等URL中加载该类。如果它们都没有加载到这个类时,则抛出ClassNotFoundException异常。

JVM在判定两个Class是否相同时,不仅要判断两个类名是否相同,而且要判断是否由同一个类加载器实例加载的。只有两者同时满足的情况下,JVM才认为这两个class是相同的。

双亲委派的优点

  1. 可以避免重复加载,当父亲已经加载了该类的时候,就没有必要让子ClassLoader再加载一次。
  2. 避免Java核心类不被随意替换

打破双亲委派

在实际的应用中双亲委派解决了java 基础类统一加载的问题,但是也存在着问题。jdk中的基础类作为用户api被调用,但是也存在调用用户的代码的情况,典型的如SPI。

Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。
这些 SPI 的接口由 Java 核心库来提供,而这些 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进类路径(CLASSPATH)里。SPI接口中的代码经常需要加载具体的实现类。

那么问题来了,SPI的接口是Java核心库的一部分,是由启动类加载器(Bootstrap Classloader)加载的,而SPI的实现类是由系统类加载器(App ClassLoader)来加载的。启动类加载器是无法找到 SPI 的实现类的,因为依照双亲委派模型,BootstrapClassloader无法委派AppClassLoader来加载类

(1)线程上下文类加载器

为解决上述问题,引入了线程上下文类加载器(Thread Context ClassLoader),线程上下文类加载器可以通过java.lang.Thread 类的setContextClassLoader方法进行设置。默认情况下为系统类加载器(App ClassLoader)
通过线程上下文类加载器,父类即可打破双亲委派模型,委托子类加载器实现类的加载。当父类无法加载某个类时,就可以委托线程上下文类加载器加载对应的类。

(2)自定义类加载器覆写loadClass()

自定义加载器,需要继承 ClassLoader 。如果不想打破双亲委派模型,就重写 ClassLoader 类中的 findClass() 方法,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写 loadClass() 方法。

SPI

SPI(服务提供接口) ,全称为 Service Provider Interface,可以理解为调用方来制定接口规范,提供给外部来实现,调用方在调用时则选择自己需要的外部实现。SPI接口一般在核心库里,由BootStrap ClassLoader加载。

SPI是一种服务发现机制。SPI约定在 Classpath 下的 META-INF/services/ 目录里创建一个以服务接口命名的文件(如:java.sql.Driver),然后文件里面记录的是此 jar 包提供的接口具体实现类的全限定名(如:Mysql中提供的 com.mysql.cj.jdbc.Driver)。
在加载接口的实现类时,通过在查找ClassPath路径下的META-INF/services文件夹中存有实现类类名的文件,并实例化文件所定义的实现类,来实例化某个接口。

SPI 通过 ServiceLoader.load() 去完成上述的实例化META-INF/services中的类。ServiceLoader.load() 会通过 线程上下文类加载器(默认为App Loader)打破双亲委派,委子类类加载器去加载实现类。

SPI的主要流程:约定一个目录,调用ServiceLoader.load()根据接口名去那个目录找到文件,文件解析得到实现类的全限定名,然后循环加载实现类和创建其实例。

图片来源
一文理类加载相关知识:类加载器、双亲委派、SPI_第4张图片

Java SPI的缺点

Java SPI 无法按需加载实现类:Java SPI 在查找扩展实现类的时候遍历 SPI 的配置文件并且将实现类全部实例化,假设一个实现类初始化过程比较消耗资源且耗时,但是你的代码里面又用不上它,这就产生了资源的浪费。

推荐阅读:

  • 推荐:三歪问我Dubbo的SPI机制是啥?(带有ServiceLoader的源码分析)
  • Java SPI详解
  • 推荐:深入理解SPI机制

总结

类的加载过程基本如下图:
一文理类加载相关知识:类加载器、双亲委派、SPI_第5张图片

  1. 大部分类都依赖双亲委派模型进行加载;
  2. 以下情况会破坏双亲委派模型:
    (1)自定义类加载器覆写了loadClass()方法
    (2)父类加载器需要使用由子类加载器加载的类,此时父类加载器会使用线程上下文加载器,去委托子类加载器去加载相应的类
  3. 线程上下文类加载器的适用场景:
    (1)当高层提供了统一接口让低层去实现,同时又要是在高层加载(或实例化)低层的类时,必须通过线程上下文类加载器来帮助高层的ClassLoader找到并加载该类。
    (2)当使用本类托管类加载,然而加载本类的ClassLoader未知时,为了隔离不同的调用者,可以取调用者各自的线程上下文类加载器代为托管。

参考

  • 推荐:【JVM】浅谈双亲委派和破坏双亲委派
  • 推荐:详细jvm-类加载机制
  • 推荐:Java 类加载器
  • 推荐:深入理解SPI机制
  • 真正理解线程上下文类加载器(多案例分析)
  • 自定义类加载器:从网上加载class到内存、实例化调用其中的方法
  • jvm(1)类的加载(三)(线程上下文加载器)
  • 类加载过程

你可能感兴趣的:(JVM,jvm,类加载器,类加载)