← 返回文章

类加载机制

发布于 2026/10/19

1. 完整回答

Java 的类加载机制,是指 JVM 在程序运行过程中,把类的字节码加载到内存,并转换成可以使用的运行时类型的过程。它支持按需加载和动态加载,让 Java 程序能够在运行时引入新的类,常见于插件系统、应用容器和动态扩展场景。

整个过程主要分为三个阶段:加载、链接和初始化。

首先是加载,JVM 通过类加载器获取类的字节码,在方法区中建立类的元数据,并生成对应的 Class 对象。

接着是链接,包含验证、准备和解析。验证负责检查字节码是否合法;准备负责为静态字段分配存储空间并设置默认值;解析负责将常量池中的符号引用转换为可以定位目标的直接引用,解析过程可以按需延迟执行。

最后是初始化,JVM 执行类的静态字段初始化语句和静态代码块,完成类级别的初始化逻辑。一个类在同一次加载对应的生命周期内只会成功初始化一次。

类加载器通常采用双亲委派模型。收到加载请求后,优先交给父加载器处理,父加载器无法完成时,再由当前加载器尝试加载。这样可以优先复用上层已经定义的类,保证核心类库的一致性。

另外,JVM 判断两个类是否相同,需要同时看类的全限定名和定义它的类加载器。即使类名相同,由不同类加载器定义,也属于不同的运行时类型。

实际项目中,Tomcat 就利用独立的类加载器隔离不同 Web 应用,使它们能够使用不同版本的依赖库。

2. 核心原理与关键概念

2.1 类加载的完整流程

整体流程:

加载 → 验证 → 准备 → 解析 → 初始化 → 使用 → 卸载

其中,验证、准备、解析统称为链接阶段。

阶段 核心原理
加载 获取类的二进制字节流,创建运行时类元数据及对应的 Class 对象
验证 检查字节码格式、类型和结构是否合法,确保能够安全执行
准备 为静态字段分配存储空间,设置默认值
解析 将常量池中的符号引用转换为直接引用
初始化 执行静态字段初始化和静态代码块
使用 创建对象、调用方法、访问字段等
卸载 满足条件时由 JVM 回收类的相关元数据

这里有两个关键区别。

第一,准备和初始化的赋值不同。

例如 static int count = 10,准备阶段通常先将 count 设置为默认值 0,初始化阶段再执行赋值操作,使其变为 10。

第二,解析可以延迟执行。

JVM 保证类在初始化前已经完成验证和准备,但不要求所有符号引用都提前解析完毕。部分引用可以在首次使用时解析。

2.2 双亲委派的底层流程

以应用类加载器加载业务类为例:

  1. 检查已加载 → 调用 findLoadedClass(),检查当前加载器是否已经加载过该类。

  2. 向上委派 → 调用父加载器的 loadClass(),逐级尝试加载。

  3. 父加载失败 → 当前加载器调用 findClass() 查找类。

  4. 定义类 → 找到字节码后,通过 defineClass() 将字节码转换为 JVM 中的类。

  5. 返回结果 → 返回对应的 Class 对象,必要时完成链接。

简化流程:

检查已加载 → 父加载器尝试 → 当前加载器查找 → 定义类 → 返回 Class

其中,父加载器是委派关系中的上级加载器,与 Java 类的继承关系无关。

2.3 关键概念

概念 含义
类加载器(ClassLoader) 根据类名定位或生成类定义,完成动态加载
双亲委派 优先由父加载器处理加载请求,父加载失败后当前加载器再尝试
定义类加载器 真正调用类定义逻辑、创建该运行时类型的加载器
类的运行时身份 由类的全限定名和定义类加载器共同决定
类初始化方法 <clinit> 由编译器根据静态字段初始化语句和静态代码块生成,负责执行类初始化逻辑

3. 高频面试追问

3.1 为什么 Java 要采用双亲委派模型?

主要是为了保证核心类库的一致性、减少重复定义,并建立清晰的类加载层次。

例如加载 java.lang.String 时,应用类加载器会先委派给上层加载器,最终由引导类加载器加载 JDK 中的 String。

这样,在默认委派机制下,应用代码就无法通过放置同名类来替换上层已经提供的核心类。

同时,不同子加载器也可以复用父加载器定义的公共类型,避免每个加载器都创建独立版本。

双亲委派是 ClassLoader 默认提供的一种设计策略,允许自定义加载器根据业务需求调整加载顺序。

继续追问:双亲委派能保证一个类在整个 JVM 中只加载一次吗?

不能保证整个 JVM 只有一份。同一个类名可以由不同的类加载器分别定义,形成不同的运行时类型。

它主要保证默认委派链上的类定义能够得到优先复用。

3.2 Java 中有哪些主要的类加载器?

