JVM 类加载器详解

JVM 类加载器详解

如果要确定两个类是否相同,必须满足以下三点:

  1. 同路径
  2. 同名
  3. 由同一个类加载器创建

即使是同路径同名的 class 类,如果被不同的类加载器加载那也是不同的

一、类加载器分类及作用

JVM 类加载器分为 四类,按层级从上到下依次为:

类加载器 作用 加载路径/范围 实现语言
启动类加载器(Bootstrap ClassLoader) 加载 JVM 核心类库 (如 java.lang.*java.util.*),唯一不继承 ClassLoader 的加载器 JAVA_HOME/jre/lib 下的核心 jar 包(如 rt.jar C/C++
扩展类加载器(Extension ClassLoader) 加载 JVM 扩展类库 (如 javax.* JAVA_HOME/jre/lib/ext 目录下的 jar 包 Java
应用程序类加载器(Application ClassLoader) 加载用户类路径(ClassPath)下的类(如项目代码、第三方依赖) -classpath-cp 指定的路径 Java
自定义类加载器(Custom ClassLoader) 用户自定义类加载逻辑(如热部署、网络加载、加密类加载) 由用户代码实现 Java

二、双亲委派机制

1. 核心流程

当类加载器收到加载请求时,按以下步骤处理:

  1. 向上委派
    不直接加载类,而是先委派父类加载器处理。
  2. 向下尝试
    若父类加载器无法完成加载(在自己的搜索范围内找不到类),子类加载器才会尝试加载。
2. 流程示例

具体实现依赖于**ClassLoader.loadClass()**

java 复制代码
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
        // 1. 检查是否已加载
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            try {
                // 2. 先委派父加载器加载
                if (parent != null) {
                    c = parent.loadClass(name, false); // 递归调用父加载器
                } else {
                    // 父加载器为 null,表示到达 Bootstrap 类加载器
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // 父加载器加载失败,继续向下执行
            }

            if (c == null) {
                // 3. 父加载器未找到,由当前类加载器加载
                c = findClass(name);
            }
        }
        return c;
    }
}
  • 用户自定义类 MyClass 的加载过程:
    1. Application ClassLoader → 委派给 Extension ClassLoader
    2. Extension ClassLoader → 委派给 Bootstrap ClassLoader
    3. Bootstrap ClassLoader 未找到 MyClass → 返回 Extension ClassLoader
    4. Extension ClassLoader 未找到 MyClass → 返回 Application ClassLoader
    5. Application ClassLoader 在 ClassPath 中找到并加载 MyClass
3. 核心意义
  • 避免重复加载:确保类在 JVM 中唯一(由父加载器优先加载)。对于避免重复加载,双亲委派通过委派链确保每个类由最顶层的父加载器优先尝试加载,如果已经加载过,就直接返回,不会再让子加载器处理。
  • 保护核心类安全 :防止用户自定义同名类(如 java.lang.String)覆盖 JVM 核心类。类加载过程会由启动类加载器强制优先加载核心类 ,加载后就会直接返回,所以子类无法通过自定义同名类来覆盖核心类

三、打破双亲委派机制的场景

1. 经典场景
  • SPI 服务发现
    JDBC 的 DriverManager 需要加载不同厂商的数据库驱动(如 MySQL、Oracle)。
    • 问题:启动类加载器想要加载不在自己加载路径下(范围内)的类,即跨级加载子加载器的类

      核心类 DriverManager(由 Bootstrap ClassLoader 加载)需调用第三方驱动的实现类(由 Application ClassLoader 加载,其位置并不在启动类加载器的扫描路径下),但双亲委派机制禁止父加载器访问子加载器的类。

    • 解决方案 :将类加载器切换为 线程上下文类加载器,将加载权临时交给子类加载器。

2. 如何打破双亲委派?
  • 重写 loadClass() 方法
    自定义类加载器不委派父加载器,直接加载类。
  • 线程上下文类加载器
    通过 Thread.currentThread().setContextClassLoader() 临时切换类加载器。

以下是java发展历程中的三次打破双亲委派机制

第一次破坏:JDK 1.2 之前的"远古时代"
  • 背景
    在 JDK 1.2 引入双亲委派模型之前,类加载器的实现没有统一规范。开发者通常直接重写 loadClass() 方法,直接实现类加载逻辑,未形成层级委派机制
  • 问题
    • 类加载逻辑混乱,容易重复加载或覆盖核心类。
    • 缺乏统一的委派规则,导致类加载器之间的协作困难。
  • JDK 1.2 的改进
    • 正式提出双亲委派模型,规范类加载流程。
    • 建议开发者仅重写 findClass()
      loadClass() 方法默认实现委派逻辑,而 findClass() 仅负责具体类查找(如从自定义路径加载字节码)。开发者只需重写 findClass() 即可实现扩展,无需破坏委派机制。
  • 意义
    通过约束开发者仅扩展 findClass(),避免直接干预委派流程,从而维护双亲委派的核心规则。

