JVM 类加载器如何保证核心类库不被覆盖?


一则或许对你有用的小广告

欢迎加入小哈的星球,你将获得:专属的实战项目(4个项目都能学) / 1v1 提问 / 简历修改 / Java 学习路线 / 社群讨论 / 学习打卡 / 每月赠书

  • 《Spring AI 项目实战(问答机器人、RAG 智能客服、联网搜索)》已完结,基于 Spring AI + Spring Boot 3.x + JDK 21...查看介绍

  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...查看介绍;演示链接:http://116.62.199.48:7070/

  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接:http://116.62.199.48/

  • 新开坑项目:《从零手撸:秒杀系统高并发优化实战》 正在更新中...,查看介绍

截止目前,星球内专栏累计输出 150w+ 字,讲解图 5110+ 张,还在持续爆肝中.. 后续还会上新更多项目,已有 4700+ 小伙伴加入学习,欢迎点击围观

面试考察点

  1. 双亲委派机制理解:面试官不仅仅是想知道你听过 "双亲委派" 这四个字,更是想知道你是否清楚完整的委派流程——从子加载器到父加载器,层层往上,每一层干了什么。

  2. 安全性思维:考察你是否理解类加载器的安全属性——不只是委派,JVM 在 ClassLoader 实现层面也做了保护,防止恶意代码伪装核心类。

  3. 边界意识:是否知道双亲委派不是铁板一块,有哪些场景会打破它(SPI、OSGi、热部署等),以及打破之后会带来什么问题。

核心答案

JVM 通过 3 层防线 保证核心类库不被覆盖:

防线 机制 作用
第一层 双亲委派机制 加载类时先委托父加载器,父加载器能加载就不自己加载
第二层 ClassLoader 源码保护 loadClass() 方法优先检查父加载器,且核心类库由 BootstrapClassLoader 独占
第三层 SecurityManager + 包名校验 禁止自定义类使用 java. 开头的包名,防止伪装核心类

深度解析

一、类加载器的层级关系

先搞清楚 Java 里的类加载器是怎么分层级的,这是理解双亲委派的前提:

上图展示了 JVM 类加载器的四层结构。有几个关键点:

  • BootstrapClassLoader 是最顶层的,由 C++ 实现(所以你在 Java 代码里打印它的 getClass() 会得到 null),它负责加载 JVM 运行所需的最核心的类,比如 java.lang.Objectjava.lang.String 这些。
  • 各加载器之间是 组合关系,不是继承关系。每个 ClassLoader 内部都有一个 parent 字段指向父加载器。
  • 加载的范围是严格分开的:Bootstraplib 核心,Extensionext 扩展,Applicationclasspath 应用。

二、双亲委派——核心保护机制

这是保证核心类库不被覆盖的 最核心机制

双亲委派的核心逻辑就一句话:先问爸爸能不能加载,爸爸搞不定再自己来

上面是加载 java.lang.String 的完整流程。即使你在自己的项目里写了一个 java.lang.String 类,ApplicationClassLoader 也不会加载它,因为请求最终会委派到 BootstrapClassLoader,由它加载 JDK 自带的版本。

用代码验证一下:

public class StringTest {
    public static void main(String[] args) {
        // 自己写一个 java.lang.String 放到 classpath 下
        // 打印看看加载的是哪个 String
        String s = new String("hello");
        System.out.println(s.getClass().getClassLoader());
        // 输出: null
        // null 表示是 BootstrapClassLoader 加载的(JDK 自带的)
        // 而不是你的 ApplicationClassLoader 加载的伪造版本
    }
}

三、ClassLoader 源码层面的保护

双亲委派不只是"约定",在 ClassLoader 的源码里写死了。来看 java.lang.ClassLoader.loadClass() 的核心逻辑(JDK 8):

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException {
    // ① 加锁,保证线程安全
    synchronized (getClassLoadingLock(name)) {
        // ② 先检查是否已经被加载过(每个 ClassLoader 有自己的缓存)
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            try {
                if (parent != null) {
                    // ③ 有父加载器 → 委托给父加载器加载
                    c = parent.loadClass(name, false);
                } else {
                    // ④ 没有父加载器 → 说明到了最顶层,委托给 Bootstrap
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // 父加载器加载失败,不处理,继续往下
            }

            if (c == null) {
                // ⑤ 父加载器都搞不定,自己尝试加载
                c = findClass(name);
            }
        }
        return c;
    }
}

关键步骤:

  • ② 先查缓存:每个类加载器内部有一个 classes 缓存(底层是个 HashMap),如果之前加载过这个类,直接返回。这保证了同一个类不会被重复加载。
  • ③ 委托父加载器parent.loadClass(name, false),递归地向上委派。
  • ⑤ 自己加载:只有父加载器全都 ClassNotFoundException 之后,才会调用自己的 findClass()

这意味着什么?就算你自定义一个 ClassLoader,重写 findClass() 方法,试图加载一个 java.lang.String——也没用,因为这个请求在步骤 ③ 就已经被父加载器拦截了。

四、包名校验——最后一道保险

就算有人绕过了双亲委派(比如自定义 ClassLoader 重写 loadClass()),JVM 还有最后一道防线:包名校验

