Java:类加载机制与与spi的联系


前言

本文记录笔者关于Java中的类加载机制的相关理解

一、类加载是什么,在哪个阶段发生,及能起到什么作用

在这一部分,我们对类加载机制的前世今生作一个简单的总结,也就是对这一部分标题中的问题作相关回答

  • Java代码经过编译后形成的.class文件,需要加载到虚拟机后才能运行。我们将.class文件加载到虚拟机的过程称之为类加载机制。一个类经过类加载机制加载到内存,然后从内存中卸载是这个类的生命周期。整个类的生命周期包含了七个阶段。
  • 根据上一条所提到的,类加载机制需要加载的是Java编译后形成的.class文件,所以类加载机制发生在编译后
  • 类加载机制能够降二进制字节流的.class文件加载到内存中,供JVM虚拟机进行调用,如生成Class文件供反射机制进行调用

简而言之,类加载就是

JVM类加载机制 接收的是: .class文件的二进制字节流,产出的是: java.lang.Class对象。

二、类加载机制

1.类加载实现-类加载器

类加载机制主要依赖类加载器和双亲委派机制进行实现,概括如下

  • Java 中将类的加载工具抽象为「类加载器(classloader)」。
  • 「双亲委派」机制是 Java 中通过加载工具(classloader)加载类文件的一种具体方式。
  • JVM 中类加载器默认使用双亲委派原则,但双亲委派模型并不是一个强制性约束。如 Java 的 「SPI 机制」、Tomcat、日志门面等场景中,均打破了双亲委派模型。

关于双亲委派,本文中的后续部分会进行讲解

类加载器

通过一个类全限定名称来获取其二进制文件(.class)流的工具,被称为类加载器(classloader)。

Java支持的四种类加载器
具体如图所示
在这里插入图片描述
如图,Java的类加载器可被分为四类

1.启动类加载器Bootstrap ClassLoader

  • 用于加载Java的核心类
  • 它不是一个 Java 类,是由底层的 C++ 实现。因此,启动类加载器不属于 Java 类库,无法被 Java 程序直接引用。Bootstrap ClassLoader 的 parent 属性为 null

2.标准扩展类加载器 Extension ClassLoader

  • 由 sun.misc.Launcher$ExtClassLoader 实现
  • 负责加载 JAVA_HOME 下 libext 目录下的或者被 java.ext.dirs 系统变量所指定的路径中的所有类库

3.应用类加载器 Application ClassLoader

  • 由 sun.misc.Launcher$AppClassLoader 实现
  • 负责在 JVM 启动时加载用户类路径上的指定类库

4.用户自定义类加载器 User ClassLoader

  • 当上述 3 种类加载器不能满足开发需求时,用户可以自定义加载器
  • 自定义类加载器时,需要继承 java.lang.ClassLoader 类。如果不想打破双亲委派模型,那么只需要重写 findClass 方法即可;如果想打破双亲委派模型,则需要重写 loadClass 方法

2.生命周期和加载过程

类的生命周期可以划分为 7 个阶段

1.加载
2.验证
3.准备
4.解析
5.初始化
6.使用
7.卸载

在这里插入图片描述

其中1-5的阶段,统一被成为类加载

1.加载

加载阶段是类加载过程的第一个阶段。在这个阶段,JVM 的主要目的是将字节码从各个位置(网络、磁盘等)转化为二进制字节流加载到内存中,接着会为这个类在 JVM 的方法区创建一个对应的 Class 对象,这个 Class 对象就是这个类各种数据的访问入口。
该过程可以总结为**「JVM 加载 Class 字节码文件到内存中,并在方法区创建对应的 Class 对象」**。

2.验证

当 JVM 加载完 Class 字节码文件,并在方法区创建对应的 Class 对象之后,JVM 便会启动对该字节码流的校验,只有符合 JVM 字节码规范的文件才能被 JVM 正确执行。
这个校验过程,大致可以分为下面几个类型

  • JVM 规范校验
    JVM 会对字节流进行文件格式校验,判断其是否符合 JVM 规范,是否能被当前版本的虚拟机处理。
    例如,校验文件是否是以 0x cafe babe 开头,主次版本号是否在当前虚拟机处理范围之内等。

  • 代码逻辑校验
    JVM 会对代码组成的数据流和控制流进行校验,确保 JVM 运行该字节码文件后不会出现致命错误。
    例如,一个方法要求传入 int 类型的参数,但是使用它的时候却传入了一个 String 类型的参数。

