Java双亲委派机制讲解和常见问题解决案例示范

1. 引言

Java 的类加载机制是 JVM 运行时系统的核心之一,而其中的双亲委派机制(Parent Delegation Model)是保证 Java 平台安全性与可扩展性的关键设计。双亲委派机制确保了 Java 体系中类的加载顺序,防止了类的重复加载与覆盖,避免了某些安全性风险。

2. 什么是双亲委派机制?

2.1 类加载器概述

Java 类的加载是通过类加载器(ClassLoader)实现的。类加载器负责将 .class 文件加载到 JVM 中,并将其转换为类对象。在 Java 中,类加载器之间存在层次关系,标准的类加载器包括:

  • Bootstrap ClassLoader:启动类加载器,用 C++ 编写,负责加载 JDK 核心类,如 java.lang.* 包中的类。
  • Extension ClassLoader:扩展类加载器,负责加载 JDK 的扩展库 lib/ext 目录下的类。
  • Application ClassLoader:应用类加载器,负责加载应用程序的类路径(classpath)下的类。

2.2 双亲委派机制原理

双亲委派机制的核心思想是:当类加载器需要加载一个类时,它首先将请求委派给父类加载器,父类加载器再依次向上委派,直到请求到达 Bootstrap 类加载器。如果父类加载器无法完成类的加载任务,子类加载器才会尝试自己加载。

这种机制确保了 Java 核心类库的优先加载和安全性。例如,如果你在自己的项目中定义了一个名为 java.lang.String 的类,双亲委派机制将确保 JVM 加载的是 Java 自带的 String 类,而不是你自定义的版本。

2.3 类加载过程

类加载过程可以分为以下几个阶段:

  • 加载:查找并加载类的二进制数据。
  • 链接:将类的二进制数据合并到 JVM 中,分为验证、准备和解析三个步骤。
  • 初始化:对类进行初始化,主要是执行类的静态代码块和初始化静态字段。

3.时序图

在这里插入图片描述

4. 双亲委派机制的常见问题与解决方案

4.1 类加载冲突

问题描述

在电商系统中,不同模块可能依赖同一类库的不同版本。例如,订单模块和支付模块都依赖第三方支付库,但版本不同。如果类加载器没有正确处理,就会导致类加载冲突,产生 ClassCastExceptionNoSuchMethodError 等异常。

问题示例

假设电商系统中的订单模块使用了 PaymentSDK v1.0,而支付模块使用了 PaymentSDK v2.0,它们都尝试加载 PaymentService 类:

// 订单模块中的 PaymentService v1.0
package com.order.payment;public class PaymentService {public void processPayment() {System.out.println("Processing payment with Payment SDK v1.0");}
}// 支付模块中的 PaymentService v2.0
package com.payment.sdk;public class PaymentService {public void processPayment() {System.out.println("Processing payment with Payment SDK v2.0");}
}

在这种情况下,由于双亲委派机制,系统可能会错误地加载同一个版本的 PaymentService,导致类冲突。

解决方案

为了避免类冲突,最好的方式是使用自定义类加载器来为不同模块隔离类加载。这样可以确保每个模块加载自己所需的类库版本。

