当前位置: 首页 > news >正文

深入解析 Java 类加载机制及双亲委派模型

🔍 Java的类加载机制是确保应用程序正确运行的基础,特别是双亲委派模型,它通过父类加载器逐层加载类,避免冲突和重复加载。但在某些特殊场景下,破坏双亲委派模型会带来意想不到的效果。本文将深入解析Java类加载机制、双亲委派模型的运作原理,以及如何在特定场景下破坏这一模型。

📌 类加载机制(Class Loading Mechanism)

Java 之所以能实现【编译一次,到处运行】,很大程度上得益于类加载机制(Class Loading Mechanism) 。Java 类的加载过程主要包含以下三个主要阶段:

1.加载(Loading)

📂 通过从 .class 文件中读取字节码,并创建对应的 Class 对象。

2.链接(Linking)

  • 🔍 验证(Verification) :确保字节码格式正确,不做坏事(如非法访问内存)。
  • 📌 准备(Preparation) :为类的静态变量分配内存,初始化默认值。
  • 🔗 解析(Resolution) :将符号引用替换为直接引用(如 String -> java.lang.String)。

3.初始化(Initialization)

⚡ 执行类的 <clinit> 方法,赋值静态变量,执行静态代码块。

在这里插入图片描述

这些步骤构成 JVM 的类加载流程,而其中最重要的规则之一就是双亲委派模型


🔰 双亲委派模型(Parent Delegation Model)

❓什么是双亲委派(Parent Delegation Model)?

它是一种递归委托机制,目的是保证 Java 核心类的安全性和唯一性。需要遵守以下规则:

✅ 当一个类加载器收到加载请求时,不会自己先加载,而是优先交给它的父类加载器
✅ 只有当 所有的父类加载器都无法加载该类 时,才会由当前加载器自己尝试加载。


🛠️类加载器(ClassLoader)

🔎 常见的类加载器

📌 类加载器作用
Bootstrap ClassLoader (引导类加载器)负责加载 JDK 核心类库(如 rt.jar, java.base),由 C++ 实现,不继承 ClassLoader
Extension ClassLoader (扩展类加载器)负责加载 JAVA_HOME/lib/ext/ 目录下的扩展类库(如 javax 包)。
Application ClassLoader (应用类加载器)负责加载CLASSPATH 下的类,也是 ClassLoader 的子类。
Custom ClassLoader (自定义类加载器)通过继承 ClassLoader 来实现动态加载、加密解密、热更新等功能。

📜 类加载器的层级

Java 默认的类加载器层级如下:

🟠 BootstrapClassLoader (引导类加载器,加载 Java 核心类,如 `java.lang.*`)
     ↓
🟡 ExtClassLoader (扩展类加载器,加载 `lib/ext` 目录下的类)
     ↓
🔵 AppClassLoader (应用类加载器,加载 `classpath` 下的类)
     ↓
🟣 Custom ClassLoader (自定义类加载器)

📦 类加载器的双亲委派模型

双亲委派机制(Parent Delegation Model) 主要用于保证类的安全性和避免重复加载。其工作流程如下:

✅ 当一个类加载器接到加载请求,它会先委派给父类加载器
✅ 如果父类加载器能够加载这个类,就直接返回已加载的类
✅ 如果父类加载器无法加载,才会由当前类加载器尝试加载这个类。

这个机制可以 防止核心 API(如 java.lang.String )被篡改,并且 提高类加载的效率(同一个类不会被重复加载)。

请添加图片描述

  • Bootstrap ClassLoader 处于 最顶层,加载 JDK 自带的核心类库。
  • Extension ClassLoaderBootstrap ClassLoader 加载,负责 JDK 的扩展类库。
  • Application ClassLoader 负责加载 用户代码(即 CLASSPATH 下的类)
  • Custom ClassLoader 继承 ClassLoader,通常用于 热加载、自定义加密加载等

💡举个栗子:

当你在代码中使用 String.class 时,JVM 不会classpath 里去找,而是直接交给 BootstrapClassLoader 加载。这样可以确保 java.lang.String 不会被篡改


💡 双亲委派的好处

