在 JVM 中,类加载器(ClassLoader)负责将磁盘上的 .class 文件读取到内存(元空间)中。Java 采用的是一种层级结构,不同的加载器负责加载不同路径下的类库。
1. 四种主要的类加载器
从 JDK 8 到 JDK 9+(模块化之后),类加载器的名称和职责稍有变化,但逻辑核心是一致的:
① 启动类加载器 (Bootstrap ClassLoader)
- 地位 :它是加载器中的"老祖宗",由 C++ 编写,嵌套在 JVM 内部。
- 职责 :负责加载 Java 的核心类库 (如
$JAVA_HOME/lib目录下的rt.jar、resources.jar等)。 - 特点 :你在 Java 代码中无法直接获取它,返回通常为
null。它加载的是像java.lang.Object、java.lang.String这样最基础的类。
② 扩展类加载器 (Extension ClassLoader) / 平台类加载器 (Platform ClassLoader)
- JDK 8 :叫 Extension ClassLoader,负责加载
$JAVA_HOME/lib/ext目录下的扩展类库。 - JDK 9+ :由于模块化,改名为 Platform ClassLoader 。它负责加载 Java 平台的一些特定模块(如
java.sql、java.xml等)。
③ 应用程序类加载器 (Application ClassLoader)
- 别名:系统类加载器 (System ClassLoader)。
- 职责 :负责加载用户类路径(ClassPath)上指定的类库。
- 特点:这是我们在开发中最常用的加载器,如果你没有自定义加载器,你写的代码默认都由它加载。
④ 自定义类加载器 (Custom ClassLoader)
- 实现 :通过继承
java.lang.ClassLoader类并重写findClass方法实现。 - 用途 :
- 加密保护 :加载加密过的
.class文件并在内存中解密。 - 非标准来源:从数据库、网络甚至是云端加载类。
- 热部署/隔离:像 Tomcat 这种 Web 容器,会为每个 Web 应用创建独立的加载器,实现应用间的类隔离。
- 加密保护 :加载加密过的
2. 双亲委派模型 (Parent Delegation Model)
这是 Java 类加载器的核心工作机制。当一个类加载器收到加载请求时,它不会自己先去加载,而是:
- 向上委托 :把请求委派给父类加载器,一直委派到顶层的 Bootstrap ClassLoader。
- 向下尝试:只有当父加载器反馈自己无法加载(在它的搜索范围没找到)时,子加载器才会尝试自己去加载。
为什么要有这个模型?(核心作用)
- 安全性 :防止核心 API 被篡改。如果没有这个机制,你写一个
java.lang.String类并让 ApplicationClassLoader 加载,那系统里就会有两个 String。有了委派机制,系统永远优先使用 Bootstrap 加载的官方版。 - 避免重复加载:保证了在整个 JVM 范围内,同一个类只会被加载一次。
3.类加载过程
JVM 的类加载过程是将磁盘上的 .class 字节码文件转化成 元空间(Metaspace) 中运行数据结构的全过程。这个过程可以细分为五个阶段:加载、验证、准备、解析、初始化。
1. 加载 (Loading)
这是类加载的第一步,主要完成三件事:
- 获取二进制流 :通过类的全限定名(如
java.lang.String)找到对应的字节码文件。 - 转化结构:将字节码中的静态存储结构转化为方法区(元空间)的运行时数据结构。
- 生成 Class 对象 :在堆中生成一个代表该类的
java.lang.Class对象,作为访问元空间中这些数据的入口。
2. 链接 (Linking)
链接阶段负责将类的二进制数据合并到 JVM 的运行时状态中,它包含三个小阶段:
① 验证 (Verification)
- 目的:确保加载的类符合 JVM 规范,不会危害虚拟机安全。
- 内容 :检查文件格式(魔数
CAFEBABE)、元数据验证、字节码验证、符号引用验证等。
② 准备 (Preparation)
- 目的 :为类变量(static 修饰的变量)分配内存并设置初始零值。
- 关键点 :此时不会执行你的赋值逻辑。
- 例如:
public static int value = 123;在准备阶段,value的值是0而不是123。 - 例外 :如果是
static final修饰的常量,由于是常量,准备阶段就会直接赋予代码中的真实值。
- 例如:
③ 解析 (Resolution)
- 目的 :将常量池内的符号引用 替换为直接引用。
- 内容 :比如代码中调用了
methodA(),在字节码里只是一个字符串符号,解析阶段会将其指向该方法在内存中的实际地址(偏移量)。
3. 初始化 (Initialization)
这是类加载过程的最后一步,也是真正开始执行 Java 代码的阶段。
- 核心逻辑 :执行类构造器
<clinit>方法的过程。 - 动作 :
- 合并所有静态变量的赋值动作和
static { ... }静态代码块。 - 按语句在源文件中出现的顺序执行。
- 父类优先 :JVM 会保证在子类的
<clinit>执行前,父类的<clinit>已经执行完毕。
- 合并所有静态变量的赋值动作和
4.使用
使用类静态方法或创建对象
5.卸载
JVM 卸载类(Class Unloading)是一个比对象回收(Object GC)严苛得多的过程。在元空间(Metaspace)中,一个类只有在同时满足以下三个条件时,才会被垃圾回收器回收。
1. 条件一:该类所有的实例都已被回收
- 要求:在 Java 堆中,已经不存在该类及其任何派生子类的任何实例对象。
- 逻辑:只要还有一个活着的实例,类定义的"模板"就必须留在元空间里,否则实例就失去了结构支撑。
2. 条件二:加载该类的 ClassLoader 已被回收
- 要求 :负责加载这个类的
ClassLoader实例本身必须已经被 GC 回收。 - 为什么这一条最难?
- 对于 启动类加载器 (Bootstrap) 、平台类加载器 (Platform) 和 应用程序类加载器 (App),它们在 JVM 运行期间几乎永远不会被回收。
- 因此,我们编写的普通业务类,在正常的程序运行中基本不可能被卸载。
- 场景 :类卸载通常只发生在频繁使用 自定义类加载器 的场景,如:
- OSGi 或 热部署 框架:动态加载一个插件,卸载插件时销毁对应的加载器。
- JSP 引擎:每次 JSP 修改后,Tomcat 可能会抛弃旧的加载器,创建新的来重新加载。
3. 条件三:该类对应的 java.lang.Class 对象没有在任何地方被引用
- 要求:无法在任何地方通过反射(Reflection)访问该类的方法或字段。
- 细节 :如果你的代码里还存着
Class<?> clazz = User.class;这种强引用,或者有线程正在执行该类的方法,那么它就不是死透的。
类卸载条件汇总表
| 维度 | 检查项 | 判定标准 |
|---|---|---|
| 实例层 | 堆内存 (Heap) | 实例总数 = 0 且被 GC 清理 |
| 加载层 | 类加载器 (ClassLoader) | 加载器实例已被回收 |
| 引用层 | 反射与根搜索 (GC Roots) | 无法通过任何路径触达该类的 Class 对象 |
6. 什么时候会触发"初始化"?
JVM 采用的是 懒加载 机制,只有在"主动使用"一个类时才会触发初始化:
- 使用
new关键字创建实例。 - 访问或修改类的静态变量(非
final)。 - 调用类的静态方法。
- 使用
java.lang.reflect进行反射调用。 - 初始化一个类时,发现其父类还没初始化,则先触发父类初始化。
- 虚拟机启动时的执行主类(包含
main方法的那个类)。