JVM篇3:双亲委派机制底层原理

作为 Java 开发者,我们每天都在写 class,每天都在 new 对象。但你是否想过,硬盘上那个静静躺着的 .class 字节码文件,究竟是如何变成内存中活生生的对象的?

这一切的幕后推手,就是 类加载器 (ClassLoader) 。而面试中最高频的考点------双亲委派机制,则是它行事的最高准则。

今天我们就来拆解一下这个机制,看看它是如何工作的,为什么要这么设计,以及 Tomcat 是如何"叛逆"地打破它的。

一、 什么是类加载器?

简单来说,类加载器的作用就是将字节码文件加载到 JVM 内存中

在 JVM 的世界里,类加载器有着森严的等级制度。主要分为以下几类(按优先级从高到低):

  1. 启动类加载器 (Bootstrap ClassLoader)

    • 身份:幕后的大 Boss。

    • 来源:它不是 Java 类,而是由 C/C++ 实现的,嵌在 JVM 内核中。

    • 职责 :负责加载 Java 的核心类库,即 JAVA_HOME/jre/lib 目录下的类(比如 rt.jar 中的 String, Object 等)。

    • 特点:Java 代码无法直接访问它。

  2. 扩展类加载器 (Extension ClassLoader)

    • 身份:核心部门经理。

    • 来源:Java 实现。上级是 Bootstrap。

    • 职责 :负责加载 JAVA_HOME/jre/lib/ext 目录下的扩展类库。

  3. 应用程序类加载器 (Application ClassLoader)

    • 身份:干活的普通员工。

    • 来源:Java 实现。上级是 Extension。

    • 职责 :负责加载 classpath 目录下(即我们自己写的代码和引入的第三方 jar 包)的类。

  4. 自定义类加载器 (Custom ClassLoader)

    • 身份:特种部队/外包团队。

    • 职责:根据特殊需求(如类隔离、热部署、加载加密 class)自定义加载规则。上级通常是 Application。


二、 什么是双亲委派机制?

了解了等级制度,我们再来看看它们是如何协同工作的。

双亲委派机制 指明了类加载器在加载类时的行为准则。

1. 工作过程(推卸责任流)

当一个类加载器收到了类加载的请求时,它的动作如下:

  1. 不自己干:它首先不会自己去尝试加载这个类。

  2. 找上级 :它会把这个请求委派给父类加载器去完成。

  3. 层层传递 :每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的 Bootstrap ClassLoader 中。

  4. 兜底处理:只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

注意:这里的"父子关系"通常不是通过 Java 的类继承(Inheritance)实现的,而是通过组合(Composition)复用父加载器的代码。

2. 为什么要这么设计?(核心好处)

这种"层层上报"的机制看似麻烦,实则为了解决两个核心问题:

  • 保证类的安全性(沙箱安全机制)

    • 防止核心类被篡改。假设黑客自定义了一个 java.lang.String 类并试图加载。如果没有双亲委派,这个类可能被 AppClassLoader 加载,从而替换掉 JDK 原生的 String 类,导致系统崩溃或泄露信息。

    • 有了双亲委派,请求最终会传给 Bootstrap,Bootstrap 发现它已经加载过核心的 String 类了,就会直接返回,黑客的伪造类根本没有机会被加载。

  • 保证类的唯一性

    • 避免重复加载。当父加载器已经加载了该类时,子加载器就没必要再加载一次。保证内存中只有一个 java.lang.Object 对象。

三、 类加载器体系:等级森严的"职场架构"(非重点)

在 JVM 中,类加载器的作用是将硬盘上的 .class 字节码文件加载到内存中。JVM 设计了一套严格的层级架构,主要包含三个核心的类加载器。

要理解它们,我们必须先理清一个核心概念:它们在"血缘"上(代码继承)和"职位"上(双亲委派)是两套完全不同的关系。

1. 三大核心类加载器

(1) 启动类加载器 (Bootstrap ClassLoader) ------ "幕后的大 Boss"
  • 职责:负责加载 Java 最核心的类库。

    • 加载路径:JAVA_HOME/jre/lib(例如 rt.jar,包含 java.lang.Stringjava.util.List 等)。
  • 实现语言C/C++

    • 它是 JVM 内核的一部分,不是 Java 类
  • 特点 :因为它是 C++ 写的,Java 代码无法直接获取到它。如果你尝试打印 String.class.getClassLoader(),结果是 null

(2) 扩展类加载器 (Extension ClassLoader) ------ "部门经理"
  • 职责:负责加载扩展功能的类库。

    • 加载路径:JAVA_HOME/jre/lib/ext
  • 实现语言Java

    • 它是 sun.misc.Launcher$ExtClassLoader 类的实例。
  • 上级:Bootstrap ClassLoader。

(3) 应用程序类加载器 (Application ClassLoader) ------ "普通员工"
  • 职责:负责加载用户代码和第三方库。

    • 加载路径:classpath(即我们项目中的 class 文件和 Maven 引入的 jar 包)。
  • 实现语言Java

    • 它是 sun.misc.Launcher$AppClassLoader 类的实例。
  • 上级:Extension ClassLoader。

2. 核心难点:继承关系 vs 委派关系

这是最容易混淆的地方。请看下图,左边是代码层面的"继承",右边是运行时的"委派"。