防止核心类被篡改:确保 java.lang.Objectjava.lang.String 等类的唯一性,避免被应用程序随意修改。
提高加载效率:如果某个类已经被父类加载器加载,子类加载器就不需要再重复加载。

🧠 但是,在某些特殊场景下,我们可能需要打破双亲委派机制。


🚨 破坏双亲委派机制的场景分析

尽管双亲委派模型是 Java 类加载的核心机制,但在某些特殊场景下,它需要被“破坏”或绕过。

🧠 为什么需要"破坏"双亲委派?

灵活性需求:某些场景需要动态加载用户提供的实现
模块化隔离:不同模块可能需要相同类的不同版本
热更新:运行时替换类定义
SPI扩展:基础框架需要加载未知的实现类


📌 破坏双亲委派机制的主要场景

(1)JDBC SPI机制

⚠️ 现象分析

📌 DriverManager 由 BootstrapClassLoader 加载(因为 DriverManager 在 rt.jar 中)。
📌 但是数据库驱动(如 mysql-connector-java)却是由 AppClassLoader 加载的。

✅ 解决方案

📌 JDBC 采用 线程上下文类加载器(Thread Context ClassLoader, TCCL) 来动态加载驱动。

// JDBC 获取连接时的类加载方式
Connection conn = DriverManager.getConnection(url);
// 内部使用 Thread.currentThread().getContextClassLoader() 来加载驱动

(2)Tomcat 等 Web 容器

⚠️ 现象分析

📌 需要隔离不同Web应用(防止类冲突)。
📌 共享某些公共库(如Servlet API)。

✅ 解决方案

📌 每个Web应用有自己的WebappClassLoader
📌 优先加载自己WEB-INF/classes和WEB-INF/lib下的类。
📌 共享类则委派给Common ClassLoader


(3)JNDI服务

⚠️ 现象分析

📌 JNDI核心类由 Bootstrap 加载。
📌 但具体实现(如LDAP、RMI等)需要由应用类加载器加载。

✅ 解决方案

📌 采用 线程上下文类加载器 来动态加载 JNDI 具体实现。


(4)热部署/热替换场景

⚠️ 现象分析

📌 需要重新加载修改后的类而不重启JVM。
📌 标准的双亲委派无法实现类卸载和重新加载。

✅ 解决方案

📌 自定义类加载器实现(如JRebel)。
📌 每个类版本由不同的类加载器实例加载。


📌 梳理破坏双亲委派的几种操作

🎯 场景🔥 破坏原因💡 解决方案
JDBC SPIDriverManager 需要 AppClassLoader 加载驱动线程上下文类加载器
Tomcat/Web 容器需要隔离不同 Web 应用每个 WebApp 有自己的类加载器
JNDI核心类由 BootstrapClassLoader 加载,具体实现需 AppClassLoader线程上下文类加载器
热部署需要重新加载类自定义类加载器(如 JRebel)

(1) 重写 ClassLoaderloadClass 方法(暴力反叛)

默认的 ClassLoader 使用 loadClass() 方法实现双亲委派,如果我们不按套路来,自己定义一个 ClassLoader,并直接从文件或网络加载 class 文件,就可以绕开双亲委派规则:

public class MyClassLoader extends ClassLoader {
    @Override
    public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        // 破坏双亲委派,不委托父加载器,直接尝试自己加载
        if (name.startsWith("com.mycompany")) { 
            return findClass(name);
        }
        return super.loadClass(name, resolve);
    }

    @Override
    public Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] bytes = loadClassData(name);
        return defineClass(name, bytes, 0, bytes.length);
    }

    private byte[] loadClassData(String name) {
        // 从文件或网络加载 class 字节码
        return new byte[0]; // 这里只是示例,实际要实现字节码加载
    }
}

这种方式通常用于 动态加载类(如热替换、插件系统) ,但可能会带来类冲突问题。


(2)线程上下文类加载器(Thread Context ClassLoader)

Java 允许在线程级别动态更换类加载器,JDBC、SPI(Service Provider Interface)机制 就是靠这个来破坏双亲委派的!

