JVM 遇到的问题-1.1

双亲委派

一、什么是双亲委派

双亲委派 :当一个类加载器收到类加载请求时,不会自己先去加载,而是把请求向上委托给父类加载器去完成,一直委托到顶层启动类加载器;如果父加载器加载失败,子类加载器才尝试自己加载。

|-----------------------------------------|
| 注意:不是父子继承关系,是组合关系 ,parent 字段指向父加载器。 |

类加载器层级(JDK8)

  1. 启动类加载器 (Bootstrap ClassLoader) :C++ 实现,加载jre/lib核心 rt.jar 等
  2. 扩展类加载器 (Extension ClassLoader) :Java 实现,加载jre/lib/ext
  3. 应用程序类加载器 (AppClassLoader):加载项目 classpath 下我们自己写的类
  4. 自定义类加载器:用户自己实现 ClassLoader

二、双亲委派完整工作流程

  1. 应用类加载器 收到加载com.xxx.Demo请求;
  2. 把请求委托给它的父加载器 ------ 扩展类加载器
  3. 扩展类加载器 继续向上委托给启动类加载器
  4. 启动类加载器在自己的加载路径查找该类:
  • ✅找到:直接加载,返回 Class 对象;
  • ❌找不到:向下回退,交给下一层子加载器;
  1. 扩展类加载器在自己路径查找,找不到继续回退;
  2. 应用程序类加载器在 classpath 查找,找到则加载;
  3. 如果全部都找不到,抛出 ClassNotFoundException。