Java 主要有三种内置类加载器,也支持自定义类加载器。

类加载器 主要职责
引导类加载器(Bootstrap ClassLoader) 加载 java.base 等核心模块中的类
平台类加载器(Platform ClassLoader) 加载由平台加载器负责的 JDK 平台类
应用类加载器(Application ClassLoader) 加载应用类路径及其负责的应用模块中的类
自定义类加载器 根据业务需求,从特定目录、网络或其他来源加载类

一般情况下,委派关系为:

应用类加载器 → 平台类加载器 → 引导类加载器

从 Java 9 开始,原来的扩展类加载器被平台类加载器取代,配合 Java 模块系统工作。

另外,引导类加载器由 JVM 实现,通常通过 null 表示,所以 String.class.getClassLoader() 通常返回 null。

3.3 哪些操作会触发类初始化?

类的初始化通常发生在第一次主动使用时。

常见情况包括:

  1. 使用 new 创建类的实例。

  2. 读取或修改类声明的普通静态字段。

  3. 调用类声明的静态方法。

  4. 使用 Class.forName() 等方式触发初始化。

  5. 初始化子类之前,先初始化父类。

  6. JVM 启动时初始化主类。

也有一些操作通常不会触发目标类初始化。

例如,访问编译期常量、获取 SomeClass.class,或者定义某个类的数组,都不会仅因为这些操作就初始化目标类。

另外,初始化子类时,父类必须先完成初始化。如果涉及声明了默认方法的接口,相关接口也可能需要提前初始化。

继续追问:static final 字段一定不会触发初始化吗?

不一定,关键在于它是否属于编译期常量。

例如 static final int MAX = 100 属于编译期常量,其他类读取时通常直接使用编译后保存的值,不会因此触发声明类初始化。

而 static final Integer MAX = 100 不属于编译期常量,读取该字段会触发声明类初始化。

3.4 Class.forName() 和 ClassLoader.loadClass() 有什么区别?

两者都能够动态加载类,主要区别在于是否主动触发类初始化。

Class.forName("com.example.User") 默认会加载并初始化指定类。

ClassLoader.loadClass("com.example.User") 通常只负责加载,不会主动执行类的静态初始化逻辑。

因此,如果某个类有静态代码块,使用前一种方式可能立即执行,后一种方式则可以等到真正主动使用该类时再执行。

另外,Class.forName() 还提供包含 initialize 参数的重载,可以显式控制是否初始化。

实际场景中,反射创建对象、动态加载驱动或插件时,可以根据需求选择合适的方式。

3.5 为什么同名类会出现 ClassCastException?

因为 JVM 判断类型身份时,同时考虑全限定名和定义类加载器。

假设插件 A 和插件 B 分别使用独立的类加载器,都加载了 com.example.User。

即使两个类的字节码完全相同,JVM 也会将它们识别为两个不同的类型。

因此,将插件 A 创建的 User 对象强制转换为插件 B 加载的 User 类型,就可能出现 ClassCastException。

这种机制可以实现插件隔离,但跨加载器通信时,需要让双方共享接口或公共类型,并确保这些类型由共同的加载器定义。

3.6 如何实现自定义类加载器?应该重写哪个方法?

通常继承 ClassLoader,优先重写 findClass()。

实现流程是:

接收类名 → 获取字节码 → 调用 defineClass() → 返回 Class 对象

ClassLoader 已经在 loadClass() 中实现了默认委派逻辑。当父加载器无法找到目标类时,才会调用自定义的 findClass()。

这样可以保留双亲委派带来的类型一致性。

如果业务需要改变加载顺序,例如针对部分插件类采用子加载器优先策略,就需要有选择地重写 loadClass()。

这种方式要额外处理类重复定义、类型冲突和加载并发问题。

3.7 Tomcat 为什么要调整双亲委派?SPI 又是怎么处理的?

这两种机制主要解决不同的类加载需求。

Tomcat 主要解决应用隔离问题。

一个 Tomcat 实例可以部署多个 Web 应用,不同应用可能依赖同一个框架的不同版本。

Tomcat 为每个 Web 应用创建独立的类加载器,通常优先加载当前应用内部的类和依赖,使不同应用能够使用各自的类库版本。

不过,JDK 核心类和容器负责的部分公共 API 仍然遵循优先委派规则,避免出现不兼容的类型定义。

SPI 主要解决上层框架发现下层实现类的问题。

例如 JDK 中的 ServiceLoader 可以使用线程上下文类加载器,查找应用提供的服务实现。

这样,框架代码即使由上层加载器加载,也能够通过指定的加载器发现应用类路径中的实现。

继续追问:线程上下文类加载器是否直接破坏了双亲委派?

线程上下文类加载器本身只是为线程提供一个可使用的类加载器引用。

