深入解析 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 {@Overridepublic Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {// 破坏双亲委派,不委托父加载器,直接尝试自己加载if (name.startsWith("com.mycompany")) { return findClass(name);}return super.loadClass(name, resolve);}@Overridepublic 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.rhkb.cn/news/41339.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【数据可视化艺术·进阶篇】热力图探秘:用色彩演绎场馆和景区的人流奥秘

假期出游&#xff0c;你是不是也遇到过这样的状况&#xff1a;想去的热门景点&#xff0c;放眼望去全是攒动的人头&#xff0c;根本没法好好欣赏风景&#xff1b;而景区里一些小众角落&#xff0c;却冷冷清清&#xff0c;鲜有人至。还有在轨道交通枢纽、大型体育场这些地方&…

理解文字识别:一文读懂OCR商业化产品的算法逻辑

文字识别是一项“历久弥新”的技术。早在上世纪初&#xff0c;工程师们就开始尝试使用当时有限的硬件设备扫描并识别微缩胶片、纸张上的字符。随着时代和技术的发展&#xff0c;人们在日常生活中使用的电子设备不断更新换代&#xff0c;文字识别的需求成为一项必备的技术基础&a…

智能监控视频聚合平台,GB28181/RTSP/SIP/RTMP直播会议融合方案

全场景智能监控聚合平台&#xff1a;打破边界&#xff0c;赋能高效协同 在数字化转型加速的今天&#xff0c;海量视频监控设备、多样化的编码协议与复杂的业务场景&#xff0c;让企业面临跨系统整合难、资源调度效率低、协作响应慢等痛点。我们的智能监控聚合平台以技术创新为…

【机器学习】imagenet2012 数据预处理数据预处理

【机器学习】数据预处理 1. 下载/解压数据2. 数据预处理3. 加载以及训练代码3.1 使用PIL等加载代码3.2 使用OpenCV的方式来一张张加载代码3.3 h5的方式来加载大文件 最后总结 这个数据大约 140个G,128w的训练集 1. 下载/解压数据 首先需要下载数据&#xff1a; 数据最后处理…

语言模型理论基础-持续更新-思路清晰

1.预训练 相似的任务A、B&#xff0c;任务A已经用大数据完成了训练&#xff0c;得到模型A。 我们利用-特征提取模型的-“浅层参数通用”的特性&#xff0c;使用模型A的浅层参数&#xff0c;其他参数再通过任务B去训练&#xff08;微调&#xff09;。 2.统计语言模型 通过条件…

IDEA的基础快捷键

文章目录 1、书写main函数2、书写输出函数println3、书写for循环4、输出变量的值或者输出函数求的值5、代码注释7、主题、字体设置8、自动生成使用信息9、关闭启动IDEA默认打开上次的项目10、字体放大放小11、代码缩进12、快速复制/删除一行13、回退14、字母大小写转换15、调试…

音视频 二 看书的笔记 MediaPlayer

此类是用于播放声音和视频的主要 API 对方不想多说向你丢了一个链接 MediaPlayer Idle 空闲状态Initialized 初始化状态 调用 setDataSource() 时会进入此状态 setDataSource必须在Idle 状态下调用&#xff0c;否则就抛出异常了了了了了。Prepared 准备状态 回调监听setOnPrep…

Linux笔记---动静态库(使用篇)

目录 1. 库的概念 2. 静态库&#xff08;Static Libraries&#xff09; 2.1 静态库的制作 2.2 静态库的使用 2.2.1 显式指定库文件及头文件路径 2.2.2 将库文件安装到系统目录 2.2.3 将头文件安装到系统目录 3. 动态库 3.1 动态库的制作 3.2 动态库的使用 3.2.1 显式…

CAS(Compare And Swap)

CAS核心原理 操作流程 CAS 包含三个参数&#xff1a;内存值&#xff08;V&#xff09;、预期值&#xff08;E&#xff09;和新值&#xff08;N&#xff09;。执行步骤如下&#xff1a; 比较&#xff1a;检查当前内存值 V 是否等于预期值 E。 交换&#xff1a;如果相等&#…

宝塔面板安装docker flarum失败,请先安装依赖应用: [‘mysql‘]:5/8

安装失败的解决方案 提示错误请先安装依赖应用: [mysql]:5/8 解决方案&#xff1a;不要使用最新的docker mysql&#xff0c;使用5.7.44版本docker mysql&#xff0c;等安装完毕再安装docker flarum就不会报错了。 如果安装完成你不知道默认的账号密码可以看这里 宝塔docker f…

c#的.Net Framework 的console 项目找不到System.Window.Forms 引用

首先确保是建立的.Net Framework 的console 项目,然后天健reference 应用找不到System.Windows.Forms 引用 打开对应的csproj 文件 在第一个PropertyGroup下添加 <UseWindowsForms>true</UseWindowsForms> 然后在第一个ItemGroup 下添加 <Reference Incl…

基于 mxgraph 实现流程图

mxgraph 可以实现复杂的流程图绘制。mxGraph里的Graph指的是图论(Graph Theory)里的图而不是柱状图、饼图和甘特图等图(chart)&#xff0c;因此想找这些图的读者可以结束阅读了。 作为图论的图&#xff0c;它包含点和边&#xff0c;如下图所示。 交通图 横道图 架构图 mxGrap…

21.Excel自动化:如何使用 xlwings 进行编程

一 将Excel用作数据查看器 使用 xlwings 中的 view 函数。 1.导包 import datetime as dt import xlwings as xw import pandas as pd import numpy as np 2.view 函数 创建一个基于伪随机数的DataFrame&#xff0c;它有足够多的行&#xff0c;使得只有首尾几行会被显示。 df …

STL之空间配置器

1. 什么是空间配置器 空间配置器&#xff0c;顾名思义就是为各个容器高效的管理空间(空间的申请与回收)的&#xff0c;在默默地工作。虽然在常规使用STL时&#xff0c;可能用不到它&#xff0c;但站在学习研究的角度&#xff0c;学习它的实现原理对我们有很大的帮助。 2. 为什…

Axure项目实战:智慧城市APP(三)教育查询(显示与隐藏交互)

亲爱的小伙伴&#xff0c;在您浏览之前&#xff0c;烦请关注一下&#xff0c;在此深表感谢&#xff01; 课程主题&#xff1a;教育查询 主要内容&#xff1a;教育公告信息&#xff0c;小升初、初升高、高考成绩查询&#xff1b;教育公告信息为传统的信息页面&#xff0c;小升…

最大字段和问题 C++(穷举、分治法、动态规划)

问题描述 给定由n个整数&#xff08;包含负整数&#xff09;组成的序列a1,a2,…,an&#xff0c;求该序列子段和的最大值。规定当所有整数均为负值时定义其最大子段和为0 穷举法 最简单的方法就是穷举法&#xff0c;用一个变量指示求和的开始位置&#xff0c;一个变量指示结束…

【数据转换】- Halcon<->Mat

背景介绍 最近在写C#联合Haclon调用C的.dll文件进行联合编程。大致需求就是C#设计界面&#xff0c;然后调用Haclon的图像处理库&#xff0c;C把目标检测的模型进行TensorRT部署生成动态链接库&#xff0c;之后界面操作加载模型、对图像进行检测等功能。 设计界面如下&#xf…

MFC中如何判断一个窗口当前状态是显示还是隐藏

文章目录 一、核心方法&#xff1a;使用 CWnd::IsWindowVisible函数原型示例代码 二、注意事项1. 父窗口的影响2. 窗口最小化/最大化状态3. 窗口尚未创建 三、扩展&#xff1a;通过窗口样式直接判断四、完整示例代码五、总结 在MFC中&#xff0c;判断窗口当前是显示还是隐藏状态…

Java 大视界 -- 基于 Java 的大数据分布式系统的监控与运维实践(155)

&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎来到 青云交的博客&#xff01;能与诸位在此相逢&#xff0c;我倍感荣幸。在这飞速更迭的时代&#xff0c;我们都渴望一方心灵净土&#xff0c;而 我的博客 正是这样温暖的所在。这里为你呈上趣味与实用兼具的知识&#xff0c;也…

消息队列性能比拼: Kafka vs RabbitMQ

本内容是对知名性能评测博主 Anton Putra Kafka vs RabbitMQ Performance 内容的翻译与整理, 有适当删减, 相关数据和结论以原作结论为准。 简介 在本视频中&#xff0c;我们将首先比较 Apache Kafka 和传统的 RabbitMQ。然后&#xff0c;在第二轮测试中&#xff0c;会将 Kaf…