Thread.currentThread().setContextClassLoader(new MyClassLoader());

JVM 在某些地方会调用 Thread.currentThread().getContextClassLoader() 来加载类,比如 ServiceLoader 机制,这使得它可以绕过双亲委派,加载应用级别的 SPI 扩展。


(3) defineClass() 方法直接加载字节码

Java 的 defineClass() 方法可以 绕过标准的类加载流程,直接把一个字节码转换成 Class 对象,而不经过双亲委派。

byte[] classBytes = ...; // 通过 IO 读取 class 文件
Class<?> clazz = defineClass("com.example.MyClass", classBytes, 0, classBytes.length);
public class MyClassLoader extends ClassLoader {
    public Class<?> loadClassFromFile(String className, String path) throws IOException {
        byte[] classData = Files.readAllBytes(Paths.get(path));
        return defineClass(className, classData, 0, classData.length);
    }

    public static void main(String[] args) throws Exception {
        MyClassLoader loader = new MyClassLoader();
        Class<?> clazz = loader.loadClassFromFile("com.example.MyClass", "/path/to/MyClass.class");

        Object obj = clazz.getDeclaredConstructor().newInstance();
        System.out.println("Loaded class: " + obj.getClass().getName());
    }
}

📌 将一个二进制的 .class 文件数据(字节数组 b)转换成 Class<?> 对象。
📌 这个方法通常用于自定义类加载器中,以加载不是由标准类加载器(如 BootstrapClassLoader、AppClassLoader)加载的类。
📌 defineClass 仅负责定义类,不会自动执行类的初始化(不会调用 静态代码块)。

defineClassloadClass 的区别

方法作用
loadClass(String name)委托双亲委派机制加载类,通常不会自行加载字节码。
defineClass(String name, byte[] b, int off, int len)直接用字节数组定义类,不经过双亲委派。

如果你要完全绕过双亲委派机制,可以自己实现 findClass 并调用 defineClass,但通常不推荐这样做,除非是像插件系统、热加载等特殊场景。


📊 总结

⚙️ 理解Java类加载机制和双亲委派模型,是开发高效、稳定应用的基础。虽然双亲委派模型能确保类加载的一致性,但在特定需求下,灵活调整或破坏它能够带来意想不到的优化。掌握这些关键细节,将帮助你在开发过程中游刃有余。💡

在这里插入图片描述

相关文章:

  • 【MARK-2小车】小车教程、上位机教程
  • 高等数学-第七版-上册 选做记录 习题5-4
  • 表的约束及代码练习
  • django入门教程之templates和static资源【五】
  • 八纲辨证总则
  • 如何优化 docker 镜像体积?
  • 【深度学习基础 2】 PyTorch 框架
  • EMQX Dashboard
  • Java 大视界 -- Java 大数据在智能金融区块链跨境支付与结算中的应用(154)
  • C#多态性入门:从零到游戏开发实战
  • Unity URP自定义Shader支持RenderLayer
  • 【Unity3D实现UI轮播效果】
  • 无人机+evtol:低空经济市场硬通货技术详解
  • HCIP(二)
  • 六十天Linux从0到项目搭建(第八天)(缓冲区、gitee提交)
  • 让 AI 更智能的检索增强生成(Retrieval-Augmented Generation)
  • 组态软件之万维组态介绍(web组态、html组态、vue2/vue3组态、组态软件、组态编辑器)
  • Redis 集群配置
  • 代码随想录算法训练营Day12 | Leetcode 226翻转二叉树、101对称二叉树、104二叉树的最大深度、111二叉树的最小深度
  • PHP框架 ThinkPHP 漏洞探测分析
  • 河南通报部分未检疫生猪流入:立案查处,涉案猪肉被封存
  • 国家统计局:要持续加大好房子建设供应力度,积极推动城市更新行动和保障房建设
  • 人民日报大家谈:为基层减负,治在根子上减到点子上
  • 大风+暴雨,中央气象台双预警齐发
  • 《歌手》回归,人均技术流,00后整顿职场
  • 海昏侯博物馆展览上新,“西汉帝陵文化展”将持续展出3个月