SPI 可以利用它选择应用类加载器完成服务发现,突破上层加载器无法直接查找下层实现的限制。

这个过程不要求应用类加载器改变自身默认的父优先加载规则。

3.8 Java 的类可以卸载吗?热部署是怎么实现的?

Java 类可以卸载,但需要满足对应类加载器能够被垃圾回收的条件。

在 HotSpot JVM 中,类的元数据主要存储在元空间。对于自定义类加载器定义的类,如果对应加载器已经不可达,并且满足 JVM 的卸载条件,就可能在垃圾回收过程中释放相关元数据。

热部署通常利用独立的类加载器管理应用或插件。

更新时,创建新的类加载器加载新版本,停止旧版本的使用,并清理旧加载器及其关联资源。

需要注意,同一个类加载器无法通过再次定义同名类来完成普通意义上的重新加载,因此通常需要新的加载器。

如果旧加载器仍被线程、静态缓存或线程上下文类加载器等引用持有,就可能无法回收,导致元空间持续增长。

3.9 ClassNotFoundException 和 NoClassDefFoundError 有什么区别?如何排查?

两者都可能与类加载失败有关,但发生的场景不同。

ClassNotFoundException 通常出现在显式按名称查找类时。

例如执行 Class.forName(),指定的类没有出现在对应类加载器可访问的范围内,就会抛出这个受检异常。

NoClassDefFoundError 通常出现在 JVM 使用某个类时,无法取得有效类定义的情况。

例如编译时存在某个依赖类,运行时依赖缺失;或者类初始化曾经失败,后续再次使用该类。

如果静态初始化代码首次执行时抛出普通异常,通常会先出现 ExceptionInInitializerError,后续使用可能出现 NoClassDefFoundError。

实际排查时,主要检查:

  1. 运行时 JAR 是否缺失,依赖版本是否一致。

  2. 类是否被错误的类加载器加载,或者加载器之间是否存在可见性问题。

  3. 完整异常堆栈中是否存在更早的初始化失败或链接错误。

在 HotSpot 中,可以使用 -Xlog:class+load=info 查看类加载信息,也可以使用 jcmd 的 VM.classloaders、VM.classloader_stats 检查类加载器情况。

4. 易混淆概念对比

4.1 加载与初始化

对比项 加载 初始化
主要作用 将字节码转换为运行时类型 执行类的静态初始化逻辑
核心操作 获取字节码、创建类元数据 执行静态字段初始化语句和静态代码块
触发时机 JVM 需要加载该类型时 首次主动使用该类型时
是否执行静态代码块 不执行 执行

4.2 准备与初始化

对比项 准备 初始化
所属阶段 链接 初始化
静态字段处理 创建字段并设置默认值 执行显式初始化逻辑
static int a = 10 通常先设置为 0 赋值为 10
是否执行用户编写的静态初始化代码 否 是

4.3 Class.forName() 与 loadClass()

对比项 Class.forName(name) ClassLoader.loadClass(name)
核心目的 按名称获取类,并默认初始化 按名称加载类
默认触发初始化 是 否
可否指定加载器 可以,通过重载 使用调用它的加载器
常见场景 反射、动态初始化 自定义加载、插件框架

4.4 ClassNotFoundException 与 NoClassDefFoundError

对比项 ClassNotFoundException NoClassDefFoundError
类型 受检异常 错误(Error)
典型触发场景 按名称显式加载类时找不到 JVM 使用类时无法取得有效类定义
常见原因 类路径缺失、加载器不可见 运行时依赖缺失、初始化曾经失败
处理思路 检查类名、依赖和加载器 检查依赖、完整错误链及初始化过程

4.5 父优先与子优先加载

对比项 父优先加载 子优先加载
加载顺序 父加载器优先 当前加载器优先
主要优势 核心类型一致、公共类型复用 类库版本隔离
主要限制 不便隔离上层已有的同名依赖 容易产生重复类型和转换冲突
应用场景 Java 默认类加载机制 Tomcat Web 应用、部分插件系统

5. 总结

Java 的类加载机制负责将字节码转换成 JVM 可以使用的运行时类型,核心过程包括加载、链接和初始化。加载负责创建类的运行时表示,链接负责验证字节码、准备静态字段和解析引用,初始化负责执行静态初始化逻辑。

默认情况下,类加载器采用双亲委派模型,通过父加载器优先加载来保证核心类库的一致性和公共类型的复用。同时,JVM 通过类的全限定名和定义类加载器共同确定类型身份,这也为应用隔离和插件化提供了基础。

需要注意,类的加载和初始化可以发生在不同时间,双亲委派也允许根据业务需求调整。Tomcat 的应用隔离、SPI 的服务发现以及动态插件系统,都是类加载机制在实际项目中的典型应用。