类加载器

类加载过程

首先回顾一下类加载的过程。类的生命周期分为以下七个阶段:加载->验证->准备->解析->初始化->使用->卸载。其中加载是生命周期的第一个阶段,类加载器用不同的方式加载jar包,可以从classpath,动态代理和网络获取要加载的类。

类加载主要做了以下三件事:

  1. 用类的全限定名找到类的class文件,也就是字节流
  2. 将字节流文件转化为方法区中运行时数据结构InstanceKlass,保存常量池,字段,方法等内容
  3. 在堆中创建相应Class对象,映射到方法区的数据结构,这部分是允许程序员操纵的

在 Java 的类加载阶段,InstanceKlassClass 对象各自扮演了不同的角色。理解它们的作用有助于更好地理解 Java 类加载和运行时的内部机制。

1. InstanceKlass

InstanceKlass 是在某些 Java 虚拟机(JVM)实现中用于表示类的内部数据结构的一个术语(例如在 HotSpot JVM 中)。它是 JVM 内部的一个概念,主要用于存储和管理类的各种元数据。InstanceKlass 包含了类的实例字段、方法、常量池等信息,主要用于:

  • 表示和管理类的元数据:包括类的字段、方法、常量池等。
  • 支持类的实例化和动态操作:在 JVM 内部用于实现类实例化、反射等操作。
  • 管理类的加载和初始化:确保类在加载、链接和初始化过程中能够正确地管理其结构和状态。

InstanceKlass 是 JVM 内部对类的表示,主要用于实现 JVM 的各种内部操作,通常不会被应用程序直接访问或操作。

2. Class 对象

Class 对象是 Java 语言层面提供的,用于表示和操作 Java 类的一个公共 API。每个 Java 类在运行时都有一个 Class 对象,用于提供对该类的元数据的访问。Class 对象的作用包括:

  • 反射:提供了用于访问类的信息和操作类的方法(如获取类的字段、方法、构造函数等)。
  • 类型检查:在运行时进行类型检查和类型转换。
  • 动态实例化:允许在运行时创建类的实例(如 Class.forNamenewInstance)。

类的加载阶段的角色和作用

  1. 加载(Loading)

    • 在这个阶段,JVM 会根据类的全限定名找到类文件(.class 文件),并将其字节码读入内存。此时,InstanceKlass 对象可能在内部被创建来管理这个类的字节码和元数据。
  2. 链接(Linking)

    • 验证(Verification):确保类文件的字节码符合 Java 虚拟机规范。
    • 准备(Preparation):为类的静态变量分配内存并初始化。
    • 解析(Resolution):将类中的符号引用解析为直接引用。
  3. 初始化(Initialization)

    • 执行类的静态初始化代码(如静态块)。

InstanceKlassClass 对象的关系

  • 在内部,InstanceKlass 是 JVM 实现中的一个结构,用于存储和管理类的内部数据和状态。
  • Class 对象是 Java 语言层面的一个公共 API,提供了对类信息的访问。

在类加载阶段,InstanceKlass 负责 JVM 内部的类数据结构管理和维护,而 Class 对象则提供了给应用程序使用的接口来访问和操作这些数据。它们各自作用于不同的层面,但在类加载和运行时的操作中是相辅相成的。

关系总结

  • **InstanceKlass**:JVM 内部用于表示和管理类的元数据,是实现细节。
  • Class 对象:Java API 提供的类元数据的公共接口,让应用程序能够通过反射等机制与类进行交互。

InstanceKlassClass 对象 在类加载过程中相互协作,使得类的内部管理与外部访问得以实现。

补充知识点:反射

反射是通过操作Class文件,在类的运行过程中动态的获取一些信息,比如当前时刻的字段和方法。首先在某个类的加载阶段,会在jvm的方法区生成一个InstanceKlass文件,这是由c++实现的,同时也会在堆中生成一个Class对象,该对象有反射的功能。
在需要使用反射的时候,利用Class.forName(全限定名),可以在堆中取出该Class对象,完成反射调用。

类加载器分类

一般来说,自上而下可以分为四个类加载器:

启动类加载器(BootStrapClassLoader)

由jvm提供c++编写,对于java程序员不可见,当打印类加载器名为null的时候即为启动类加载器。一般用于加载java核心类(rt)

扩展类加载器(ExtensionClassLoader)

加载一些拓展jar包,比如applet等一系列非java核心功能,但是有时候有需要用到的官方包

应用程序类加载器(ApplicationClassLoader)

加载在classpath下用户编写的所有java类和jar包

自定义类加载器

用户为了实现某些特定功能编写的,首先说明在java中的ClassLoader继承结构,ClassLoader是一个抽象类,URLClassLoader实现了在对应路径下的二进制文件写入内存的操作。其中由几个重要方法:

  • protected Class<?> findClass(String name):传入的参数为全限定名。默认实现是空方法,在URLClassLoader中将全限定名转换成了路径(.改成了/,后缀添上了.class),自定义类加载器需要重写该方法,自定义逻辑去实际查找并加载类的字节码。例如,从网络、数据库或非标准路径加载类。
  • public Class<?> loadClass(String name):入口方法,遵循双亲委派机制。双亲委派机制:当要加载一个类的时候,如果当前类加载器发现已经加载过该类(调用findLoadedClass,这个方法会使用native方法完成判断是否加载),就直接返回该Class对象,如果没有加载就委派父加载器进行加载,直到启动类加载器。启动类加载器会自顶向下判断是否能够加载当前类,比如启动类加载器加载不了的就给扩展类加载器,如果全部都加载不了,则报错ClassNotFoundException
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    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) {
    // 父加载器找不到类
    }

    if (c == null) {
    // 如果父加载器也找不到类,则调用 findClass 方法加载
    c = findClass(name);
    }
    }

    if (resolve) {
    resolveClass(c);
    }

    return c;
    }
    }
  • protected final Class<?> defineClass():做一些类名的校验,然后调用虚拟机的native方法将字节码信息加载到虚拟机缓存中