3.准备

准备阶段中,JVM 将为类变量分配内存并初始化。

准备阶段,有两个关键点需要注意
1.内存分配的对象
2.初始化的类型

这里需要注意的点在于两点
1.内存分配的对象是类变量而不是类成员变量
2.这里的变量初始化只针对类变量,且初始化指的是为变量赋予 Java 语言中该数据类型的零值,而不是用户代码里初始化的值。

例:

public static int factor = 3;
public String website = "www.google.com";

如上代码,在准备阶段,只会为 factor 变量分配内存,而不会为 website 变量分配内存,同时为factor变量所赋的值为Java变量类型的默认初始值0,而不是程序所希望为他赋的值3

4.解析

解析过程中,JVM 针对「类或接口」、「字段」、「类方法」、「接口方法」、「方法类型」、「方法句柄」、「调用点限定符」这 7 类引用进行解析。解析过程的主要任务是将其在常量池中的符号引用,替换成其在内存中的直接引用

5.初始化

到了初始化阶段,用户定义的 Java 程序代码才真正开始执行。在这个阶段,JVM 会根据语句执行顺序对类对象进行初始化。

  • 一般来说,当 JVM 遇到下面 5 种情况的时候会触发初始化
    遇到 new、getstatic、putstatic、invokestatic 这 4 条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。

    生成这 4 条指令的最常见的 Java 代码场景是使用 new 关键字实例化对象的时候、读取或设置一个类的静态字段(被 final 修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。

  • 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。

  • 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。

  • 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类。

  • 当使用 JDK 1.7 动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果是 REF_getstatic、REF_putstatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行初始化时,则需要先出触发其初始化。

6.使用

当 JVM 完成初始化阶段之后,JVM 便开始从入口方法开始执行用户的程序代码。

7.卸载

当用户程序代码执行完毕后,JVM 便开始销毁创建的 Class 对象,最后负责运行的 JVM 也退出内存。

三、Java类加载机制的特点

类加载机制的3个特点

双亲委派

1.JVM 中,类加载器默认使用双亲委派原则

负责依赖

2.如果一个加载器在加载某个类的时候,发现这个类依赖于另外几个类或接口,也会去尝试加载这些依赖项。

缓存加载

3.为了提升加载效率,消除重复加载,一旦某个类被一个类加载器加载,那么它会缓存这个加载结果,不会重复加载。

双亲委派

JVM 中,类加载器默认使用双亲委派原则。

双亲委派机制是一种任务委派模式,是 Java 中通过加载工具(classloader)加载类文件的一种具体方式。 具体表现为

  • 如果一个类加载器收到了类加载请求,它并不会自己先加载,而是把这个请求委托给父类的加载器去执行。
  • 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的引导类加载器 BootstrapClassLoader
  • 如果父类加载器可以完成类加载任务,就成功返回;倘若父类加载器无法完成加载任务,子加载器才会尝试自己去加载。
  • 父类加载器一层一层往下分配任务,如果子类加载器能加载,则加载此类;如果将加载任务分配至系统类加载器(AppClassLoader)也无法加载此类,则抛出异常。
为什么JVM其实是将类加载机制向上委托,却被翻译成双亲委派机制
  • 文档翻译错误,Oracle官方解释文档中parent属性,会翻译成了双亲
  • classLoader类中,存在parent指针,设置双亲属性,具体见下

ExtClassLoader.parent=null;

AppClassLoader.parent=ExtClassLoader

//自定义
XxxClassLoader.parent=AppClassLoader

需要注意的是,启动类加载器(BootstrapClassLoader)不是一个 Java 类,它是由底层的 C++ 实现,因此启动类加载器不属于 Java 类库,无法被 Java 程序直接引用,所以 ExtClassLoader.parent=null;。

委派

设置双亲之后,便可以进行委派。委派过程也就是类加载的过程

ClassLoader中里面有三个重要方法,具体如下:
1.loadClass()
2.findClass()
3.defineClass()

实现双亲委派的代码都集中在 java.lang.ClassLoader 的 loadClass() 方法中。

public abstract class ClassLoader {
    // 委派的父类加载器
    private final ClassLoader parent;

    public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }


    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        // 保证该类只加载一次
        synchronized (getClassLoadingLock(name)) {
            // 首先,检查该类是否被加载
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    if (parent != null) {
                    	//父类加载器不为空,则用该父类加载器
                        c = parent.loadClass(name, false);
                    } else {
                    	//若父类加载器为空,则使用启动类加载器作为父类加载器
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    //若父类加载器抛出ClassNotFoundException ,
                    //则说明父类加载器无法完成加载请求
                }

                if (c == null) {
                    //父类加载器无法完成加载请求时
                    //调用自身的findClass()方法进行类加载
                    c = findClass(name);
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }
}

上述代码的相关检测流程如下:

  • 先检查类是否已经被加载过
  • 若没有加载,则调用父加载器的 loadClass() 方法进行加载
  • 若父加载器为空,则默认使用启动类加载器作为父加载器
  • 如果父类加载失败,抛出 ClassNotFoundException 异常后,再调用自己的 findClass() 方法进行加载
loadClass、findClass、defineClass 方法的区别

ClassLoader 中和类加载有关的方法有很多,前面提到了 loadClass(),除此之外,还有 findClass() 和 defineClass() 等。这3个方法的区别如下

  • loadClass():默认的双亲委派机制在此方法中实现
  • findClass():根据名称或位置加载 .class 字节码
  • definclass():把 .class 字节码转化为 Class 对象

双亲委派的优点

  • 避免类的重复加载:通过委派的方式,可以避免类的重复加载。当父加载器已经加载过某一个类时,子加载器就不会再重新加载这个类。
  • 保证安全性:通过双亲委派的方式,可以保证安全性 。因为 BootstrapClassLoader 在加载的时候,只会加载 JAVA_HOME 中的 jar 包里面的类,如 java.lang.String,那么这个类是不会被随意替换的,除非有人跑到你的机器上,破坏你的 JDK。

双亲委派的缺点

  • 在双亲委派中,子类加载器可以使用父类加载器已经加载过的类,但是父类加载器无法使用子类加载器加载过的类(类似继承的关系)。
    Java 提供了很多服务提供者接口(SPI,Service Provider Interface),它可以允许第三方为这些接口提供实现,比如数据库中的 SPI 服务 - JDBC。这些 SPI 的接口由 Java 核心类提供,实现者确是第三方。如果继续沿用双亲委派,就会存在问题,提供者由 Bootstrap ClassLoader 加载,而实现者是由第三方自定义类加载器加载。这个时候,顶层类加载就无法使用子类加载器加载过的类。
    要解决上述问题,就需要打破双亲委派原则。

四、打破双亲委派模型

双亲委派模型并不是一个强制性约束,而是 Java 设计者推荐给开发者的类加载器的实现方式。在一定条件下,为了完成某些操作,可以 “打破” 模型。

打破双亲委派模型的具体方法包括

  • 重写 loadClass() 方法
  • 利用线程上下文加载器
重写 loadClass 方法
  • 在双亲委派的过程,都是在 loadClass() 方法中实现的,因此要想要破坏这种机制,可以自定义一个类加载器,继承 ClassLoader 并重写 loadClass() 方法即可,使其不进行双亲委派。
利用线程上下文加载器
  • 利用线程上下文加载器(Thread Context ClassLoader)也可以打破双亲委派。
  • Java 应用上下文加载器默认是使用 AppClassLoader。若想要在父类加载器使用到子类加载器加载的类,可以使用 Thread.currentThread().getContextClassLoader()。
// 使用线程上下文类加载器加载资源
public static void main(String[] args) throws Exception{
    String name = "java/sql/Array.class";
    Enumeration<URL> urls = Thread.currentThread().getContextClassLoader().getResources(name);
    while (urls.hasMoreElements()) {
        URL url = urls.nextElement();
        System.out.println(url.toString());
    }
}

六、类加载机制和spi的联系

  • spi打破了双亲委托的类加载机制,直接在load方法中调用反射获取到了相关类的实例

七、类加载机制和反射的联系

  • 类加载机制是JVM通过文件系统将一系列.class文件转化为二进制流加载到JVM内存并生成一个该类的Class对象,为后面的程序运行提供资源的动作。
  • 反射是对于任意一个类,在 运行 状态中,对于任意一个类,都能够知道这个类的所有属性和方法 ;对于任
    意一个对象,都能够调用它的任意方法和属性,既然能拿到,我们就可以修改部分类型信息;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。Class对象将编译产生的.class文件通过加载器加载进内存的堆中的,这里就体现出类加载机制。
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值