核心源码逻辑(ClassLoader#loadClass)

|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| java protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 1.检查该类是否已经加载过 Class<?> c = findLoadedClass(name); if (c == null) { try { // 2.有父加载器,则向上委派 if (parent != null) { c = parent.loadClass(name, false); } else { // parent为null代表父是Bootstrap启动类加载器 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { //父加载器找不到,捕获异常继续往下执行 } if (c == null) { //3.父都加载失败,才调用findClass由当前加载器自己实现加载 c = findClass(name); } } if (resolve) { resolveClass(c); } return c; } } |

双亲委派的好处

  1. 沙箱安全 :防止篡改核心类。比如自己写java.lang.String,会被启动类加载器优先加载系统原生 String,不会加载恶意自定义版本。
  2. 类的唯一性:同一个全限定类名,保证只被同一个类加载器加载一次,避免重复加载。

三、打破双亲委派模型

双亲委派是约定,不是强制语法

|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| JDK8:重写 loadClass() 方法就可以破坏; JDK9~ JDK17: * JDK9 引入 JPMS 模块系统,没有废除双亲委派,只是增加强封装约束;loadClass 依旧可以重写实现打破双亲委派。 * 最大变化:禁止自定义加载器加载 JDK 核心类(java.*),即使重写 loadClass 也不行 ;JDK17 彻底关闭非法访问,动态类需要--add‑opens或者使用 Lookup.defineClass。 * JDK17 打破双亲委派 3 种途径: * ①重写loadClass(),业务类优先自己加载,系统包必须委派父加载器; * ②线程上下文类加载器 TCCL,SPI 逆向委派,JDBC 沿用; * ③新 API ModuleLayer(官方模块隔离); * Tomcat10 + 依然重写 loadClass,只是增加系统包过滤适配 JPMS。 * 区分:重写findClass不破坏双亲委派;重写 loadClass 修改加载顺序,才是真正打破。 |

破坏的本质

不向上委托,当前类加载器优先自己加载类,不再先走父加载器。

方式

  1. 重写 ClassLoader 的 loadClass(),改写委派逻辑,不再向上委托。

|-----------------------------------------------------------------------|
| 注意:不要重写 findClass,findClass 是留给子类实现自己读取字节码的,原有 loadClass 才是实现双亲委派的逻辑。 |

四、典型打破双亲委派场景

场景 1:SPI 服务提供者接口(JDBC 驱动)

JDBC Driver 接口属于 rt.jar,由启动类加载器 加载。

但是各个数据库厂商驱动(mysql-connector)是第三方 jar,在 classpath,应用类加载器加载。

问题:启动类加载器看不到 classpath 下 mysql 驱动 jar,是怎么解决加载的?

解决方案:线程上下文类加载器 Thread.getContextClassLoader () ,拿到应用类加载器,反向去加载驱动类,父加载器委托子加载器加载类,逆向委派,打破双亲委派

流程简述:

  1. DriverManager 是核心类,Bootstrap 加载;
  2. Bootstrap 类加载器看不到 mysql 驱动;
  3. 使用线程上下文类加载器(AppClassLoader),去加载各个数据库实现的 Driver;

|---------------------------------------------------|
| 这就是父加载器使用子加载器完成类加载,逆向委派 ,是 JDK 原生打破双亲委派最经典案例。 |

场景 2:热加载 / 热部署

比如 Tomcat:

  • 每个 web 应用有自己的 WebappClassLoader;
  • 同一个类名,不同 web 应用需要各自独立版本;
  • 如果使用双亲委派,父加载器加载一次之后所有应用共用同一个 Class;
  • Tomcat 自定义类加载器,优先自己 web 目录加载 class,再交给父加载器,破坏双亲委派;
  • 同时支持热部署:丢弃旧类加载器,新建类加载器重新加载 class。

|--------------------------------------------------------------------------------------------------------------------------------------------|
| Tomcat 类加载:CommonClassLoader → CatalinaClassLoader → SharedClassLoader → WebAppClassLoader,WebAppClassLoader 优先加载 WEB‑INF/classes,再委托父加载器。 |

场景 3:OSGi 模块化框架

OSGi 实现热插拔模块,每个 Bundle 有独立类加载器;

Bundle 之间互相导入导出类,类加载在各个 Bundle 加载器之间来回交互,完全不遵循双亲委派,实现模块动态安装卸载。

场景 4:自定义热部署框架

比如开发工具 JRebel,自己实现 ClassLoader,重写 loadClass,优先读取修改后的 class 字节码,实现不重启更新代码。

场景 5:代码中自定义 ClassLoader 重写 loadClass

自己继承 ClassLoader,重写loadClass,不调用 super.loadClass,直接读取字节码调用 defineClass,完全不走向上委派逻辑。

总结:

  1. 双亲委派流程:收到请求→委托父加载器,直到 Bootstrap;父找不到,再自己 findClass;
  2. 好处:安全防护、保证类唯一;
  3. 打破双亲委派的本质:重写loadClass,不再向上委托;
  4. 典型场景:
  • JDBC:线程上下文类加载器,父加载器调用子加载器逆向委派
  • Tomcat WebappClassLoader,每个 web 应用独立类隔离,优先本地加载;
  • OSGi 模块化;

热部署、热加载自定义类加载器。

相关推荐
SKH.1 小时前
Linux软件编程(5)线程
java·linux·jvm
liangbo74 小时前
JVM规范第 2章:从 class 文件到运行时数据区
java·jvm
沐苏瑶6 小时前
JVM-内存区域、垃圾回收与类加载机制
java·jvm
Mr. zhihao18 小时前
死锁排查实战:JVM 唯一会“自动报案“的问题(场景 B5)
java·jvm·gc
Mr. zhihao1 天前
OOM 排查实战(大结局):从老年代爬坡到 VisualVM 引用链实锤(场景 B4)
jvm·gc·oom·mat·visulvm
H_老邪1 天前
JVM 遇到的问题-1.0
jvm
liangbo71 天前
JVM规范第 4 章:class 文件格式
java·jvm
夕除1 天前
redis--008
java·jvm·数据库
liangbo71 天前
JVM规范第 1章:从一段历史到「JVM 并不认识 Java」
java·jvm