⭐ 推荐:社区规则条款 V1.0

如何理解 Java 的类加载机制和双亲委派模型?

liulian
liulian 初级会员超兽战士
发布于 2026-10-12 19:28 ·1 浏览 ·0 回复
内容摘要

梳理 Java 类加载的触发时机、加载到初始化的五个阶段及类加载器分层,并解释双亲委派模型如何通过父加载器优先避免核心类被篡改,从而说明自定义 java.lang.String 不会被加载。

学完这篇,你能说清 Java 类加载的五个阶段各做什么、双亲委派模型解决了什么问题,以及为什么"自己写一个 java.lang.String"不会被加载。

第一步:先搞清类加载在什么时机发生

类加载不是一个动作,而是一段流程。JVM 把 .class 文件读进内存、变成可用的 Class 对象,这个过程叫类加载。

它被触发的时机主要有三种:遇到 new、getstatic、putstatic、invokestatic 这类指令时;反射调用时(Class.forName);初始化子类时发现父类还没初始化。注意,加载 ≠ 初始化,加载只是流程里的第一步。

从 JDK 9 开始,加载的触发还多了一个来源:模块系统(JPMS)里 module-info.class 描述的可读性关系。

第二步:记住类加载的五个阶段

一个类的完整生命周期是:加载 → 验证 → 准备 → 解析 → 初始化,之后才是使用和卸载。

  • 加载:通过类的全限定名找到二进制字节流,可以是磁盘上的 .class、jar 包里的条目、网络传输的字节,甚至运行时动态生成的字节码(比如 JDK 8 的 Unsafe.defineClass)。
  • 验证:检查字节码格式、语义、字节码指令是否合法,防止恶意代码破坏 JVM。这是最耗时但很关键的一步,可以用 -Xverify:none 关闭,但生产环境不要关。
  • 准备:给静态变量分配内存并赋零值。static int a = 10; 在这一步 a 是 0,不是 10。
  • 解析:把常量池里的符号引用替换成直接引用。
  • 初始化:执行 <clinit>() 方法,也就是静态变量赋值语句和静态代码块。到这一步 a 才变成 10。

注意:准备阶段赋零值、初始化阶段赋真实值,这两步经常被混为一谈,面试题里也爱考。另外,static final 的编译期常量(如 static final int b = 20;)在准备阶段就被赋成 20 了,因为它被标记为 ConstantValue。

第三步:理解"类加载器"是什么

类加载器(ClassLoader)就是负责"加载"这一步的组件,它的核心方法是 loadClass(String name, boolean resolve)。

JDK 8 及以前,主要有三层:

  1. 启动类加载器(Bootstrap ClassLoader):C++ 实现,加载 <JAVA_HOME>/lib 下的核心类,比如 rt.jar。它没有对应的 Java 对象,String.class.getClassLoader() 返回 null。
  2. 扩展类加载器(Extension ClassLoader):Java 实现,加载 <JAVA_HOME>/lib/ext 目录。
  3. 应用程序类加载器(Application ClassLoader):加载 -classpath 指定的路径,也就是你项目里的类。

JDK 9 之后,扩展类加载器被平台类加载器(Platform ClassLoader)取代,加载的是 <JAVA_HOME>/lib 下的模块,rt.jar 也被拆成了 java.base 等模块。如果你在 JDK 9+ 打印 ClassLoader.getSystemClassLoader().getParent(),看到的是 jdk.internal.loader.ClassLoaders$PlatformClassLoader。

第四步:双亲委派模型到底委派了什么

双亲委派模型的逻辑就一句话:**收到加载请求时,先让父加载器去加载;父加载器加载不了,自己才尝试加载。**

源码里就是 java.lang.ClassLoader#loadClass 那段,简化后是:

protected Class<?> loadClass(String name, boolean resolve) {
    synchronized (getClassLoadingLock(name)) {
        // 1. 先看自己有没有加载过
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            try {
                if (parent != null) {
                    c = parent.loadClass(name, false); // 2. 委派给父
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
            }
            if (c == null) {
                c = findClass(name); // 3. 父加载不了,自己加载
            }
        }
        if (resolve) resolveClass(c);
        return c;
    }
}

它带来两个好处:避免类重复加载(同一个类由父加载器加载一次就够了),以及保证核心类库安全。你写一个 java.lang.String,请求会一路委派到启动类加载器,最终加载的是 JDK 自带的 String,你的那个永远不会被加载。

注意:双亲委派是"推荐模型",不是强制约束。它靠的是 loadClass 的默认实现,你完全可以重写 loadClass 打破它。

第五步:知道什么时候需要打破双亲委派

三种典型场景:

  • JDBC:java.sql.Driver 在 rt.jar 里,由启动类加载器加载;但 com.mysql.cj.jdbc.Driver 在 classpath 上,启动类加载器看不到。于是 DriverManager 用 线程上下文类加载器(Thread.currentThread().getContextClassLoader())反过来去加载子加载器能看到的类。
  • Tomcat:每个 webapp 一个 WebappClassLoader,先自己加载应用类(WEB-INF/classes),加载不到才委派给父,实现应用间隔离。
  • 热部署 / OSGi:通过自定义加载器重复加载同名类的新版本。

写自定义加载器时,通常重写 findClass 而不是 loadClass,这样能保留双亲委派的整体逻辑。

小结

  • 类加载流程:加载 → 验证 → 准备 → 解析 → 初始化,准备阶段赋零值,初始化阶段赋真实值。
  • 三层加载器:Bootstrap(null)、Platform(JDK 9+,原 Extension)、Application(classpath)。
  • 双亲委派的核心是 loadClass 里"先父后子"的顺序,作用是防重复加载和防核心类被替换。
  • 打破委派的常见手段:重写 loadClass、使用线程上下文类加载器。
  • 自定义加载器优先重写 findClass,别轻易动 loadClass。
版权声明:本文来自 GJ站长论坛《如何理解 Java 的类加载机制和双亲委派模型?》
原文链接:https://www.gj0.com/thread-1797.html
转载请注明出处并保留本声明;内容仅代表作者观点,与本站立场无关。若本文涉嫌侵权,请联系本站处理。

全部回复 0

还没有回复,来抢沙发~