类从被加载到内存中开始,直到被从内存中卸载为止,它的整个生命周期包括:验证、准备、解析、初始化、使用和卸载7 个阶段。
其中验证、准备、解析 3 个部分统称为连接(Linking)
类加载过程的第一步,主要完成下面 3 件事情:
加载阶段(准确地说,是加载阶段中获取类的二进制字节流的动作)可以使用系统提供的类加载器(ClassLoader)来完成,也可以由用户自定义的类加载器完成,开发人员可以通过定义自己的类加载器去控制字节流的获取方式。
加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,方法区中的数据存储格式由虚拟机实现自行定义,虚拟机并未规定此区域的具体数据结构。然后在java堆中实例化一个java.lang.Class类的对象,这个对象作为程序访问方法区中的这些类型数据的外部接口。
验证是链接阶段的第一步,这一步主要的目的是确保class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身安全。
验证阶段主要包括四个检验过程:文件格式验证、元数据验证、字节码验证和符号引用验证。
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。这个阶段中有两个容易产生混淆的知识点,首先是这时候进行内存分配的仅包括类变量(static 修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在java堆中。
解析阶段是虚拟机常量池内的符号引用替换为直接引用的过程。
类的初始化阶段是类加载过程的最后一步,在准备阶段,类变量已赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员通过程序制定的主观计划去初始化类变量和其他资源,或者可以从另外一个角度来表达:初始化阶段是执行类构造器()方法的过程。
类加载器主要分为四类:
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是相同的。
在实际的应用中双亲委派解决了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来加载类。
为解决上述问题,引入了线程上下文类加载器(Thread Context ClassLoader),线程上下文类加载器可以通过java.lang.Thread 类的setContextClassLoader方法进行设置。默认情况下为系统类加载器(App ClassLoader)。
通过线程上下文类加载器,父类即可打破双亲委派模型,委托子类加载器实现类的加载。当父类无法加载某个类时,就可以委托线程上下文类加载器加载对应的类。
自定义加载器,需要继承 ClassLoader 。如果不想打破双亲委派模型,就重写 ClassLoader 类中的 findClass() 方法,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写 loadClass() 方法。
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()根据接口名去那个目录找到文件,文件解析得到实现类的全限定名,然后循环加载实现类和创建其实例。
Java SPI 无法按需加载实现类:Java SPI 在查找扩展实现类的时候遍历 SPI 的配置文件并且将实现类全部实例化,假设一个实现类初始化过程比较消耗资源且耗时,但是你的代码里面又用不上它,这就产生了资源的浪费。
推荐阅读: