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

news2025/3/30 15:10:55

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

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2322533.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MySQL数据库精研之旅第四期:解锁库操作高阶技能

专栏&#xff1a;MySQL数据库成长记 个人主页&#xff1a;手握风云 目录 一、查看所有表 1.1. 语法 二、创建表 2.1. 语法 2.2. 示例 2.3. 表在磁盘上对应的⽂件 三、查看表结构 3.1. 语法 3.2. 示例 四、修改表 4.1. 语法 4.2. 示例 五、删除表 5.1. 语法 5.2.…

【DevOps】DevOps and CI/CD Pipelines

DevOps 是一种将开发与运维实践相结合的模式&#xff0c;旨在缩短软件开发周期并交付高质量软件。 DevOps 是什么&#xff1f; 开发团队与运维团队之间的协作 • 持续集成与持续交付&#xff08;CI/CD&#xff09; • 流程自动化 • 基础设施即代码&#xff08;IaC&#xff09;…

VS自定义静态库并在其他项目中使用

1、VS创建一个空项目或者静态库项目 2、右键项目 属性 修改生成文件类型 3、生成解决方案 4、复制.h文件和.lib文件作为静态库 5、创建一个新项目 测试使用新生成的静态库 在新项目UseStaticLib中加一个新文件夹lib&#xff0c;lib中放入上面的.h和.lib文件。 6、vs中右…

力扣32.最长有效括号(栈)

32. 最长有效括号 - 力扣&#xff08;LeetCode&#xff09; 代码区&#xff1a; #include<stack> #include<string> /*最长有效*/ class Solution { public:int longestValidParentheses(string s) {stack<int> st;int ans0;int ns.length();st.push(-1);fo…

vue3 项目中预览 word(.docx)文档方法

vue3 项目中预览 word&#xff08;.docx&#xff09;文档方法 通过 vue-office/docx 插件预览 docx 文档通过 vue-office/excel 插件预览 excel 文档通过 vue-office/pdf 插件预览 pdf 文档 安装插件 npm install vue-office/docx vue-demi示例代码 <template><Vu…

DHCP(Dynamic Host Configuration Protocol)原理深度解析

目录 一、DHCP 核心功能 二、DHCP 工作流程&#xff08;四阶段&#xff09; 三、关键技术机制 1. 中继代理&#xff08;Relay Agent&#xff09; 2. Option 82&#xff08;中继信息选项&#xff09; 3. 租期管理 4. 冲突检测 四、DHCP 与网络架构交互 1. MLAG 环境 2.…

创建login.api.js步骤和方法

依次创建 login.api.js、home.api.js...... login.api.js、home.api.js 差不多 导入到 main.js main.js 项目中使用

基于springboot二手交易平台(源码+lw+部署文档+讲解),源码可白嫖!

摘要 人类现已迈入二十一世纪&#xff0c;科学技术日新月异&#xff0c;经济、资讯等各方面都有了非常大的进步&#xff0c;尤其是资讯与网络技术的飞速发展&#xff0c;对政治、经济、军事、文化等各方面都有了极大的影响。 利用电脑网络的这些便利&#xff0c;发展一套二手交…

帕金森患者的生活重塑:从 “嘴” 开启康复之旅

当提到帕金森病&#xff0c;许多人会联想到震颤、僵硬和行动迟缓等症状。这种神经系统退行性疾病&#xff0c;给患者的生活带来了巨大的挑战。然而&#xff0c;你可知道&#xff0c;帕金森患者恢复正常生活&#xff0c;可以从 “嘴” 开始管理&#xff1f; 帕金森病在全球影响着…

JVM 为什么不使用引用计数算法?——深入解析 GC 策略

在 Java 中&#xff0c;垃圾回收&#xff08;Garbage Collection, GC&#xff09;是一个至关重要的功能&#xff0c;它能够自动管理内存&#xff0c;回收不再使用的对象&#xff0c;从而防止内存泄漏。然而&#xff0c;在垃圾回收的实现上&#xff0c;JVM 并未采用引用计数算法…

【HarmonyOS NEXT】EventHub和Emitter的使用场景与区别

一、EventHub是什么&#xff1f; 移动应用开发的同学应该比较了解EventHub&#xff0c;类似于EventBus。标准的事件广播通知&#xff0c;订阅&#xff0c;取消订阅的处理。EventHub模块提供了事件中心&#xff0c;提供订阅、取消订阅、触发事件的能力。 类似的框架工具有很多…

01-系统编程

一、程序和进程的区别&#xff1a; window系统&#xff1a; 1、程序存储在硬盘中&#xff0c;文件格式为.exe后缀&#xff0c;静态的 2、进程运行在内存中&#xff0c;动态的 Linux系统 1、程序存储在硬盘中&#xff0c;文件格式为.ELF&#xff08;可执行的链接文件&#…

Linux编译器gcc/g++使用完全指南:从编译原理到动静态链接

一、gcc/g基础认知 在Linux开发环境中&#xff0c;gcc和g是我们最常用的编译器工具&#xff1a; gcc&#xff1a;GNU C Compiler&#xff0c;专门用于编译C语言程序g&#xff1a;GNU C Compiler&#xff0c;用于编译C程序&#xff08;也可编译C语言&#xff09; &#x1f4cc…

26考研|数学分析:定积分及应用

这一部分作为数学分析的灵魂&#xff0c;在数学分析的计算中&#xff0c;绝大部分的问题都可以转换成定积分的计算问题&#xff0c;所以在这部分的学习中&#xff0c;一定要注意提升计算能力&#xff0c;除此之外&#xff0c;由积分引出的相关积分不等式也是分析的重点和难点&a…

扩展卡尔曼滤波

1.非线性系统的线性化 标准卡尔曼滤波 适用于线性化系统&#xff0c;扩展卡尔曼滤波 则扩展到了非线性系统&#xff0c;核心原理就是将非线性系统线性化&#xff0c;主要用的的知识点是 泰勒展开&#xff08;我另外一篇文章的链接&#xff09;&#xff0c;如下是泰勒展开的公式…

4.Matplotlib:基础绘图

一 直方图 1.如何构建直方图 将值的范围分段&#xff0c;将整个值的范围分成一系列间隔&#xff0c;然后计算每个间隔中有多少值。 2.直方图的适用场景 一般用横轴表示数据类型&#xff0c;纵轴表示分布情况。 直方图可以用于识别数据的分布模式和异常值&#xff0c;以及观察数…

VSCode 市场发现恶意扩展正在传播勒索软件!

在VSCode 市场中发现了两个隐藏着勒索软件的恶意扩展。其中一个于去年 10 月出现在微软商店&#xff0c;但很长时间没有引起注意。 这些是扩展ahban.shiba 和 ahban.cychelloworld&#xff0c;目前已从商店中删除。 此外&#xff0c;ahban.cychelloworld 扩展于 2024 年 10 月…

工作流引擎Flowable介绍及SpringBoot整合使用实例

Flowable简介 Flowable 是一个轻量级的业务流程管理&#xff08;BPM&#xff09;和工作流引擎&#xff0c;基于 Activiti 项目发展而来&#xff0c;专注于提供高性能、可扩展的工作流解决方案。它主要用于企业级应用中的流程自动化、任务管理和审批流等场景。 Flowable 的核心…

K8s证书--运维之最佳选择(K8s Certificate - the best Choice for Operation and Maintenance)

K8s证书--运维之最佳选择 No -Number- 01 一个月速通CKA 为了速通CKA&#xff0c;主要办了两件事情 1. 在官方的Killercoda上&#xff0c;练习CKA的题目。把命令敲熟悉。 // https://killercoda.com/killer-shell-ckad 2. 使用K3s在多台虚拟机上快速搭建了K8s集群&…

Leaflet.js+leaflet.heat实现热力图

Leaflet热力图 #mermaid-svg-I1zXN0OrNCBGKEWy {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-I1zXN0OrNCBGKEWy .error-icon{fill:#552222;}#mermaid-svg-I1zXN0OrNCBGKEWy .error-text{fill:#552222;stroke:#5522…