第二次破坏:模型自身的缺陷(基础类型回调用户代码)
  • 背景

    双亲委派模型要求父加载器优先加载类,但某些场景下,​父加载器加载的类需要调用子加载器加载的实现类,导致无法直接通过委派机制实现。

  • 典型案例

    • JDBC SPI(如 DriverManager):

      • DriverManager(由 Bootstrap ClassLoader 加载)需要加载不同数据库厂商的 Driver 实现类(由 Application ClassLoader 加载)。
    • 按双亲委派规则,父加载器(Bootstrap)无法直接访问子加载器(Application)的类,导致驱动无法加载。

  • 解决方案

    • 线程上下文类加载器(TCCL)
      通过 Thread.currentThread().setContextClassLoader() 将类加载器切换为子加载器(如 Application ClassLoader),使父加载器代码可间接访问子加载器的类。
  • 意义

    通过打破双亲委派,解决了核心库(如 JDBC)需要动态扩展的难题,但需谨慎使用以避免安全风险。


第三次破坏:动态性需求(热替换、模块热部署)
  • 背景

    现代应用对 ​动态性 的要求需要更灵活的类加载机制,而双亲委派的层级化模型无法满足。

  • 典型案例

    Tomcat 热部署:

    • 每个 Web 应用使用独立的 WebappClassLoader,重启应用时直接替换类加载器,实现代码热替换。
  • 意义

    牺牲双亲委派的部分安全性,换取系统的高灵活性和动态性,适应云原生、微服务等现代架构需求。


四 、自定义类加载器

使用场景
  • 想加载非 classpath 随意路径中的类文件
  • 通过接口来使用实现,希望解耦时,常用在框架设计和SPI思想
  • 这些类希望予以隔离,不同应用的同名类都可以加载(其实就是违背双委机制的意义1),不冲突,常见于 tomcat 容器
步骤
  1. 继承 ClassLoader 父类
  2. 要遵从双亲委派机制,重写 findClass 方法(不是重写 loadClass 方法,否则不会走双亲委派机制)
  3. 读取类文件的字节码,调用父类的 defineClass 方法来加载类
  4. 使用者调用该类加载器的 loadClass 方法

补充问题

为什么tomcat要自定义类加载器?

  1. 无法隔离不同web应用的类

    比如两个web应用分别依赖同一个库的不同版本(如 log4j 1.x 和 log4j 2.x)

    若遵循双亲委派:

    1. 父加载器加载 log4j-1.2.17.jar
    2. 应用 2 的类加载器委托父加载器时,发现 Log4j 类已加载,被迫使用旧版本,导致兼容性问题。

    打破双亲委派后:

    1. 应用 2 的 WebApp ClassLoader 优先加载自己的 log4j-2.14.1.jar
    2. 两个应用使用各自版本的 Log4j,互不干扰。
  2. 无法支持热部署

    Web 应用需要在不重启 Tomcat 的情况下重新加载类,但双亲委派模型下,一旦类被父加载器加载,无法卸载和重新加载。

    • tomcat内部是使用WebApp ClassLoader,每个web应用都会独立使用一个类加载器

    • 在加载类时,先检查本地缓存,再尝试自己加载,然后才是委派给父加载器

    • 这样在 Web 应用重启时,Tomcat 会销毁旧的 WebApp ClassLoader,并创建新的类加载器重新加载类,旧的类可被 GC 回收,实现热部署。

jvm中类加载后怎么卸载,什么时候卸载?

卸载类即该类的 Class 对象被 GC,卸载类需要满足3个要求:

  1. 该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象
  2. 该类没有在其他任何地方被引用
  3. 该类的类加载器的实例已被 GC,一般是可替换类加载器的场景

注:

  • 类加载器本身必须被垃圾回收,其加载的类才被卸载
  • 在 JVM 生命周期类,由 JVM 自带的类加载器加载的类是不会被卸载的,自定义的类加载器加载的类是可能被卸载。因为 JVM 会始终引用启动、扩展、系统类加载器,这些类加载器始终引用它们所加载的类,这些类始终是可及的

因此回到问题本身:

1.只有类加载器被卸载,类没有实例且class对象没有被引用才会被卸载

2.卸载方式是三个条件全部满足时被GC回收

相关推荐
安然~~~2 小时前
常见的【垃圾收集算法】
java·jvm
低调小一3 小时前
理解 JVM 的 8 个原子操作与 `volatile` 的语义
java·jvm
七夜zippoe3 小时前
JVM 调优在分布式场景下的特殊策略:从集群 GC 分析到 OOM 排查实战(二)
jvm·分布式
Familyism3 小时前
Java虚拟机——JVM
java·开发语言·jvm
^辞安3 小时前
什么是Mvcc
java·数据库·mysql
烈风3 小时前
009 Rust函数
java·开发语言·rust
这次选左边3 小时前
Flutter混合Android开发Release 打包失败GeneratedPluginRegistrant.java,Plugin不存在
android·java·flutter
yujkss3 小时前
23种设计模式之【策略模式】-核心原理与 Java 实践
java·设计模式·策略模式
tuokuac3 小时前
使用建造者模式创建对象
java