defineClass() 与 loadClass() 和 findClass() 的关系:

  • loadClass():loadClass() 是类加载的入口方法,负责按照双亲委派模型加载类。如果父类加载器无法加载该类,loadClass() 最后会调用当前类加载器的 findClass()。
  • findClass():findClass() 是自定义类加载器用于查找类字节码的地方。一般自定义类加载器会重写 findClass(),在找到类的字节码后,通过调用 defineClass() 将其定义为 Class 对象。
  • defineClass():defineClass() 是类加载过程中的核心步骤,负责将字节码转化为 JVM 识别的 Class 对象。通常是 findClass() 方法中的最后一步。
  • 调用关系:loadClass双亲委派机制,用findClass实现文件到字节数组的转换,defineCLass完成字节数组到内存的转化

还有一个要注意的点:

loadClass()有一个resolve参数,这个参数默认为false,代表只进行加载阶段,不进行连接阶段。比如要加载的类中有静态代码块,只调用loadclass是不会初始化这个代码块的,原因是只有加载阶段,不会进行连接更不会初始化了。

双亲委派机制

  1. 自底向上委托父类加载器完成加载
  2. 自顶向下加载类
  3. 好处:可以避免重复加载类;保证安全性

打破双亲委派机制——Tomcat

在Tomcat中,往往包含了很多的web应用,这些应用的全限定名很可能相同(例如启动了两个相同的服务)。对于双亲委派机制而言,每一个web应用都会委派父类加载器进行加载来保证当前classpath下的全限定名唯一性,比如demo1加载了com/yyf/service.class而demo2也有这个类,此时根据机制由于存在一个全限定名相同的类,demo2下的新类不会被加载,这样就会出现问题,因此我们要打破双亲委派机制。

Tomcat的类加载器

Tomcat定义了一系列类加载器:

  • common:通用类,Tomcat和web应用都要使用的类
  • catalina:Tomcat要使用的类(注意:在tomcat中默认没有配置这个的路径,此时catalina类加载器即为common加载器,如果需要使用catalina加载器,需要在catalina.properties中进行路径的配置)
  • shared:web应用要使用的可以共享的类,比如spring和mybatis(注意:与上面的类似,tomcat默认没有配置,此时也为common类加载器)
  • 并行web应用类加载器:每个应用一个

注:共享类加载器
如果所有的web应用都是基于SSM的,可以配置共享类加载器实现只加载一次Spring容器,但是可能由于版本不同可能导致兼容问题,以及由于存在的大量反射会导致类污染,所以需要谨慎选择shared加载的类

如何打破双亲委派机制

有了前面web应用隔离的需求,tomcat通过为每一个web应用创建一个并行web类加载器,只有当全限定名和加载器都是同一个时,才会被认为是重复加载,

  • 左边是双亲委派机制:当需要加载一些内部类的时候还是要走双亲委派机制到启动类加载器
  • 右边是打破双亲委派机制:比如com/yyf/myclass.class这个类,并行web应用类加载器就会直接自己加载,但是如果没有加载成功还是会委派父类加载器进行加载

打破双亲委派机制?——spi机制

spi机制是一种服务发现机制,分为服务提供者和调用者,spi机制规定了需要在classpath下的META-INF/service下以服务调用者的全限定名为文件名创建一个文件,内容为服务提供者的全限定名。再使用ServiceLoader加载服务调用者,就可以调用多个服务提供者。

在jdbc中,可能有多个数据库源。java.sql.Driver需要能够调用多个数据库的驱动比如mysql和Oracle,Driver位于rt中需要用启动类加载器进行加载,而mysql驱动是第三方jar包,需要用应用程序类加载器。但是在默认情况下,一个类及其依赖类由同一个类加载器加载。由于spi的存在,mysql的实现类也会在Driver加载的时候通过启动类加载器进行加载,这显然是错误的。

线程上下文类加载器

问题重述,对于Driver这个类,是需要用启动类加载器进行加载,但由于spi让其发现了mysql的实现类,需要对其进行加载,此时需要打破双亲委派机制,启动类加载器委派应用程序加载器加载mysql驱动

其实在每个线程都会默认保存一个应用程序类加载器,一般来说子线程会继承父线程的应用程序类加载器。在spi问题中,我们只需要取出线程保存的应用程序类加载器实现mysql驱动的加载即可。

同样的,在前文所述的tomcat的spring加载中,我们的代码会使用到spring的一系列注解,在spring容器初始化的过程中也会产生依赖。也就是说,本来是shared(也有可能是common)类加载器加载的spring类,但需要用到classpath下的自己写的应该由并行web应用处理的类。这与spi机制所产生的问题是一致的。

此时也是通过线程上下文类加载器进行解决,只不过这里在开辟一个线程的时候重新set这个变量为当前的并行web应用类加载器,原理与上面的类似。

真的打破了双亲委派机制吗?

其实没有,从宏观上看,这里的Driver还是启动类加载器进行加载,mysql还是应用程序类加载器进行加载,也没有重新写loadclass方法。但也确实是由启动类加载器委派应用程序类加载器进行了加载。只不过是角度不同的问题,从宏观上没有打破,微观上打破了。