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 类加载的本质。

相关推荐
程序员敲代码吗2 小时前
持续集成/持续部署(CI/CD) for Python
jvm·数据库·python
哈哈不让取名字2 小时前
使用Fabric自动化你的部署流程
jvm·数据库·python
阿崽meitoufa18 小时前
JVM虚拟机:运行时数据区域总览
jvm
江君是实在人18 小时前
java jvm 调优
java·开发语言·jvm
阿崽meitoufa19 小时前
JVM虚拟机:垃圾收集算法
java·jvm·算法
程序员敲代码吗20 小时前
使用Python进行PDF文件的处理与操作
jvm·数据库·python
佛祖让我来巡山20 小时前
【面试题】为什么 Java 8 移除了永久代(PermGen)并引入了元空间(Metaspace)?
jvm·元空间·永久代
风送雨1 天前
FastAPI 学习教程 · 第5部分
jvm·学习·fastapi
程序员敲代码吗1 天前
如何从Python初学者进阶为专家?
jvm·数据库·python