解决代码
public class CustomClassLoader extends ClassLoader {private String classpath;public CustomClassLoader(String classpath, ClassLoader parent) {super(parent);this.classpath = classpath;}@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {String fileName = classpath + name.replace('.', '/') + ".class";try {byte[] classData = Files.readAllBytes(Paths.get(fileName));return defineClass(name, classData, 0, classData.length);} catch (IOException e) {throw new ClassNotFoundException("Class not found: " + name, e);}}
}public class ECommerceSystem {public static void main(String[] args) throws Exception {// 自定义类加载器分别加载订单模块和支付模块CustomClassLoader orderClassLoader = new CustomClassLoader("order_module/classes/", ECommerceSystem.class.getClassLoader());CustomClassLoader paymentClassLoader = new CustomClassLoader("payment_module/classes/", ECommerceSystem.class.getClassLoader());// 通过各自的类加载器加载 PaymentService 类Class<?> orderPaymentServiceClass = orderClassLoader.loadClass("com.order.payment.PaymentService");Class<?> paymentPaymentServiceClass = paymentClassLoader.loadClass("com.payment.sdk.PaymentService");Object orderPaymentService = orderPaymentServiceClass.getDeclaredConstructor().newInstance();Object paymentPaymentService = paymentPaymentServiceClass.getDeclaredConstructor().newInstance();// 正确调用不同版本的支付服务orderPaymentServiceClass.getMethod("processPayment").invoke(orderPaymentService);paymentPaymentServiceClass.getMethod("processPayment").invoke(paymentPaymentService);}
}

通过这种方式,订单模块和支付模块可以加载各自版本的 PaymentService,避免类加载冲突。

4.2 打破双亲委派

问题描述

有时为了灵活性或功能实现,开发者可能会刻意打破双亲委派机制,例如一些插件系统需要加载自己的类版本,而不是依赖父类加载器中的类。这可能会带来安全风险或导致系统行为异常。

问题示例

以下示例展示了插件系统直接加载自己的 String 类,打破双亲委派机制:

// 插件模块中的 String 类
package com.plugin;public class String {public void print() {System.out.println("Plugin String class");}
}// 插件类加载器
public class PluginClassLoader extends ClassLoader {@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {if (name.equals("java.lang.String")) {// 强行加载自定义的 String 类return defineClass(name, new byte[]{/*...class bytecode...*/}, 0, 0);}return super.findClass(name);}
}

这种做法会导致系统加载到插件的 String 类,取代标准库中的 String 类,造成运行时异常或难以调试的问题。

解决方案

为了避免此类问题,应严格遵循双亲委派机制,避免破坏系统核心类的加载。插件系统应该使用自定义类加载器加载插件内部的类,但不应该加载或覆盖核心 Java 类库。

解决代码

修改后的 PluginClassLoader 遵循双亲委派机制,避免加载核心类库:

public class SafePluginClassLoader extends ClassLoader {@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {// 限制插件只能加载指定路径下的类,不能加载 java.* 包中的类if (name.startsWith("java.")) {throw new ClassNotFoundException("Cannot load system class: " + name);}return super.findClass(name);}
}

这样,插件系统在加载自定义类时,不会破坏系统核心类库的加载顺序和安全性。

4.3 类加载器泄漏

问题描述

在使用自定义类加载器的电商系统中,如果类加载器引用了大量的类或资源,且没有正确释放,可能会导致类加载器泄漏。这会使类加载器及其加载的类无法被垃圾回收,造成内存泄漏。

问题示例

以下代码展示了一个错误的类加载器实现,它持有了外部资源引用,导致内存泄漏:

public class LeakyClassLoader extends ClassLoader {private static List<Object> references = new ArrayList<>(); // 静态引用,导致类加载器无法被回收@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {Class<?> clazz = super.findClass(name);references.add(clazz);  // 错误地将类对象放入静态列表return clazz;}
}

由于 references 是静态的,类加载器及其加载的所有类将无法被垃圾回收,导致内存泄漏。

解决方案
  1. 及时清理引用:避免静态变量引用类加载器加载的类,确保类加载器不持有外部引用。
  2. 使用 WeakReference:如果必须持有引用,可以使用 WeakReference,确保 JVM 能正确回收类加载器和类。
解决代码
public class SafeClassLoader extends ClassLoader {private List<WeakReference<Object>> references = new ArrayList<>();@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {Class<?> clazz = super.findClass(name);references.add(new WeakReference<>(clazz));  // 使用弱引用,防止内存泄漏return clazz;}public void clearReferences() {references.clear();  // 及时清理引用}
}

通过使用 WeakReference,可以确保类加载器加载的类不会导致内存泄漏。

4.4 类加载顺序问题

问题描述

在复杂的电商系统中,类加载的顺序可能会影响应用程序的行为。如果类加载顺序不当,可能会导致错误版本的类被加载,进而引发 NoClassDefFoundErrorClassNotFoundException

问题示例

假设系统中同时存在 OrderService 的两个版本,并且类加载器顺序配置错误,可能导致加载了错误版本的 OrderService

// 订单模块的 OrderService v1
package com.order;public class OrderService {public void processOrder() {System.out.println("Processing order with OrderService v1");}
}// 新版本的 OrderService v2
package com.order;public class OrderService {public void processOrder() {System.out.println("Processing order with OrderService v2");}
}// 错误的类加载顺序可能导致系统加载旧版本
解决方案
  1. 统一类加载器配置:确保类加载器的优先级和顺序配置正确,避免加载错误版本的类。
  2. 日志调试:通过输出类加载器的日志信息,确保正确的类被加载。
解决代码

在应用中通过日志记录类加载器信息,确保加载到正确的类版本:

public class OrderServiceLoader {public static void main(String[] args) {try {Class<?> orderServiceClass = Class.forName("com.order.OrderService");Object orderService = orderServiceClass.getDeclaredConstructor().newInstance();System.out.println("Loaded by: " + orderServiceClass.getClassLoader());  // 输出类加载器信息orderServiceClass.getMethod("processOrder").invoke(orderService);} catch (Exception e) {e.printStackTrace();}}
}

通过这种方式,可以在调试时确认类加载顺序是否正确,避免加载错误版本的类。

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

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

相关文章

ARP欺骗的多种手法

学习参考&#xff1a; ARP欺骗的各种d玩法-CSDN博客 https://juejin.cn/post/7383702153892954164 一、什么是ARP欺骗 1.什么是ARP&#xff1f; ARP (Address Resolution Protocol) 是一种网络层协议&#xff0c;用于将 IP 地址转换为物理地址&#xff08;MAC 地址&#xff0…

湖科大-计网真题笔记

09 序列号不涉及首部

前端开发笔记-- 黑马程序员4

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 css 三角写法用户界面鼠标样式取消表单轮廓vertical-align文本溢出 html5 新标签多媒体标签视频标签![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/d85d…

深入解析 HashMap 的 remove() 方法及其相关实现

HashMap 是 Java 中最常用的集合类之一&#xff0c;它提供了高效的键值对存储和检索功能。本文将详细解析 HashMap 的 remove() 方法及其相关的内部实现&#xff0c;包括 removeNode() 和 removeTreeNode() 方法。通过这些方法&#xff0c;我们可以了解 HashMap 如何高效地移除…

中国剩余定理 C++

题目 解题思路 原链接&#xff1a;https://www.acwing.com/solution/content/3539/ 大致步骤&#xff1a; 将第2,3,4…n个方程不断与第一个方程合并&#xff0c;得到方程a1k1a2k2m2-m1;用扩展欧几里得算法解出a1k1a2k2gcd(a1, a2)的结果&#xff0c;再将结果扩大(m2-m1)/d倍即…

Linux:进程控制(三)——进程程序替换

目录 一、概念 二、使用 1.单进程程序替换 2.多进程程序替换 3.exec接口 4.execle 一、概念 背景 当前进程在运行的时候&#xff0c;所执行的代码来自于自己的源文件。使用fork创建子进程后&#xff0c;子进程执行的程序中代码内容和父进程是相同的&#xff0c;如果子进…

12.2 Linux_进程间通信_共享内存

概述 什么是共享内存&#xff1a; 共享内存又叫内存映射&#xff0c;可以通过mmap()映射普通文件。 实际上就是将磁盘中的一个文件映射到内存的一个缓冲区中去&#xff0c;这样进程就可以直接将这块空间当作普通内存来访问&#xff0c;不需要再使用I/O中的read/write去访问这…

CV实战01 YOLOv5实现图像分割

网上翻了一天&#xff0c;没找到称心的教程&#xff0c;最后发现还是Ultralytics官方的教程文档好用&#xff01;这里贴上官方教程一起学习&#xff01; 【1&#xff1a;找到官方教程文档】 yolov5官方下载地址&#xff1a;GitHub - ultralytics/yolov5: YOLOv5 &#x1f680…

数字后端零基础入门系列 | Innovus零基础LAB学习Day1

一 Floorplan 数字IC后端设计如何从零基础快速入门&#xff1f;(内附数字IC后端学习视频&#xff09; Lab5-1这个lab学习目标很明确——启动Innovus工具并完成设计的导入。 在进入lab之前&#xff0c;我们需要进入我们的FPR工作目录。 其中ic062为个人服务器账户。比如你端…

多线程代码案例

案例一.单例模式 单例模式是一种设计模式;类似于棋谱,有固定套路,针对一些特定场景可以给出一些比较好的解决方案; 只要按照设计模式来写代码,就可以保证代码不会太差,保证了代码的下限; --------------------------------------------------------------------------------…

【优选算法】(第三十六篇)

目录 ⼆叉树的锯⻮形层序遍历&#xff08;medium&#xff09; 题目解析 讲解算法原理 编写代码 ⼆叉树的最⼤宽度&#xff08;medium&#xff09; 题目解析 讲解算法原理 编写代码 ⼆叉树的锯⻮形层序遍历&#xff08;medium&#xff09; 题目解析 1.题目链接&#xf…

植物大战僵尸杂交版

最新版植物大战僵尸杂交版 最近本款游戏火爆 下载资源如下&#xff1a; win版本&#xff1a;2.3.7 链接&#xff1a;下载地址 提取码&#xff1a;9N3P Mac&#xff08;苹果版本&#xff09;&#xff1a;2.0.0 链接&#xff1a;下载地址 提取码&#xff1a;Bjaa 介绍&#xff…

mysql/doris 计算两个时间相差n天n时n分示范

mysql/doris 计算两个时间相差n天n时n分示范 两个时间&#xff1a;so.create_time&#xff0c;so.update_time CONCAT(FLOOR(DATEDIFF(HOUR ,so.create_time,so.update_time)/24),天,DATEDIFF(HOUR ,so.create_time,so.update_time)%24,时,DATEDIFF(MINUTE ,so.create_time,so…

【重学 MySQL】六十六、外键约束的使用

【重学 MySQL】六十六、外键约束的使用 外键约束的概念关键字主表和从表/父表和子表外键约束的创建条件外键约束的特点外键约束的创建方式外键约束的删除外键约束的约束等级外键约束的级联操作外键约束的示例外键约束的作用开发场景阿里开发规范 在MySQL中&#xff0c;外键约束…

(已解决)vscode使用launch.json进行debug调试报错:Couldn‘t spawn debuggee:embedded null byte

Launch.json 进行debug时报错&#xff1a; 主要原因是vscode全局配置被整乱了&#xff0c;下面是个人解决的方法&#xff0c;以供参考. 在网上也寻找过解决方法&#xff0c;有的说是&#xff0c;在launch.json中&#xff0c;添加一行"python":"/root/miniconda3…

git版本控制软件,操作方法

git版本库操作 1. 注册用户信息 git config --global (邮箱和用户名) 2. 创建工作区 git init 3. 编写文件 vim readme.txt 4. 把文件放到暂存区 git add readme.txt 5. 查看工作区状态 git status 6. 把文件放到本地版本库里 git commit -m "" filename 7. 查看日志…

总结拓展十四:批次管理(2)

1、批次管理后台配置 1.1 批次管理级别配置(T-code:OMTC) ——路径&#xff1a;IMG->后勤-常规->批次管理->指定级别并激活状态管理 1.2 批次状态管理配置(T-code:OMTC) ——路径&#xff1a;IMG->后勤-常规->批次管理->指定级别并激活状态管理 批状态管…

2.1.ReactOS系统NtReadFile函数的实现。

ReactOS系统NtReadFile函数的实现。 ReactOS系统NtReadFile函数的实现。 文章目录 ReactOS系统NtReadFile函数的实现。NtReadFile函数的定义NtReadFile函数的实现 NtReadFile()是windows的一个系统调用&#xff0c;内核中有一个叫NtReadFile的函数 NtReadFile函数的定义 NTS…

【Go初阶】两万字快速入门Go语言

初见golang语法 package mainimport "fmt"func main() {/* 简单的程序 万能的hello world */fmt.Println("Hello Go")} 第一行代码package main定义了包名。你必须在源文件中非注释的第一行指明这个文件属于哪个包&#xff0c;如&#xff1a;package main…

如何捕捉行情爆发的前兆

在金融市场的激烈角逐中&#xff0c;每一次行情的爆发都是投资者获取丰厚回报的关键时刻。然而&#xff0c;如何识别并把握这些时刻&#xff0c;却是一门需要深厚金融专业知识和敏锐洞察力的艺术。今天&#xff0c;我们就来深入探讨行情爆发的初期信号&#xff0c;揭示那些能够…