【JVM】什么是双亲委派机制
文章目录
- 1、类加载机制
- 2、双亲委派模型
- 2.1、介绍
- 2.2、为什么需要双亲委派
- 2.3、源码解析
- 3、破坏双亲委派
- 3.1、介绍
- 3.2、破坏实现
- 3.3、破坏双亲委派的例子
- 4、线程上下文类加载器
1、类加载机制
类加载阶段分为加载、连接、初始化三个阶段,而加载阶段需要通过类的全限定名来获取定义了此类的二进制字节流。Java特意把这一步抽出来用类加载器来实现。把这一步骤抽离出来使得应用程序可以按需自定义类加载器。并且得益于类加载器,OSGI、热部署等领域才得以在JAVA中得到应用。
这一部分可参考之前的文章《【JVM】搞清类加载机制》。
在Java中任意一个类都是由这个类本身和加载这个类的类加载器来确定这个类在JVM中的唯一性。也就是你用A类加载器加载的com.aa.ClassA
和B类加载器加载的com.aa.ClassA
它们是不同的,也就是用instanceof
这种对比都是不同的,所以即使都来自于同一个class文件但是由不同类加载器加载的那就是两个独立的类。
站在Java虚拟机的角度来看,只存在两种不同的类加载器,分别是启动类加载器和其它所有的类加载器。其中启动类加载器使用C++
语言实现,是虚拟机自身的一部分;其它所有的类加载器则都是由Java
语言实现,是独立于虚拟机外部,并且全部都继承自抽象类java.lang.ClassLoader
。
站在Java开发人员的角度来看,类加载器就应该划分得更细致一些。自JDK1.2
以来,Java一直保持着三层类加载器、双亲委派的类加载结构。其中JDK提供的三层类加载器如下:
- 启动类加载器(Boostrap Class Loader):C++语言实现,是虚拟机自身的一部分,负责加载存放在
<JAVA_HOME>\lib
目录,或者被-Xbootclasspath
参数所指定的路径中存放的,而且是Java虚拟机能够识别的。启动类加载器无法被Java程序直接引用,在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器时,直接使用null代替即可;
注意的设计,Java虚拟机是按照文件名识别的,如rt.jar、tools.jar,名字不符合的类库及时放在lib目录中也不会被加载。
-
扩展类加载器(Extension Class Loader):独立于虚拟机外部,于类
sun.misc.launcher$ExtClassLoader
中以Java代码的形式实现,负责加载<JAVA_HOME>\lib\ext
目录,或者被java.ext.dirs
系统变量所指定的路径中所有的类库; -
应用程序类加载器(Application Class Loader):独立于虚拟机外部,由
sun.misc.launcher$AppClassLoader
以Java代码的形式实现,负责加载**用户类路径(ClassPath
)**上所有的类库,在开发过程中可直接使用这个类加载器。如果应用程序中没有自定义过自己的类加载器,一般情况下就是这个程序中默认的类加载器。
2、双亲委派模型
2.1、介绍
介绍完JDK中的三种类加载器,那么就不得不说一下双亲委派模型了。
JDK9之前的Java应用都是由上述三种类加载器互相配合来完成加载的,如果觉得有必要,还可自定义类加载器来对功能进行拓展。
双亲委派模型(Parents Delegation Medel)是指各种类加载器之间的层级关系。该模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。同时这里类加载器之间的父子关系一般是通过组合关系来复用父加载器的代码而非继承。
双亲委派模型核心的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传递到最顶层的启动类加载器中。只有当父加载器反馈自己无法完成这个加载请求,即搜索范围内没有找到所需要的类时,子加载器才会尝试自己去完成加载。
2.2、为什么需要双亲委派
双亲委派保证类加载器,自下而上的委派,又自上而下的加载,保证每一个类在各个类加载器中都是同一个类。
使用双亲委派模型来组织类加载器之前的关系,一个显而易见的好处就是Java中的类随着它的类加载器一起具备了一种带有优先级的层级关系。
一个非常明显的目的就是保证Java官方的类库<JAVA_HOME>\lib
和扩展类库<JAVA_HOME>\lib\ext
的加载安全性,不会被开发者覆盖。
例如类java.lang.Object
,它存放在rt.jar
之中,无论哪个类加载器要加载这个类,最终都是委派给启动类加载器加载,从而保证Object类在程序的各种类加载器环境中从始至终都是同一个类。
如果我们自己开发开源框架,也可以自定义类加载器,利用双亲委派模型,保护自己框架需要加载的类不被应用程序覆盖。
2.3、源码解析
双亲委派模型对于保证Java程序的稳定运作极为重要,但它的实现却异常简单,用以实现双亲委派的代码只有短短十余行,全部集中在java.lang.ClassLoader
的loadClass()
方法之中:
public abstract class ClassLoader {//每个类加载器都有个父加载器private final ClassLoader parent;/*** 双亲委派核心实现*/public Class<?> loadClass(String name) {//查找一下这个类是不是已经加载过了Class<?> c = findLoadedClass(name);//如果没有加载过if( c == null ){//先委派给父加载器去加载,注意这是个递归调用if (parent != null) {c = parent.loadClass(name);}else {// 如果父加载器为空,查找Bootstrap加载器是不是加载过了c = findBootstrapClassOrNull(name);}}// 如果父加载器没加载成功,调用自己的findClass去加载if (c == null) {c = findClass(name);}return c;}protected Class<?> findClass(String name){//1. 根据传入的类名name,到在特定目录下去寻找类文件,把.class文件读入内存...//2. 调用defineClass将字节数组转成Class对象return defineClass(buf, off, len);}// 将字节码数组解析成一个Class对象,用native方法实现protected final Class<?> defineClass(byte[] b, int off, int len){...}
}
从上面的代码可以得到几个关键信息:
- JVM 的类加载器是分层次的,它们有父子关系,而这个关系不是继承维护,而是组合,每个类加载器都持有一个 parent 字段,指向父加载器。
AppClassLoader
的parent
是ExtClassLoader
,ExtClassLoader
的parent
是BootstrapClassLoader
,但是BootstrapClassLoader
的parent=null
。
-
defineClass
方法的职责是调用native
方法把 Java 类的字节码解析成一个 Class 对象。 -
findClass
方法的主要职责就是找到.class
文件并把.class
文件读到内存得到字节码数组,然后调用defineClass
方法得到 Class 对象。子类必须实现findClass
。 -
loadClass
方法的主要职责就是实现双亲委派机制:首先检查这个类是否已经被加载过了,如果加载过了直接返回,否则委派给父加载器加载,这是一个递归调用,一层一层向上委派,最顶层的类加载器(启动类加载器)无法加载该类时,再一层一层向下委派给子类加载器加载。
3、破坏双亲委派
3.1、介绍
双亲委派模型对于保证Java程序的稳定运作极为重要,但它并不是一个具有强制性约束力的模型,而是Java设计者推荐给开发者们的类加载器实现方式,这就代表着双亲委派机制是可以被破坏的。
3.2、破坏实现
前面提及到我们是可以自定义类加载器的,实现的方法也很简单,继承ClassLoader
类并重写相关的方法即可。而当我们想要破坏双亲委派计智时,我们也需要自定义一个类加载器,通过实现自己的类加载逻辑打破原有的加载顺序。
在这里对ClassLoader
类的三个核心方法进行介绍,在前面的源码解析中也有提及:
loadClass()
:主要进行类加载的方法,默认的双亲委派机制就实现在这个方法中;findClass()
:根据名称或位置加载.class字节码;definclass()
:把字节码转化为Class。
接下来将举一个简单的例子,当只有加载String时才会委派给父加载器进行加载,否则通过自定义的类加载逻辑进行加载,从而破坏双亲委派机制。具体实现如下:
public class MyClassLoader extends ClassLoader {@Overridepublic Class<?> loadClass(String name) throws ClassNotFoundException {if (!"java.lang.String".equals(name)) {// 如果类名不是java.lang.String,直接加载类return findClass(name);} else {// 如果类名是java.lang.String,委派给父ClassLoader加载return super.loadClass(name);}}@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {// 实现自己的类加载逻辑// ...}
}
需要注意的是,破坏双亲委派机制可能会导致类加载的混乱和不稳定性,不推荐在正式的应用程序中使用。双亲委派机制的目的是为了保证类的加载是有序的、稳定的,能够避免类的重复加载和冲突。在一些特殊的情况下,可能需要破坏双亲委派机制来实现一些特定的需求,但在大多数情况下,最好遵循双亲委派机制的原则。
3.3、破坏双亲委派的例子
破坏双亲委派的例子有许多,最容易想到的便是自己搞破坏了,还有许多我们是不知道的,如双亲委派机制历史中的三次较大规模“被破坏”的情况、JDBC、OSGI等模块化技术、Tomcat等。
这里简单说一下Tomcat是如何破坏双亲委派机制的。
在Tomcat中,每个Web应用程序都有自己的类加载器,称为Web应用程序类加载器(WebAppClassLoader
)。当Tomcat启动时,会为每个Web应用程序创建一个独立的WebAppClassLoader,它负责加载该Web应用程序的类。
为了保证每个Web项目互相独立,所以不能都由AppClassLoader加载,因此自定义了类加载器WebappClassLoader
,WebappClassLoader
继承自URLClassLoader
,重写了findClass和loadClass
,并且WebappClassLoader
的父类加载器设置为AppClassLoader
。
WebappClassLoader
加载过程如下:
WebappClassLoader.loadClass
中会先在缓存中查看类是否加载过;- 如果已经加载过,直接返回;
- 如果没有加载过,就交给
ExtClassLoader
,ExtClassLoader
再交给BootstrapClassLoader
加载; - 如果都加载不了,才自己加载;
- 如果自己也加载不了,就遵循原始的双亲委派,交由AppClassLoader递归加载。
4、线程上下文类加载器
线程上下文类加载器其实是一种类加载器传递机制。可以通过java.lang.Thread#setContextClassLoader
方法给一个线程设置上下文类加载器,在该线程后续执行过程中就能通过java.lang.Thread#getContextClassLoader
把这个类加载器取出来使用。
如果创建线程时未设置上下文类加载器,将会从父线程(parent = currentThread()
)中获取,如果在应用程序的全局范围内都没有设置过,就默认是应用程序类加载器。
一个典型的例子便是JNDI服务,JNDI现在已经是Java的标准服务,它的代码由启动类加载器去加载(在JDK 1.3时放进去的rt.jar),但JNDI的目的就是对资源进行集中管理和查找,它需要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者(SPI,Service Provider Interface)的代码,但启动类加载器不可能去加载ClassPath下的类。
但是有了线程上下文类加载器就好办了,JNDI服务使用线程上下文类加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载的动作,这种行为实际上就是打破了双亲委派模型的层次结构来逆向使用类加载器,实际上已经违背了双亲委派模型的一般性原则,但这也是无可奈何的事情。
Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI、JDBC、JCE、JAXB和JBI等。
参考:
- 《深入理解Java虚拟机-JVM高级特性与实践》
- 你确定你真的理解"双亲委派"了吗?
- 《深入拆解Tomcat & Jetty》——Tomcat如何打破双亲委托机制?
- 【JVM】搞清类加载机制
相关文章:

【JVM】什么是双亲委派机制
文章目录 1、类加载机制2、双亲委派模型2.1、介绍2.2、为什么需要双亲委派2.3、源码解析 3、破坏双亲委派3.1、介绍3.2、破坏实现3.3、破坏双亲委派的例子 4、线程上下文类加载器 1、类加载机制 类加载阶段分为加载、连接、初始化三个阶段,而加载阶段需要通过类的全…...

网络安全 Day24-select高级用法和多表连接
select高级用法和多表连接 1. select 多子句单表高级实践1.1 select 多子句高级语法1.2 聚合函数1.3 group by 实践1.4 having 筛选1.5 order by 排序1.6 limit 2. 多表连接 1. select 多子句单表高级实践 1.1 select 多子句高级语法 where 和 having 区别是后者是分组后进行…...

JUC并发编程之volatile详解
目录 1. volatile 1.1 volatile关键字的作用 1.1.1 变量可见性 1.1.2 禁止指令重排序 1.2 volatile可见性案例 1.3 volatile非原子性案例 1.4 volatile 禁止重排序 1.5 volatile 日常使用场景 送书活动 1. volatile 在并发编程中,多线程操作共享的变量时&a…...

swing布局详解
1. 布局管理器接口 (1)说明 布局管理器接口为LayoutManager和LayoutManager2,LayoutManager2是LayoutManager的子类。 (2)常用方法 方法描述LayoutManageraddLayoutComponent(String name, Component comp) removeL…...

el-table某一列嵌套使用el-popover,使用click触发,导致页面下拉框组件无法触发弹框关闭(解决办法)
在弹框触发的方法里加上document.body.click() 即可 尝试了很多其他的方法都没用,只有这个解决了 完整代码: <el-select change"sourceChange" clearable ><el-optionv-for"option in list1":key"option.code":…...

正泰电力携手图扑:VR 变电站事故追忆反演
VR(Virtual Reality,虚拟现实)技术作为近年来快速发展的一项新技术,具有广泛的应用前景,支持融合人工智能、机器学习、大数据等技术,实现更加智能化、个性化的应用。在电力能源领域,VR 技术在高性能计算机和专有设备支…...

报错 -bash: wget: command not found
1、报错 -bash: wget: command not found 可以重装 wget 工具: 卸载 wget 工具 yum remove wget下载 wget 工具 yum -y install wget最后尝试 wget “url” 又OK了,一般是原来的wget初始化有文件损坏造成的。...

HashMap扩容和Redis中Dict 扩容
扩容时机: Hash Map:要在某个临界点进行扩容处理,该临界点就是HashMap中元素的数量在数值上等于threshold(table数组长度*加载因子) Dict: 当每次新增键值对的时 , 会检测 负载因子(LoadFactor) , 判断以…...

【Redis】内存数据库Redis进阶(Redis持久化)
目录 分布式缓存 Redis 四大问题Redis 持久化RDB (Redis DataBase)RDB执行时机RDB启动方式——save指令save指令相关配置save指令工作原理save配置自动执行 RDB启动方式——bgsave指令bgsave指令相关配置bgsave指令工作原理 RDB三种启动方式对比RDB特殊启动形式RDB优点与缺点 A…...

在PHP8中检测数据类型-PHP8知识详解
在PHP 8中,可以使用多种方法来检测数据类型。以下是常用的四种方法:使用 gettype() 函数、使用 is_* 系列函数、使用 get_debug_type() 函数、使用 get_class() 函数。 一、使用 gettype() 函数 gettype() 函数返回给定变量的数据类型。例如:…...

amoeba实现MySQL读写分离
amoeba实现MySQL读写分离 准备环境:主机A和主机B作主从配置,IP地址为192.168.131.129和192.168.131.130,主机C作为中间件,也就是作为代理服务器,IP地址为192.168.131.136。三台服务器操作系统为RHEL6.4 x86_64,为…...

angr学习-入门篇
前言: 资源链接:GitHub - jakespringer/angr_ctf(题库仓库,里面有个讲解angr的PPT,里面有官方的题解很详细) GitHub - Hustcw/Angr_Tutorial_For_CTF: angr tutorial for ctf 安装: 关于angr…...

基于java SpringBoot和HTML的博客系统
随着网络技术渗透到社会生活的各个方面,传统的交流方式也面临着变化。互联网是一个非常重要的方向。基于Web技术的网络考试系统可以在全球范围内使用互联网,可以在本地或异地进行通信,大大提高了通信和交换的灵活性。在当今高速发展的互联网时…...

动态sql以及常用的标签
什么是动态sql: 指根据不同的条件生成不同的sql 搭建环境: 建表: create table blog( id varchar(50) not null comment 博客id, title varchar(100) not null comment 博客标题, author varchar(30) not null comment 博客作者, create_ti…...

DID以及社交网络中的ZKP
1. 引言 本文关键术语为: Decentralized Identity (DID,去中心化身份) or self-sovereign identity (SSI,自治身份) :是一个基于开放标准的框架,使用自主、独立的标识符和可验证证书,实现可信的数据交换。…...

基于SWAT-MODFLOW地表水与地下水耦合
耦合模型被应用到很多科学和工程领域来改善模型的性能、效率和结果,SWAT作为一个地表水模型可以较好的模拟主要的水文过程,包括地表径流、降水、蒸发、风速、温度、渗流、侧向径流等,但是对于地下水部分的模拟相对粗糙,考虑到SWAT…...

2023拒绝内卷!两年转行网络安全真实看法!
我目前转行网络安全两年,没啥天分,全靠努力,基本能够得上中级的水平了。看到大家对转行网络安全挺感兴趣,也有挺多争议,想把我的建议和经验告诉大家。 有很多人觉得网络安全已经饱和了,现在选择这个工作&a…...

【SA8295P 源码分析】57 - libDSI_MAX96789_0.so驱动库 之 QDI_Panel_Init 显示屏初始化函数 代码分析
【SA8295P 源码分析】57 - libDSI_MAX96789_0.so驱动库 之 QDI_Panel_Init 显示屏初始化函数 代码分析 一、QDI_Panel_Init() 显示屏初始化函数:Panel_DSI_MAX96789_0_Init()二、QDI_Panel_SetPower() 显示屏初始化:Panel_DSI_MAX96789_0_PowerLCD()三、QDI_Panel_GetInfo() …...

IDEA偶尔编译的时候不识别lombok
偶尔IDEA启动项目的时候会识别不到lombok,识别不到get()跟set()方法 方案 在settings添加下面代码 -Djps.track.ap.dependenciesfalse...

rust学习-构建服务器
单线程server 服务器会依次处理每一个请求,在完成第一个连接的处理之前不会处理第二个连接 // cat main.rs use std::io::prelude::*; use std::net::TcpListener; use std::net::TcpStream;fn main() {let listener TcpListener::bind("127.0.0.1:7878&quo…...

Java并发----进程、线程、并行、并发
一、进程与线程 进程 程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的 当一个程序被运行…...

【计算机网络】第 4 课 - 物理层
欢迎来到博主 Apeiron 的博客,祝您旅程愉快 ! 时止则止,时行则行。动静不失其时,其道光明。 目录 1、物理层的基本概念 2、物理层协议的主要任务 3、物理层任务 4、总结 1、物理层的基本概念 在计算机网络中,用来…...

深入理解MVVM架构模式
MVVM原理 MVVM是一种用于构建用户界面的软件架构模式,它的名称代表着三个组成部分:Model(模型)、View(视图)和ViewModel(视图模型)。MVVM的主要目标是将应用程序的UI与其底层数据模…...

配置IPv6 over IPv4手动隧道示例
组网需求 如图1所示,两台IPv6主机分别通过SwitchA和SwitchC与IPv4骨干网络连接,客户希望两台IPv6主机能通过IPv4骨干网互通。 图1 配置IPv6 over IPv4手动隧道组网图 配置思路 配置IPv6 over IPv4手动隧道的思路如下: 配置IPv4网络。配置接…...

Vue3--->组合式API与Pinia
目录 使用create-vue搭建 1、使用create-vue创建项目 2、项目目录和关键文件 组合式API 1、组合式API - setup选项 2、组合式API - reactive和ref函数 3、组合式API - computed 4、组合式API - watch 1、基础使用 - 侦听单个数据 2、基础使用 - 侦听多个数据 3、immediate&…...

三言两语说透柯里化和反柯里化
JavaScript中的柯里化(Currying)和反柯里化(Uncurrying)是两种很有用的技术,可以帮助我们写出更加优雅、泛用的函数。本文将首先介绍柯里化的概念、实现原理和应用场景,然后介绍反柯里化的概念、实现原理和应用场景,通过大量的代码示例帮助读…...

细讲TCP三次握手四次挥手(四)
常见面试题 为什么TCP连接的时候是3次?2次不可以吗? 因为需要考虑连接时丢包的问题,如果只握手2次,第二次握手时如果服务端发给客户端的确认报文段丢失,此时服务端已经准备好了收发数(可以理解服务端已经连接成功)据…...

HarmonyOS/OpenHarmony元服务开发-配置卡片的配置文件
卡片相关的配置文件主要包含FormExtensionAbility的配置和卡片的配置两部分: 1.卡片需要在module.json5配置文件中的extensionAbilities标签下,配置FormExtensionAbility相关信息。FormExtensionAbility需要填写metadata元信息标签,其中键名称…...

mac安装nacos,M1芯片
第一步,官网下载 》nacos官网 去github中下载对应的版本,本人下载的是1.4.1版本 在这儿选择其他的版本,下面这里选择 tar.gz 压缩包 解压后放到一个非中文的目录下,我选择在 user目录下面创建一个other目录,将使用的环…...

老板说把跳针改过去,什么是主板跳针
最近骑车拍了很多视频,把电脑磁盘堆满了,想着买一条固态SSD卡扩展一下。 一咬牙一跺脚,直接安排,毫不犹豫。顺带加装了无限网卡和蓝牙5.2。 收到后立马安装。安装完发现识别不到新磁盘 确认安装没问题。然后就去问固态硬盘的客服 …...