区别一:谁是 Java 类?谁不是?
  • AppClassLoaderExtClassLoader 都是 Java 类 。它们在代码中都继承java.lang.ClassLoader(这个抽象类定义了 loadClass 方法)。

  • Bootstrap ClassLoaderC++ 程序 。它没有继承 java.lang.ClassLoader(因为 C++ 无法继承 Java 类)。它是 JVM 自带的底层加载机制。

区别二:父子关系的本质

我们在双亲委派中说的"父加载器(Parent)",指的不是 类继承(Extends)关系,而是组合(Composition)关系

  • 代码层面(继承) : AppClassLoader 和 ExtClassLoader 是兄弟关系,它们都继承自 URLClassLoader,最终继承自 java.lang.ClassLoader

  • 逻辑层面(委派): JVM 在启动时,通过代码将它们组装成上下级:

    • AppClassLoader 的 parent 属性被设置为 ExtClassLoader。

    • ExtClassLoader 的 parent 属性被设置为 null(因为 Bootstrap 是 C++ 写的,Java 引用不到,所以用 null 代表它)。

3. 自定义类加载器 (Custom ClassLoader)

除了 JVM 自带的三个,我们还可以根据需求实现自定义类加载器

  • 实现方式 :编写一个 Java 类,继承 java.lang.ClassLoader

  • 应用场景

    • 类隔离:如 Tomcat,为了隔离不同 Web 应用的同名类。

    • 加密保护:加载加密过的 class 文件,在内存中解密。

    • 热部署:不重启服务的情况下替换类。

  • 层级位置:通常挂载在 Application ClassLoader 之下。


四、 破坏双亲委派:Tomcat 的"叛逆"

虽然双亲委派是标准推荐的规则,但在某些特定场景下,我们必须打破它。最经典的案例就是 Tomcat

1. 为什么要破坏?

双亲委派有一个致命的"缺点":它默认全类名(包名+类名)一样,就是同一个类。

但在 Tomcat 这种 Web 容器里,这行不通。

  • 场景

    • 你的 WebApp A 依赖 Spring 4.0

    • 你的 WebApp B 依赖 Spring 5.0

    • 这两个 jar 包里的类名都叫 org.springframework.context.ApplicationContext

  • 冲突: 如果遵循双亲委派,AppClassLoader 加载了 Spring 4 的类,那么 App B 想要用 Spring 5 的时候,加载器一看名字一样,直接把 4 的给它了,App B 瞬间报错。

  • 需求 :我们需要 类隔离

2. 如何破坏?

在 Java 代码中,双亲委派的逻辑写在 java.lang.ClassLoaderloadClass() 方法中:

原生逻辑:先检查 loaded -> parent.loadClass() -> findClass()

要破坏双亲委派,只需要自定义类加载器,重写 loadClass() 方法,强制让他从0层转移到以-1层作为起点。把"先找父类"的逻辑去掉,改成"先找自己"。

3. Tomcat 的解决方案

Tomcat 为每个 Web 应用创建了一个独立的 WebappClassLoader

  • WebappClassLoader (隔离)

    • 针对 /WEB-INF/classes/WEB-INF/lib 下的类。

    • 破坏规则 :加载请求来了,先自己尝试加载。自己找不到,再遵循双亲委派去找父加载器(加载 JDK 核心类)。

    • 结果:WebApp A 用自己的加载器加载 Spring 4,WebApp B 用自己的加载器加载 Spring 5。JVM 认为它们是不同的类,互不干扰。

  • SharedClassLoader (共享)

    • 如果有多个应用版本一致(比如都用 Spring 5),为了节省内存,避免重复加载。

    • Tomcat 提供了 SharedClassLoader,让它去加载指定目录下的共享 jar 包。这一部分是遵循双亲委派的。

四、 总结

  • 类加载器 负责将字节码搬运到内存。

  • 双亲委派 是"向上委托,向下加载"的机制,核心目的是安全防重复

  • 破坏双亲委派 并不全是坏事,在需要类隔离 (如 Tomcat)或热部署 的场景下,我们需要通过自定义类加载器重写 loadClass 来实现"优先加载自己"。

理解了这些,你就真正看懂了 Java 类加载的本质。

相关推荐
野犬寒鸦28 分钟前
从零起步学习并发编程 || 第七章:ThreadLocal深层解析及常见问题解决方案
java·服务器·开发语言·jvm·后端·学习
闻哥3 小时前
Kafka高吞吐量核心揭秘:四大技术架构深度解析
java·jvm·面试·kafka·rabbitmq·springboot
星辰_mya4 小时前
Elasticsearch线上问题之慢查询
java·开发语言·jvm
蓝帆傲亦4 小时前
代码革命!我用Claude Code 3个月完成1年工作量,这些实战经验全给你
jvm·数据库·oracle
Codiggerworld17 小时前
JVM内存模型——你的对象住在哪里?
jvm
马猴烧酒.19 小时前
【面试八股|JVM虚拟机】JVM虚拟机常考面试题详解
jvm·面试·职场和发展
2301_7903009620 小时前
Python数据库操作:SQLAlchemy ORM指南
jvm·数据库·python
m0_7369191021 小时前
用Pandas处理时间序列数据(Time Series)
jvm·数据库·python
_F_y21 小时前
C++重点知识总结
java·jvm·c++
爱学习的阿磊1 天前
使用Fabric自动化你的部署流程
jvm·数据库·python