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回收

相关推荐
夜之眷属12 小时前
Core dump 崩溃排查:JVM 宕机后,那份 core 文件怎么用 gdb 还原现场
java·运维·服务器·jvm
波加曼大王12 小时前
# vLLM不要迷信PagedAttention神话,聊聊线上藏着的内部碎片陷阱
java·架构
SL_staff12 小时前
目标健康度自检清单:开发者视角下的目标-计划-任务链路断点诊断
java·开源·github
for_ever_love__12 小时前
MySQL 高频面试题 30 问:索引、事务、锁、日志、主从与分库分表(MySQL 8.0 版)
java·数据库·spring boot·mysql·spring cloud·面试·总结
神仙别闹12 小时前
基于C++实现的贪吃蛇游戏平台
java·c++·游戏
布吉岛的石头13 小时前
Java 程序员第 49 阶段11:Java 接 BERT:用 ONNX Runtime 做句向量推理
java·人工智能·python·深度学习·bert·transformer
niucloud-admin13 小时前
JAVA V6 多商户商城 开发文档——JAVA服务端
java·开发语言
alexlifexyz13 小时前
写了个 Claude Code 插件:AI 写的 Java 违反阿里规约,就写不进文件
java
威哥爱编程13 小时前
【AI全栈12-01】Spring Boot 为何是 AI 全栈后端优选
java·spring boot·后端
威哥爱编程13 小时前
【AI全栈12-03】Spring Boot 用多模型路由把智能客服成本降下来
java·spring boot·后端