// 假设你写了一个恶意的自定义 ClassLoader,绕过双亲委派
public class EvilClassLoader extends ClassLoader {
    @Override
    public Class<?> loadClass(String name) {
        // 直接自己加载,不委托给 parent!
        return findClass(name);  // 试图加载自己定义的 java.lang.String
    }
}

JVM 在类加载的 验证阶段 会检查:如果类名以 java. 开头,必须由 BootstrapClassLoader 加载。否则直接抛出 SecurityException

java.lang.SecurityException: Prohibited package name: java.lang

这个校验在 ClassLoaderdefineClass() 方法中实现:

// java.lang.ClassLoader#defineClass() 源码(简化)
protected final Class<?> defineClass(String name, byte[] b, ...) {
    // 检查包名
    if (checkName(name) && name.startsWith("java.")) {
        // java. 开头的包名,只允许 BootstrapClassLoader 定义
        // 其他 ClassLoader 尝试定义直接抛 SecurityException
        throw new SecurityException("Prohibited package name: " + name);
    }
    // ...
}

所以,即使你绕过了双亲委派,也过不了这一关。

五、打破双亲委派的场景

双亲委派是个好设计,但不是没有例外。面试官很可能追问这个:

场景 怎么打破的 为什么需要打破
SPI(ServiceLoader) 线程上下文类加载器(Thread ContextClassLoader BootstrapClassLoader 看不到 classpath 的实现类
Tomcat 每个 WebApp 有独立的 ClassLoader,优先自己加载 不同应用可能依赖同一个库的不同版本
OSGi 模块化的类加载器网状结构 模块间热部署、动态加载
热部署(热加载) 每次加载创建新的 ClassLoader 实例 丢弃旧的 ClassLoader 让旧类被 GC 回收

其中 SPI 是最经典的例子。JDBC 的接口(java.sql.Driver)在 rt.jar 里,由 BootstrapClassLoader 加载。但具体实现(比如 MySQL Driver)在 classpath 下,BootstrapClassLoader 根本看不到 classpath。怎么解决?

// java.sql.DriverManager 使用线程上下文类加载器加载 SPI 实现
// 线程上下文类加载器默认是 ApplicationClassLoader
ClassLoader cl = Thread.currentThread().getContextClassLoader();
// 用 ApplicationClassLoader 去加载 classpath 下的 MySQL Driver
Class<?> driverClass = cl.loadClass("com.mysql.cj.jdbc.Driver");

这就是"父加载器"反过来委托"子加载器"的典型案例。JDBC 这个设计确实有点无奈,但也是 JDK 6 引入 SPI 机制后最优雅的解法了。

六、JDK 9+ 模块化的变化

JDK 9 引入了模块化系统(JPMS),类加载器有了变化:

  • ExtensionClassLoader 改名为 PlatformClassLoader
  • BootstrapClassLoader 不再只加载 rt.jar(因为 rt.jar 没了),改为加载平台模块
  • 模块的 module-info.java 中可以声明哪些包对外暴露(exports),哪些只在模块内可见
  • 新增了 Module Layer 机制,提供了比双亲委派更灵活的类加载策略

不过双亲委派的核心思想没有变,只是在模块化层面多了一层封装和控制。

面试高频追问

  1. 追问一:能不能自己写一个 java.lang.String 替换 JDK 的?

    不能。三层防线都挡着:双亲委派会让 BootstrapClassLoader 先加载 JDK 自带的版本;即使绕过双亲委派,defineClass() 会抛 SecurityException: Prohibited package name: java.lang;如果还想绕,SecurityManager 也会拦截。

  2. 追问二:双亲委派有什么缺点?

    最大的问题是 灵活性不足。比如上面说的 SPI 场景,父加载器加载的接口需要用到子加载器才能看到的实现类,双亲委派天然不支持这种 "向下委托",只能用线程上下文类加载器这种比较 hack 的方式绕过。

  3. 追问三:Tomcat 是怎么打破双亲委派的?

    Tomcat 的每个 Web 应用都有独立的 WebAppClassLoader,它加载类时的顺序是:先检查是否已加载 → 自己的 WEB-INF/classesWEB-INF/lib → 委托给父加载器。和标准双亲委派 相反,是先自己加载,加载不到再委托给父。这样不同应用可以用同一个库的不同版本,互不干扰。

常见面试变体

  • "什么是双亲委派模型?为什么要这样设计?"
  • "如何破坏双亲委派机制?有哪些实际场景?"
  • "Tomcat 的类加载器是怎么设计的?"
  • "ClassLoaderloadClass()findClass() 有什么区别?"

记忆口诀

三层防线:双亲委派(先问爹)→ 源码保护(loadClass() 写死了委派逻辑)→ 包名校验(java. 包名禁止自定义加载)。一个例外:SPI 用线程上下文类加载器打破委派。

总结

JVM 保护核心类库不被覆盖,靠的是 双亲委派 + defineClass() 包名校验 + SecurityManager 三层防线。其中双亲委派是核心:所有类加载请求先向上委托到 BootstrapClassLoader,核心类由它独占加载,下层加载器没有机会覆盖。面试时把这个机制讲清楚,再提一下 SPI 和 Tomcat 打破双亲委派的场景,基本满分。